Přeskočit na hlavní obsah

Application configuration

Application configuration

This documentation provides a comprehensive guide to establishing application environments for various stages and configuring your AVA application accordingly. The guide is divided into two sections: the legacy method of environment setup and the newer approach involving dynamic loading. The newer approach is available starting from version 170.2.0 of the libraries. A complete list will be provided subsequently.

Common setup and configuration

In the angular.json we specify:

configurations": {
"dynamic": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.dynamic.ts"
}
],
}
}

In this code, we specify that we will be using the environment.dynamic.ts file for the deployment.

Than there is a appsettings.json file in the assets folder:

{
"production": false,
"IdentityServer_URL": "https://#{Dns_AsolAppWebSite}#/api/asol/idp",
"IdentityManager_URL": "https://#{Dns_AsolAppWebSite}#/api/asol/idm",
"PlatformStorePdm_URL": "https://#{Dns_AsolAppWebSite}#/api/asol/pdm",
"ContentManager_Url": "https://#{Dns_AsolAppWebSite}#/api/asol/cnt",
"PlatformStoreStore_URL": "https://#{Dns_AsolAppWebSite}#/api/asol/store",
"PlatformStoreOrder_URL": "https://#{Dns_AsolAppWebSite}#/api/asol/ord",
"SubjectManagement_URL": "https://#{Dns_AsolAppWebSite}#/api/asol/idmsm",
"Guidance_Url": "https://#{Dns_AsolAppWebSite}#:44305",
"PortalApp_Url": "https://#{Dns_AsolAppWebSite}#/apps/portal/my-apps",
"CaptchaKey": "#{CaptchaClientKey}#",
"gtmId": "#{GTMId}#",
"Telemetry": {
"instrumentationKey": "3dcb720c-e4bd-4028-8d40-755f89a1c757",
"endPointUrl": "https://#{Dns_AsolAppWebSite}#:40901",
"apiVersion": "1.0",
"serviceName": "",
"serviceVersion": ""
},
"sideMenuDebug": "#{EnableDebug}#",
"topMenuDebug": "#{EnableDebug}#",
"enableLeadTracking": "#{EnableLeadTracking}#",
"TelemetryKey": "3dcb720c-e4bd-4028-8d40-755f89a1c757",
"Telemetry_Url": "https://#{Dns_AsolAppWebSite}#:40901",
"Community_Url": "https://#{Dns_AsolAppWebSite}#:50061",
"LogData_Url": "https://#{Dns_AsolAppWebSite}#:40311",
"DomainCatalog_Url": "https://#{Dns_AsolAppWebSite}#:44351",
"CustomAttributes_Url": "https://#{Dns_AsolAppWebSite}#/api/asol/cuatt",
"portal_Url": "https://#{Dns_AsolAppWebSite}#/apps/portal",
"Product_Url": "https://#{Dns_AsolAppWebSite}#/apps/product",
"Store_App_Url": "https://#{Dns_AsolAppWebSite}#/apps/store/171",
"GlobalCnt_Url": "https://#{AsolGlobalCntSource}#/api/asol/cnt"
}

There are several variables that are replaced during the deployment process. Specifically, #{AsolGlobalCntSource}# and #{Dns_AsolAppWebSite}# are crucial for supporting dynamic DNS. The first variable should point to the production server, where the main content manager resides, while the second should define the current DNS.

Legacy Method - Direct Import

The initial method involves directly importing appsettings from a JSON file. In this approach, the settings are imported into the environment.dynamic.ts file and utilized within the application. The primary disadvantage of this method is that the variables defined in the appsettings.json file are embedded into the JavaScript files in the final build bundle. Consequently, to change the DNS, it is necessary to locate and replace all instances of these variables across all built JavaScript files. This limitation is the primary reason for transitioning to a more modern method.

import config from "../assets/appsettings.json";

export const environment = {
production: true,

identityServerUrl: config.IdentityServer_URL,
identityManagerUrl: config.IdentityManager_URL,
subjectManagementUrl: config.SubjectManagement_URL,
platformStorePdmUrl: config.PlatformStorePdm_URL,
platformStoreStoreUrl: config.PlatformStoreStore_URL,
};

New Method

Starting with @asol-platform/core@170.2.0, a new approach has been implemented. Prior to the application startup, the assets file is loaded dynamically, utilizing observables and APP_INITIALIZER. This method dynamically loads the assets/appsettings.json file of the host application. Consequently, to change the DNS, it is only necessary to modify this single file in the final bundle.

Library versions
  • @asol-platform/core@170.2.6
  • @asol-platform/services@170.2.2
  • @asol-platform/authentication@170.2.0
  • @asol-platform/common@170.2.0

The controls libary does not need update, as it has no .forRoot configuration, which needs to be updated.

Migration

For migration, a backward-compatible approach has been adopted. By default, the legacy approach is selected. To employ the new method, the following line must be added to the appsettings.json file:

{
"DiscoveryBehaviour": { "Discovery": true }
}

If the DiscoveryBehaviour property is absent or if DiscoveryBehaviour.Discovery is set to false, the legacy approach will be utilized. Conversely, if DiscoveryBehaviour.Discovery is true, the data will be loaded from the appsettings.json file. This configuration should already be in place for all libraries.

Using the configuration

Legacy method

In the app.module.ts file, each library may include a .forRoot() method, where the environment attribute to be utilized is specified. For example, for the services library:

AsolPlatformServicesModule.forRoot({
subjectManagementUrl: environment.subjectManagementUrl,
contentManagerUrl: environment.contentManagerUrl,
platformStorePdmUrl: environment.platformStorePdmUrl,
identityManagerUrl: environment.identityManagerUrl,
identityServerUrl: environment.identityServerUrl,
telemetryConfig: {
instrumentationKey: environment.telemetryKey,
endPointUrl: environment.telemetryUrl,
apiVersion: '1.0',
serviceName: '',
serviceVersion: '1.0',
},
gtmId: environment.gtmId,
}),

Subsequently, within the application, these configuration files can be utilized to communicate with the backend or other frontend applications. For example:

Module federation:

const m = await loadRemoteModule({
type: "module",
remoteEntry: `${environment.storeAppUrl}/remoteEntry.js`,
exposedModule: "./PublicStoreSelectionComponent",
});
this.container()?.createComponent(m.PublicStoreSelectionComponent);

Backend API request:

export class YourService {
private dataService = inject(AsolApiDataService);

method() {
return this.dataService.create<unknown>(
`${environment.platformStoreStoreUrl}/api/v1/Message/public`,
{ ...data, captcha }
);
}
}

New approach

With new approach every attribute should be defined in the appsettings.json file (after the deployment). As we load it in the core library, we share it via AsolDiscoveryService. This service provides observable-based access to the loaded configuration. There are several methods available:

  • getConfig(appId?): Observable<AsolConfig | undefined>;
    • Returns the entire configuration object for the specified app
  • getStringAttribute(attributeName, appId?): Observable<string | undefined>;
    • Returns a string attribute from the configuration
  • getBooleanAttribute(attributeName, appId?): Observable<boolean | undefined>;
    • Returns a boolean attribute from the configuration
  • getAsolConfigAttribute(attributeName, appId?): Observable<AsolConfig | undefined>;
    • Returns an AsolConfig object attribute from the configuration
  • isHostConfigReady(): Observable<boolean>;
    • Returns an observable indicating when the host configuration is ready to use

Additionally, there are signals available for non-observable usage:

  • isUsingConfigs: Signal<boolean>;
    • Determines if the appsettings config is being used
  • isUsingDiscovery: Signal<boolean>;
    • Determines if the discovery backend service is being used

Module federation:

export class Component {
private discovery = inject(AsolDiscoveryService);

method() {
this.discovery.getStringAttribute("Store_App_Url").subscribe((storeUrl) => {
const m = await loadRemoteModule({
type: "module",
remoteEntry: `${storeUrl}/remoteEntry.js`,
exposedModule: "./PublicStoreSelectionComponent",
});
this.container()?.createComponent(m.PublicStoreSelectionComponent);
});
}
}

Backend API request:

export class YourService {
private dataService = inject(AsolApiDataService);
private discovery = inject(AsolDiscoveryService);

method() {
return this.discovery.getStringAttribute("PlatformStoreStore_URL").pipe(
switchMap((storeAPI) => {
return this.dataService.create<unknown>(
`${storeAPI}/api/v1/Message/public`,
{ ...data, captcha }
);
})
);
}
}

Development with the configuration

Legacy development

The legacy solution should already be implemented in the code and in the template, which of the project when it is created. The only thing which needs to be done is changing environement.ts file based on the wanted configuration.

New approach development

For local development a second appsetting.json is needed. It can be a part of the current environment.ts files and then provided to the app.module.ts:

export const environment = {
production: false,
appsettingsFileName: "appsettings.dev.json",
};
AsolPlatformCoreModule.forRoot({
appId: 'Your-app-id',
appsettingsFileName: environment.appsettingsFileName,
isDevMode: !environment.production,
}),

Then a file named appsettings.dev.json should contain all of the required settings:

{
"DiscoveryBehaviour": { "Discovery": true },
"production": false,
"IdentityServer_URL": "https://nqa.avaplace.com/api/asol/idp",
"IdentityManager_URL": "https://nqa.avaplace.com/api/asol/idm",
"PlatformStorePdm_URL": "https://nqa.avaplace.com/api/asol/pdm",
"ContentManager_Url": "https://nqa.avaplace.com/api/asol/cnt",
"PlatformStoreStore_URL": "https://nqa.avaplace.com/api/asol/store",
"PlatformStoreOrder_URL": "https://nqa.avaplace.com:44324",
"SubjectManagement_URL": "https://nqa.avaplace.com:44328",
"Guidance_Url": "https://nqa.avaplace.com:44305",
"PortalApp_Url": "https://nqa.avaplace.com:50009/my-apps",
"CaptchaKey": "#{CaptchaClientKey}#",
"gtmId": "#{GTMId}#",
"Telemetry": {
"instrumentationKey": "3dcb720c-e4bd-4028-8d40-755f89a1c757",
"endPointUrl": "https://nqa.avaplace.com:40901",
"apiVersion": "1.0",
"serviceName": "",
"serviceVersion": ""
},
"sideMenuDebug": "#{EnableDebug}#",
"topMenuDebug": "#{EnableDebug}#",
"enableLeadTracking": "#{EnableLeadTracking}#",
"SubjectManagement_Url": "https://nqa.avaplace.com:44328",
"TelemetryKey": "3dcb720c-e4bd-4028-8d40-755f89a1c757",
"Telemetry_Url": "https://nqa.avaplace.com:40901",
"Community_Url": "https://nqa.avaplace.com:50061",
"LogData_Url": "https://nqa.avaplace.com:40311",
"DomainCatalog_Url": "https://nqa.avaplace.com:44351",
"CustomAttributes_Url": "https://nqa.avaplace.com:44355",
"portal_Url": "https://nqa.avaplace.com:50009",
"Product_Url": "https://nqa.avaplace.com:50129",
"GlobalCnt_Url": "https://avaplace.com/api/asol/cnt",

"Store_App_Url": "http://localhost:10000",
"Store_App_Url_Assets_Testing": "http://localhost:4200/assets/appsettings.store.json"
}

With this setup, we have everything needed for the local development. However, only for the host applications.

Doing some logic in the APP_INITIALIZER

Sometimes you need another APP_INITIALIZER to run logic before the app starts and wait for it to finish (e.g., preloading tenant feature flags or branding assets). We propose a solution to manage entry logic and ensure it can be executed after the application configuration has been loaded. The isHostConfigReady() method of the AsolDiscoveryService has been made available specifically for this purpose—to allow other APP_INITIALIZER instances to interact with the dynamically loaded environment configuration.

...
import { APP_INITIALIZER } from '@angular/core';
import {
AsolDiscoveryService,
AsolPlatformCoreModule,
} from '@asol-platform/core';
import { environment } from '../environments/environment';
import { initializeHeadLink } from './shared/services/head-link.function';

@NgModule({
imports: [
AsolPlatformCoreModule.forRoot({
appId: 'ASOLEU-Public-portal-AP-',
appsettingsFileName: environment.appsettingsFileName,
isDevMode: !environment.production,
}),
]
providers: [
{
provide: APP_INITIALIZER,
useFactory: initializeHeadLink,
deps: [AsolDiscoveryService],
multi: true,
},
]
})
export class AppModule {}
import { AsolDiscoveryService } from "@asol-platform/core";
import { Observable, tap } from "rxjs";

export function initializeHeadLink(
discoveryService: AsolDiscoveryService
): () => Observable<unknown> {
return (): Observable<unknown> => {
return discoveryService.isHostConfigReady().pipe(
tap(() => {
// do your logic here, configuration is now ready
})
);
};
}

Module federation and the dynamic appsettings loading

Currently we should be able to work with module federation on local environemnts. Either we are using proxy config, to load data from the server, or we are running both applications locally.

In the first case, it works perfectly with dynamic appsettings. As it is loaded via proxy too, there are no CORS errors.

On the other hand, when running both applications locally, we need to specify local assets testing URL. Store applications for example requests attribute Store_App_Url_Assets_Testing to be in the configuration. When looking to the URL, we should notice, that it wants appsetting.store.json from local application. So we need to copy the appsettings.json from remote application to the host application. This way there will be no CORS errors too, and developer can test the combination of host and remote app as it would be deployed.

Module federation setup

To achieve already mentioned configuration of the remote application, we need to load the configuration dynamically. The AsolDiscoveryService can be used to load the configuration for remote applications:

import { inject, Injectable } from "@angular/core";
import { AsolDiscoveryService } from "@asol-platform/core";
import { AsolApiDataService } from "@asol-platform/services";
import { mergeMap, Observable } from "rxjs";
import { environment } from "../../../../environments/environment";
import { STORE_API_ROUTES } from "../constants/store-api-routes.constant";
import { AsolStoreSearchResult } from "../models/asol-store-search-result.interface";
import { StoreCatalogSearchParams } from "../models/store-catalog-search-params.interface";
import { APPLICATION_CODE } from "./../../../shared/constants/application-code.constant";

@Injectable({
providedIn: "root",
})
export class StoreCatalogService {
private discovery = inject(AsolDiscoveryService);
constructor(private apiDataService: AsolApiDataService) {}

search(
searchParams: StoreCatalogSearchParams
): Observable<AsolStoreSearchResult> {
// Load the remote app configuration
return this.discovery
.loadConfig(
APPLICATION_CODE.STORE,
"Store_App_Url",
"Store_App_Url_Assets_Testing"
)
.pipe(
mergeMap((config) => {
let url = environment.platformStoreStoreUrl;

if (config?.["PlatformStoreStore_URL"]) {
url = config["PlatformStoreStore_URL"];
}

return this.getResult(url, searchParams);
})
);
}

private getResult(url: string, searchParams: StoreCatalogSearchParams) {
return this.apiDataService.getOne<AsolStoreSearchResult>(
url + STORE_API_ROUTES.BASE + STORE_API_ROUTES.SEARCH,
searchParams
);
}
}

The loadConfig method accepts three parameters:

  • appCode: The code of the remote application to load configuration for
  • remoteAccessAttributeUrl: The attribute name in the appsettings.json that contains the URL of the remote app
  • remoteAssetsTestingUrl: The fallback URL for local testing of two applications running locally

For local testing with two applications running simultaneously, you need to specify a testing URL that points to the remote app's appsettings. For example, if testing the store application locally, the Store_App_Url_Assets_Testing attribute should point to a copy of the store's appsettings.json file in the host application's assets folder (e.g., http://localhost:4200/assets/appsettings.store.json). This avoids CORS errors when loading the remote application configuration.