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 forremoteAccessAttributeUrl: The attribute name in the appsettings.json that contains the URL of the remote appremoteAssetsTestingUrl: 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.