Skip to content

Commit

Permalink
Merge branch 'master' into chore/pr-labeller-2
Browse files Browse the repository at this point in the history
  • Loading branch information
jfmcquade authored Sep 16, 2024
2 parents e7ab562 + 53fa95b commit 81e5970
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 101 deletions.
23 changes: 19 additions & 4 deletions packages/data-models/deployment.model.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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;
export const DEPLOYMENT_CONFIG_VERSION = 20240912.0;

/** Configuration settings available to runtime application */
export interface IDeploymentRuntimeConfig {
Expand All @@ -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;
/**
Expand All @@ -20,8 +22,14 @@ 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: IAppConfig;
app_config: IAppConfigOverride;
/** 3rd party integration for logging services */
error_logging?: {
/** sentry/glitchtip logging dsn */
Expand Down Expand Up @@ -156,10 +164,17 @@ export const DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS: IDeploymentRuntimeConfig = {
_app_builder_version: "",
name: "",
api: {
enabled: true,
db_name: "plh",
endpoint: "https://apps-server.idems.international/api",
},
app_config: {} as any, // populated by `getDefaultAppConstants()`,
analytics: {
enabled: true,
provider: "matomo",
siteId: 1,
endpoint: "https://apps-server.idems.international/analytics",
},
app_config: {},

firebase: {
config: null,
Expand Down
13 changes: 8 additions & 5 deletions packages/scripts/src/tasks/providers/appData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down
29 changes: 8 additions & 21 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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.
Expand All @@ -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],
Expand Down
17 changes: 17 additions & 0 deletions src/app/deployment-features.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
45 changes: 45 additions & 0 deletions src/app/shared/services/analytics/analytics.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
2 changes: 2 additions & 0 deletions src/app/shared/services/analytics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./analytics.module";
export * from "./analytics.service";
66 changes: 25 additions & 41 deletions src/app/shared/services/deployment/deployment.service.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,33 @@
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";

@Injectable({ providedIn: "root" })
/**
* Deployment runtime config settings
* Token to inject deployment config value into any service.
* This is populated from json file before platform load, as part of src\main.ts
*
* 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
* Can be used directly by any service or module initialised at any time
* (including app.module.ts).
*
* Services that access the deployment config therefore do not need to await
* DeploymentService init/ready methods.
* @example Inject into service
* ```ts
* constructor(@Inject(DEPLOYMENT_CONFIG))
* ```
* @example Inject into module
* ```
* {provide: MyModule, useFactory:(config)=>{...}, deps: [DEPLOYMENT_CONFIG]`}
* ```
*/
export class DeploymentService extends AsyncServiceBase {
constructor(private http: HttpClient) {
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;
}
export const DEPLOYMENT_CONFIG: InjectionToken<IDeploymentRuntimeConfig> =
new InjectionToken<IDeploymentRuntimeConfig>("Application Configuration");

/** 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)
);
/**
* 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");
}
}
38 changes: 20 additions & 18 deletions src/app/shared/services/server/interceptors.ts
Original file line number Diff line number Diff line change
@@ -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<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// 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()) {
Expand All @@ -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 },
];
1 change: 0 additions & 1 deletion src/environments/environment.prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
};
4 changes: 0 additions & 4 deletions src/environments/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 81e5970

Please sign in to comment.