From 9ba340f6cbe1f16e87d63a668eb0885ce8015f6d Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 12 Sep 2024 14:11:13 -0700 Subject: [PATCH 1/5] feat: wip deployment config injection token --- src/main.ts | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/main.ts b/src/main.ts index e6c5f3302d..9dc3321e31 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,16 +5,36 @@ import { defineCustomElements } from "@ionic/pwa-elements/loader"; import { AppModule } from "./app/app.module"; import { environment } from "./environments/environment"; +import { DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, IDeploymentRuntimeConfig } from "packages/data-models"; +import { DEPLOYMENT_CONFIG } from "./app/shared/services/deployment/deployment.service"; if (environment.production) { enableProdMode(); } -platformBrowserDynamic() - .bootstrapModule(AppModule) - .catch((err) => console.log(err)); +/** Load deployment config from asset json, returning default config if not available*/ +const loadConfig = async () => { + const res = await fetch("/assets/app_data/deployment.json"); + if (res.status === 200) { + const deploymentConfig = await res.json(); + console.log("[DEPLOYMENT] config loaded", deploymentConfig); + return deploymentConfig; + } else { + console.warn("[DEPLOYMENT] config not found, using defaults"); + return DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS; + } +}; -if (!Capacitor.isNative) { - // Call PWA custom element loader after the platform has been bootstrapped - defineCustomElements(window); -} +// Initialise platform once deployment config has loaded, setting the value of the +// global DEPLOYMENT_CONFIG injection token from the loaded json +// https://stackoverflow.com/a/62151011 +loadConfig().then((deploymentConfig) => { + platformBrowserDynamic([{ provide: DEPLOYMENT_CONFIG, useValue: deploymentConfig }]) + .bootstrapModule(AppModule) + .catch((err) => console.log(err)); + + if (!Capacitor.isNativePlatform()) { + // Call PWA custom element loader after the platform has been bootstrapped + defineCustomElements(window); + } +}); From 8c3f7e3234762eeba09ecc84b8d408fbaf13456f Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 12 Sep 2024 17:19:09 -0700 Subject: [PATCH 2/5] chore: code tidying --- packages/data-models/deployment.model.ts | 6 +-- .../services/deployment/deployment.service.ts | 49 ++++++------------- src/main.ts | 15 ++++-- 3 files changed, 28 insertions(+), 42 deletions(-) diff --git a/packages/data-models/deployment.model.ts b/packages/data-models/deployment.model.ts index 473d4b6018..752a02a411 100644 --- a/packages/data-models/deployment.model.ts +++ b/packages/data-models/deployment.model.ts @@ -1,5 +1,5 @@ import type { IGdriveEntry } from "../@idemsInternational/gdrive-tools"; -import type { IAppConfig } from "./appConfig"; +import type { IAppConfigOverride } from "./appConfig"; /** Update version to force recompile next time deployment set (e.g. after default config update) */ export const DEPLOYMENT_CONFIG_VERSION = 20240910.0; @@ -21,7 +21,7 @@ export interface IDeploymentRuntimeConfig { endpoint?: string; }; /** Optional override of any provided constants from data-models/constants */ - app_config: IAppConfig; + app_config: IAppConfigOverride; /** 3rd party integration for logging services */ error_logging?: { /** sentry/glitchtip logging dsn */ @@ -159,7 +159,7 @@ export const DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS: IDeploymentRuntimeConfig = { db_name: "plh", endpoint: "https://apps-server.idems.international/api", }, - app_config: {} as any, // populated by `getDefaultAppConstants()`, + app_config: {}, firebase: { config: null, diff --git a/src/app/shared/services/deployment/deployment.service.ts b/src/app/shared/services/deployment/deployment.service.ts index eb897ab8b2..c50287a8b9 100644 --- a/src/app/shared/services/deployment/deployment.service.ts +++ b/src/app/shared/services/deployment/deployment.service.ts @@ -1,8 +1,16 @@ -import { Injectable } from "@angular/core"; -import { AsyncServiceBase } from "../asyncService.base"; -import { HttpClient } from "@angular/common/http"; -import { catchError, map, of, firstValueFrom } from "rxjs"; -import { DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, IDeploymentRuntimeConfig } from "packages/data-models"; +import { Inject, Injectable, InjectionToken } from "@angular/core"; +import { IDeploymentRuntimeConfig } from "packages/data-models"; +import { SyncServiceBase } from "../syncService.base"; + +/** + * Token to inject deployment config value into any service. + * This is populated from json file before platform load, as part of src\main.ts + * + * Can be used directly in the constructor via `@Inject(DEPLOYMENT_CONFIG)`, + * or values accessed from the DeploymentService + */ +export const DEPLOYMENT_CONFIG: InjectionToken = + new InjectionToken("Application Configuration"); @Injectable({ providedIn: "root" }) /** @@ -15,35 +23,8 @@ import { DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, IDeploymentRuntimeConfig } from "pa * Services that access the deployment config therefore do not need to await * DeploymentService init/ready methods. */ -export class DeploymentService extends AsyncServiceBase { - constructor(private http: HttpClient) { +export class DeploymentService extends SyncServiceBase { + constructor(@Inject(DEPLOYMENT_CONFIG) public readonly config: IDeploymentRuntimeConfig) { super("Deployment Service"); - this.registerInitFunction(this.initialise); - } - - /** Private writeable config to allow population from JSON */ - private _config = DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS; - - /** Read-only access to deployment runtime config */ - public get config() { - return this._config; - } - - /** Load active deployment configuration from JSON file */ - private async initialise() { - const deployment = await firstValueFrom(this.loadDeployment()); - if (deployment) { - this._config = deployment; - } - } - - private loadDeployment() { - return this.http.get("assets/app_data/deployment.json").pipe( - catchError(() => { - console.warn("No deployment config available"); - return of(null); - }), - map((v) => v as IDeploymentRuntimeConfig) - ); } } diff --git a/src/main.ts b/src/main.ts index 9dc3321e31..c01efccbfb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,7 +13,7 @@ if (environment.production) { } /** Load deployment config from asset json, returning default config if not available*/ -const loadConfig = async () => { +const loadConfig = async (): Promise => { const res = await fetch("/assets/app_data/deployment.json"); if (res.status === 200) { const deploymentConfig = await res.json(); @@ -21,13 +21,18 @@ const loadConfig = async () => { return deploymentConfig; } else { console.warn("[DEPLOYMENT] config not found, using defaults"); - return DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS; + return { ...DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, app_config: {} as any }; } }; -// Initialise platform once deployment config has loaded, setting the value of the -// global DEPLOYMENT_CONFIG injection token from the loaded json -// https://stackoverflow.com/a/62151011 +/** + * Initialise platform once deployment config has loaded, setting the value of the + * global DEPLOYMENT_CONFIG injection token from the loaded json + * https://stackoverflow.com/a/62151011 + * + * The configuration is loaded before the rest of the platform so that config values + * can be used to configure modules imported in app.module.ts + */ loadConfig().then((deploymentConfig) => { platformBrowserDynamic([{ provide: DEPLOYMENT_CONFIG, useValue: deploymentConfig }]) .bootstrapModule(AppModule) From 42410db29500c9a452afa6f41e36b95b0789ac64 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 12 Sep 2024 23:16:35 -0700 Subject: [PATCH 3/5] refactor: analytics module --- packages/data-models/deployment.model.ts | 17 ++++++- .../scripts/src/tasks/providers/appData.ts | 13 +++--- src/app/app.module.ts | 29 ++++-------- src/app/deployment-features.module.ts | 17 +++++++ .../services/analytics/analytics.module.ts | 45 +++++++++++++++++++ src/app/shared/services/analytics/index.ts | 2 + src/environments/environment.prod.ts | 1 - src/environments/environment.ts | 4 -- 8 files changed, 96 insertions(+), 32 deletions(-) create mode 100644 src/app/deployment-features.module.ts create mode 100644 src/app/shared/services/analytics/analytics.module.ts create mode 100644 src/app/shared/services/analytics/index.ts diff --git a/packages/data-models/deployment.model.ts b/packages/data-models/deployment.model.ts index 752a02a411..46ac67f3f1 100644 --- a/packages/data-models/deployment.model.ts +++ b/packages/data-models/deployment.model.ts @@ -2,7 +2,7 @@ import type { IGdriveEntry } from "../@idemsInternational/gdrive-tools"; import type { IAppConfigOverride } from "./appConfig"; /** Update version to force recompile next time deployment set (e.g. after default config update) */ -export const DEPLOYMENT_CONFIG_VERSION = 20240910.0; +export const DEPLOYMENT_CONFIG_VERSION = 20240912.0; /** Configuration settings available to runtime application */ export interface IDeploymentRuntimeConfig { @@ -12,6 +12,8 @@ export interface IDeploymentRuntimeConfig { _content_version: string; api: { + /** Specify whether to enable communication with backend API (default true)*/ + enabled: boolean; /** Name of target db for api operations. Default `plh` */ db_name?: string; /** @@ -20,6 +22,12 @@ export interface IDeploymentRuntimeConfig { * */ endpoint?: string; }; + analytics: { + enabled: boolean; + provider: "matomo"; + endpoint: string; + siteId: number; + }; /** Optional override of any provided constants from data-models/constants */ app_config: IAppConfigOverride; /** 3rd party integration for logging services */ @@ -156,9 +164,16 @@ export const DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS: IDeploymentRuntimeConfig = { _app_builder_version: "", name: "", api: { + enabled: true, db_name: "plh", endpoint: "https://apps-server.idems.international/api", }, + analytics: { + enabled: true, + provider: "matomo", + siteId: 1, + endpoint: "https://apps-server.idems.international/analytics", + }, app_config: {}, firebase: { diff --git a/packages/scripts/src/tasks/providers/appData.ts b/packages/scripts/src/tasks/providers/appData.ts index bb26fc59ac..a51d3da942 100644 --- a/packages/scripts/src/tasks/providers/appData.ts +++ b/packages/scripts/src/tasks/providers/appData.ts @@ -60,17 +60,20 @@ const copyDeploymentDataToApp = () => { }; function generateRuntimeConfig(deploymentConfig: IDeploymentConfigJson): IDeploymentRuntimeConfig { - const { api, app_config, firebase, supabase, error_logging, git, name } = deploymentConfig; + const { analytics, api, app_config, error_logging, firebase, git, name, supabase, web } = + deploymentConfig; return { + _app_builder_version: packageJSON.version, + _content_version: git.content_tag_latest || "", + analytics, api, app_config, - firebase, - supabase, error_logging, - _app_builder_version: packageJSON.version, - _content_version: git.content_tag_latest || "", + firebase, name, + supabase, + web, }; } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 322d875069..591b129364 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,15 +1,14 @@ -import { APP_INITIALIZER, ErrorHandler, NgModule } from "@angular/core"; +import { ErrorHandler, NgModule } from "@angular/core"; import { BrowserModule } from "@angular/platform-browser"; import { FormsModule } from "@angular/forms"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { RouteReuseStrategy } from "@angular/router"; -import { HttpClientModule } from "@angular/common/http"; +import { HTTP_INTERCEPTORS, HttpClientModule } from "@angular/common/http"; import { IonicModule, IonicRouteStrategy } from "@ionic/angular"; // Libs import { LottieModule } from "ngx-lottie"; import player from "lottie-web"; -import { MatomoModule, MatomoRouterModule } from "ngx-matomo-client"; // Native import { HTTP } from "@ionic-native/http/ngx"; @@ -19,13 +18,12 @@ import { Device } from "@ionic-native/device/ngx"; import { AppComponent } from "./app.component"; import { AppRoutingModule } from "./app-routing.module"; import { SharedModule } from "./shared/shared.module"; -import { environment } from "src/environments/environment"; -import { httpInterceptorProviders } from "./shared/services/server/interceptors"; import { TemplateComponentsModule } from "./shared/components/template/template.module"; import { ContextMenuModule } from "./shared/modules/context-menu/context-menu.module"; import { TourModule } from "./feature/tour/tour.module"; import { ErrorHandlerService } from "./shared/services/error-handler/error-handler.service"; -import { DeploymentService } from "./shared/services/deployment/deployment.service"; +import { ServerAPIInterceptor } from "./shared/services/server/interceptors"; +import { DeploymentFeaturesModule } from "./deployment-features.module"; // Note we need a separate function as it's required // by the AOT compiler. @@ -48,27 +46,16 @@ export function lottiePlayerFactory() { // LottieCacheModule.forRoot(), TemplateComponentsModule, TourModule, - MatomoModule.forRoot({ - siteId: environment.analytics.siteId, - trackerUrl: environment.analytics.endpoint, - }), - MatomoRouterModule, ContextMenuModule, + DeploymentFeaturesModule, ], providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, HTTP, Device, - // ensure deployment service initialized before app component load - { - provide: APP_INITIALIZER, - multi: true, - useFactory: (deploymentService: DeploymentService) => { - return () => deploymentService.ready(); - }, - deps: [DeploymentService], - }, - httpInterceptorProviders, + // Use custom api interceptor to handle interaction with server backend + { provide: HTTP_INTERCEPTORS, useClass: ServerAPIInterceptor, multi: true }, + // Use custom error handler { provide: ErrorHandler, useClass: ErrorHandlerService }, ], bootstrap: [AppComponent], diff --git a/src/app/deployment-features.module.ts b/src/app/deployment-features.module.ts new file mode 100644 index 0000000000..463e4fcbc2 --- /dev/null +++ b/src/app/deployment-features.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from "@angular/core"; + +import { AnalyticsModule } from "./shared/services/analytics"; + +/** + * Module imports required for specific deployment features + * + * NOTE - as angular needs all modules to be statically defined during compilation + * it is not possible to conditionally load modules at runtime. + * + * Therefore all modules are defined and loaded as part of the core build process, + * but it is still possible to override this file to create specific feature-optimised builds + * + * This is a feature marked for future implementation + */ +@NgModule({ imports: [AnalyticsModule] }) +export class DeploymentFeaturesModule {} diff --git a/src/app/shared/services/analytics/analytics.module.ts b/src/app/shared/services/analytics/analytics.module.ts new file mode 100644 index 0000000000..24f0e01b17 --- /dev/null +++ b/src/app/shared/services/analytics/analytics.module.ts @@ -0,0 +1,45 @@ +import { NgModule } from "@angular/core"; + +import { + MATOMO_CONFIGURATION, + MatomoConfiguration, + provideMatomo, + withRouter, +} from "ngx-matomo-client"; + +import { IDeploymentRuntimeConfig } from "packages/data-models"; +import { DEPLOYMENT_CONFIG } from "../deployment/deployment.service"; +import { environment } from "src/environments/environment"; + +/** When running locally can configure to target local running containing (if required) */ +const devConfig: MatomoConfiguration = { + disabled: true, + trackerUrl: "http://localhost/analytics", + siteId: 1, +}; + +/** + * When configuring the analytics module + * This should be imported into the main app.module.ts + */ +@NgModule({ + imports: [], + providers: [ + provideMatomo(null, withRouter()), + // Dynamically provide the configuration used by the matomo provider so that it can + // access deployment config (injected from token) + { + provide: MATOMO_CONFIGURATION, + useFactory: (deploymentConfig: IDeploymentRuntimeConfig): MatomoConfiguration => { + if (environment.production) { + const { enabled, endpoint, siteId } = deploymentConfig.analytics; + return { disabled: !enabled, siteId, trackerUrl: endpoint }; + } else { + return devConfig; + } + }, + deps: [DEPLOYMENT_CONFIG], + }, + ], +}) +export class AnalyticsModule {} diff --git a/src/app/shared/services/analytics/index.ts b/src/app/shared/services/analytics/index.ts new file mode 100644 index 0000000000..4e8738bfb5 --- /dev/null +++ b/src/app/shared/services/analytics/index.ts @@ -0,0 +1,2 @@ +export * from "./analytics.module"; +export * from "./analytics.service"; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 9b5eec46a6..340c5e26e9 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -14,5 +14,4 @@ export const environment = { domains: ["plh-demo1.idems.international", "plh-demo.idems.international"], chatNonNavigatePaths: ["/chat/action", "/chat/msg-info"], variableNameFlows: ["character_names"], - analytics: { endpoint: "https://apps-server.idems.international/analytics", siteId: 1 }, }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 6825f94d3a..d0bf9509f2 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -14,10 +14,6 @@ export const environment = { domains: ["plh-demo1.idems.international", "plh-demo.idems.international"], chatNonNavigatePaths: ["/chat/action", "/chat/msg-info"], variableNameFlows: ["character_names"], - /** Local Settings */ - analytics: { endpoint: "http://localhost/analytics", siteId: 1 }, - /** Production Settings **/ - // analytics: { endpoint: "https://apps-server.idems.international/analytics", siteId: 1 }, }; // This file can be replaced during build by using the `fileReplacements` array. From a2bd9ae9d5d7957579a0dfcdc680bed5cb5aedc4 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 12 Sep 2024 23:17:43 -0700 Subject: [PATCH 4/5] refactor: http interceptors --- .../shared/services/server/interceptors.ts | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/app/shared/services/server/interceptors.ts b/src/app/shared/services/server/interceptors.ts index 85e5119a7d..954d89cbf6 100644 --- a/src/app/shared/services/server/interceptors.ts +++ b/src/app/shared/services/server/interceptors.ts @@ -1,31 +1,38 @@ -import { Injectable } from "@angular/core"; +import { Inject, Injectable } from "@angular/core"; import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, - HTTP_INTERCEPTORS, HttpHeaders, } from "@angular/common/http"; -import { environment } from "src/environments/environment"; import { Observable } from "rxjs"; - -let { db_name, endpoint: API_ENDPOINT } = environment.deploymentConfig.api; - -// Override development credentials when running locally -if (!environment.production) { - // Docker endpoint. Replace :3000 with /api if running standalone api - API_ENDPOINT = "http://localhost:3000"; - db_name = "dev"; -} +import { DEPLOYMENT_CONFIG } from "../deployment/deployment.service"; +import { IDeploymentRuntimeConfig } from "packages/data-models"; /** Handle updating urls intended for api server */ @Injectable() export class ServerAPIInterceptor implements HttpInterceptor { + // Inject the global deployment config to use with requests + constructor(@Inject(DEPLOYMENT_CONFIG) private deploymentConfig: IDeploymentRuntimeConfig) {} + + /** + * Intercept all http requests to rewrite including database api endpoint and + * deployment-db-name headers, as read from deployment config + */ intercept(req: HttpRequest, next: HttpHandler): Observable> { // assume requests targetting / (e.g. /app_users) is directed to api endpoint if (req.url.startsWith("/")) { - const replacedUrl = `${API_ENDPOINT}${req.url}`; + const { db_name, endpoint, enabled } = this.deploymentConfig.api; + // If not using api silently cancel any requests to the api + // TODO - better to disable in service (could also replace interceptor with service more generally) + if (!enabled) return; + if (!db_name || !endpoint) { + console.warn("api endpoint not configured, ignoring request", req.url); + return; + } + + const replacedUrl = `${endpoint}${req.url}`; // append deployment-specific values (header set/append methods inconsistent so create new) const headerValues = { "x-deployment-db-name": db_name }; for (const key of req.headers.keys()) { @@ -37,8 +44,3 @@ export class ServerAPIInterceptor implements HttpInterceptor { return next.handle(req); } } - -/** Http interceptor providers in outside-in order */ -export const httpInterceptorProviders = [ - { provide: HTTP_INTERCEPTORS, useClass: ServerAPIInterceptor, multi: true }, -]; From 63972aed1a65fe5c68b10f8f20bdccdc6507ba90 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Thu, 12 Sep 2024 23:45:18 -0700 Subject: [PATCH 5/5] chore: code tidying --- .../services/deployment/deployment.service.ts | 25 +++++++++++-------- src/main.ts | 2 +- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/app/shared/services/deployment/deployment.service.ts b/src/app/shared/services/deployment/deployment.service.ts index c50287a8b9..e49f9c534d 100644 --- a/src/app/shared/services/deployment/deployment.service.ts +++ b/src/app/shared/services/deployment/deployment.service.ts @@ -6,23 +6,26 @@ import { SyncServiceBase } from "../syncService.base"; * Token to inject deployment config value into any service. * This is populated from json file before platform load, as part of src\main.ts * - * Can be used directly in the constructor via `@Inject(DEPLOYMENT_CONFIG)`, - * or values accessed from the DeploymentService + * Can be used directly by any service or module initialised at any time + * (including app.module.ts). + * + * @example Inject into service + * ```ts + * constructor(@Inject(DEPLOYMENT_CONFIG)) + * ``` + * @example Inject into module + * ``` + * {provide: MyModule, useFactory:(config)=>{...}, deps: [DEPLOYMENT_CONFIG]`} + * ``` */ export const DEPLOYMENT_CONFIG: InjectionToken = new InjectionToken("Application Configuration"); -@Injectable({ providedIn: "root" }) /** - * Deployment runtime config settings - * - * NOTE - this is intialized using an `APP_INITIALIZER` token within - * the main app.module.ts and will block all other services from loading until - * it is fully initialised - * - * Services that access the deployment config therefore do not need to await - * DeploymentService init/ready methods. + * The deployment service provides access to values loaded from the deployment json file + * It is an alternative to injecting directly via `@Inject(DEPLOYMENT_CONFIG)` */ +@Injectable({ providedIn: "root" }) export class DeploymentService extends SyncServiceBase { constructor(@Inject(DEPLOYMENT_CONFIG) public readonly config: IDeploymentRuntimeConfig) { super("Deployment Service"); diff --git a/src/main.ts b/src/main.ts index c01efccbfb..de4c3e402c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,7 +21,7 @@ const loadConfig = async (): Promise => { return deploymentConfig; } else { console.warn("[DEPLOYMENT] config not found, using defaults"); - return { ...DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, app_config: {} as any }; + return DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS; } };