My company uses Keycloak for authentication connected with LDAP and returning a user object filled with corporative data.
Yet in this period we are all working from home and in my daily work having to authenticate in my corporative server every time I reload the app, has proven to be an expensive overhead. Especially with intermittent internet connections.
How can I fake the Keycloak call and make keycloak.protect() work as it has succeeded?
I can install a Keyclock server in my machine, but I'd rather not do that because it would be another server running in it besides, vagrant VM, Postgres server, be server, and all the other things I leave open.
It would be best to make a mock call and return a fixed hard-coded object.
My project's app-init.ts is this:
import { KeycloakService } from 'keycloak-angular';
import { KeycloakUser } from './shared/models/keycloakUser';
<...>
export function initializer(
keycloak: KeycloakService,
<...>
): () => Promise<any> {
return (): Promise<any> => {
return new Promise(async (res, rej) => {
<...>
await keycloak.init({
config: environment.keycloakConfig,
initOptions: {
onLoad: 'login-required',
// onLoad: 'check-sso',
checkLoginIframe: false
},
bearerExcludedUrls: [],
loadUserProfileAtStartUp: false
}).then((authenticated: boolean) => {
if (!authenticated) return;
keycloak.getKeycloakInstance()
.loadUserInfo()
.success(async (user: KeycloakUser) => {
// ...
// load authenticated user data
// ...
})
}).catch((err: any) => rej(err));
res();
});
};
I just need one fixed logged user. But it has to return some fixed customized data with it. Something like this:
{ username: '111111111-11', name: 'Whatever Something de Paula',
email: 'whatever#gmail.com', department: 'sales', employee_number: 7777777 }
EDIT
I tried to look at the idea of #BojanKogoj but AFAIU from Angular Interceptor page and other examples and tutorials, it has to be injected in a component. Keycloak initialization is called on app initialization, not in a component. Also Keycloak's return is not the direct return of init() method. It passes through other objects in the .getKeycloakInstance().loadUserInfo().success() sequence.
Or maybe it's just me that didn't fully understand it. If anyone can come with an example of an interceptor that can intercept the call and return the correct result, that could be a possibility.
Edit2
Just to complement that what I need is for the whole keycloak's system to work. Please notice that the (user: KeycloakUser) => { function is passed to success method of keycloak's internal system. As I said above, routes have a keycloak.protect() that must work. So it's not just a simple case of returning a promise with a user. The whole .getKeycloakInstance().loadUserInfo().success() chain has to be mocked. Or at least that's how I understand it.
I included an answer with the solution I made based on #yurzui's answer
Will wait a couple of days to award the bounty to see if someone can came up with an even better solution (which I doubt).
You can leverage Angular environment(or even process.env) variable to switch between real and mock implementations.
Here is a simple example of how to do that:
app-init.ts
...
import { environment } from '../environments/environment';
export function initializer(
keycloak: KeycloakService
): () => Promise<any> {
function authenticate() {
return keycloak
.init({
config: {} as any,
initOptions: {onLoad: 'login-required', checkLoginIframe: false},
bearerExcludedUrls: [],
loadUserProfileAtStartUp: false
})
.then(authenticated => {
return authenticated ? keycloak.getKeycloakInstance().loadUserInfo() : Promise.reject();
});
}
// we use 'any' here so you don't have to define keyCloakUser in each environment
const { keyCloakUser } = environment as any;
return () => {
return (keyCloakUser ? Promise.resolve(keyCloakUser) : authenticate()).then(user => {
// ...
// do whatever you want with user
// ...
});
};
}
environment.ts
export const environment = {
production: false,
keyCloakUser: {
username: '111111111-11',
name: 'Whatever Something de Paula',
email: 'whatever#gmail.com',
}
};
environment.prod.ts
export const environment = {
production: true,
};
Update
If you want to mock KeycloakService on client side then you can tell Angular dependency injection to handle that:
app.module.ts
import { environment } from '../environments/environment';
import { KeycloakService, KeycloakAngularModule } from 'keycloak-angular';
import { MockedKeycloakService } from './mocked-keycloak.service';
#NgModule({
...
imports: [
...
KeycloakAngularModule
],
providers: [
{
provide: KeycloakService,
useClass: environment.production ? KeycloakService : MockedKeycloakService
},
{
provide: APP_INITIALIZER,
useFactory: initializer,
multi: true,
deps: [KeycloakService]
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
mocked-keycloak.service.ts
import { Injectable} from '#angular/core';
import { KeycloakService } from 'keycloak-angular';
#Injectable()
class MockedKeycloakService extends KeycloakService {
init() {
return Promise.resolve(true);
}
getKeycloakInstance() {
return {
loadUserInfo: () => {
let callback;
Promise.resolve().then(() => {
callback({
userName: 'name'
});
});
return {
success: (fn) => callback = fn
};
}
} as any;
}
}
Although you explicitly state that you think that mocking is the best option, I suggest to reconsider it in favor of setting up local Keycloak instance using docker. It becomes easy when you provide a realm to bootstrap your environment. I've been using this approach with success for over 2 years of developing applications that work with Keycloak. This approach will let you "substitute calls to your corporate server" hence I post it here.
Assuming that you have docker & docker-compose installed, you'll need:
1. docker-compose.yaml
version: '3.7'
services:
keycloak:
image: jboss/keycloak:10.0.1
environment:
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: admin
KEYCLOAK_IMPORT: /tmp/dev-realm.json
ports:
- 8080:8080
volumes:
- ./dev-realm.json:/tmp/dev-realm.json
2. dev-realm.json (exact content depend on required settings, this is the minimum that you've mentioned in your question)
{
"id": "dev",
"realm": "dev",
"enabled": true,
"clients": [
{
"clientId": "app",
"enabled": true,
"redirectUris": [
"*"
],
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"secret": "mysecret",
"publicClient": false,
"protocol": "openid-connect",
"fullScopeAllowed": false,
"protocolMappers": [
{
"name": "department",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"user.attribute": "department",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "department",
"userinfo.token.claim": "true"
}
},
{
"name": "employee_number",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"user.attribute": "employee_number",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "employee_number",
"userinfo.token.claim": "true"
}
}
]
}
],
"users": [
{
"username": "111111111-11",
"enabled": true,
"firstName": "Whatever Something de Paula",
"email": "whatever#gmail.com",
"credentials": [{
"type": "password",
"value": "demo"
}],
"attributes": {
"department": "sales",
"employee_number": 7777777
}
}
]
}
3. Create dedicated Angular environment that will use the "http://localhost:8080/auth" and realm "dev" for your local development
The advantages of this approach over mocking:
all OIDC and keycloak features are working. I admit that it depends if you need them but you are free to use realm/client roles, groups, 'real' OIDC flow with token refreshal. This gives you guarantee that your local setup will work also with corporate service
this setup can be stored in repository (contrary to manual setup of Keycloak server) and used both for working on web applications and backend services
By default, Keycloak uses a H2 in-memory database and needs about 600MB of RAM so I'd argue that it is a relatively low footprint.
Solution
I was able to mock Keycloak service using the method #yurzui suggested. I'll document it here as it may be useful for somebody.
Initially I had posted a solution where I conditionally exported the mock or real classes from the mock module. All worked well on dev mode, but when I tried to build the application for publishing in production server I got an error, so I had to return to the 2 class solution. I explain the problem in details this question .
This is the working code (so far).
Frontend:
With a little help from #kev's answer in this question and #yurzui (again :D) in this one, I created a MockKeycloakService class:
import { Injectable } from '#angular/core';
import { KeycloakService } from 'keycloak-angular';
import { environment } from '../../../environments/environment';
#Injectable({ providedIn: 'root' })
export default class MockKeycloakService {
init() {
console.log('[KEYCLOAK] Mocked Keycloak call');
return Promise.resolve(true);
}
getKeycloakInstance() {
return {
loadUserInfo: () => {
let callback : any;
Promise.resolve().then(() => {
callback({
username: '77363698953',
NOME: 'Nelson Teixeira',
FOTO: 'assets/usuarios/nelson.jpg',
LOTACAOCOMPLETA: 'DIOPE/SUPOP/OPSRL/OPSMC (local)',
});
});
return { success: fn=>callback = fn };
}
} as any;
}
login() {}
logout() {}
}
const KeycloakServiceImpl =
environment.production ? KeycloakService : MockKeycloakService
export { KeycloakServiceImpl, KeycloakService, MockKeycloakService };
then I substituted it in app.module:
<...>
import { KeycloakAngularModule } from 'keycloak-angular';
import { KeycloakServiceImpl } from 'src/app/shared/services/keycloak-mock.service';
import { initializer } from './app-init';
<...>
imports: [
KeycloakAngularModule,
<...>
],
providers: [
<...>,
{
provide: APP_INITIALIZER,
useFactory: initializer,
multi: true,
deps: [KeycloakServiceImpl, <...>]
},
<...>
],
bootstrap: [AppComponent]
})
export class AppModule { }
Then changed the type of keycloak service variable in app-init, that was the only change, but then I could remove KeycloackService import as it's being provided in app.module:
import { KeycloakUser } from './shared/models/keycloakUser';
<...>
export function initializer(
keycloakService: any,
<...>
): () => Promise<any> {
return (): Promise<any> => {
return new Promise(async (res, rej) => {
<...>
await keycloak.init({
config: environment.keycloakConfig,
initOptions: {
onLoad: 'login-required',
// onLoad: 'check-sso',
checkLoginIframe: false
},
bearerExcludedUrls: [],
loadUserProfileAtStartUp: false
}).then((authenticated: boolean) => {
if (!authenticated) return;
keycloak.getKeycloakInstance()
.loadUserInfo()
.success(async (user: KeycloakUser) => {
<...>
})
}).catch((err: any) => rej(err));
res();
});
};
But in the component I still have to check which environment I'm on and instance the class correctly:
<...>
import { MockKeycloakService } from '../../shared/services/keycloak.mock.service';
import { environment } from '../../../environments/environment';
<...>
export class MainComponent implements OnInit, OnDestroy {
<...>
keycloak: any;
constructor(
<...>
) {
this.keycloak = (environment.production) ? KeycloakServiceImpl : new KeycloakServiceImpl();
}
async doLogout() {
await this.keycloak.logout();
}
async doLogin() {
await this.keycloak.login();
}
<...>
}
Backend:
That was easier, again I created a KeycloakMock class:
import KeyCloack from 'keycloak-connect';
class KeycloakMock {
constructor(store, config) {
//ignore them
}
middleware() {
return (req, res, next) =>{
next();
}}
protect(req, res, next) {
return (req, res, next) =>{
next();
}}
}
const exportKeycloak =
(process.env.NODE_ENV == 'local') ? KeycloakMock : KeyCloack;
export default exportKeycloak;
Then I just substitued 'keycloak-connect' import on app.js by this class, and everythig worked fine. It connects to the real service if I set production = true and it mocks it with production = false.
Very cool solution. If anyone has anything to say on my implementation of #yurzui idea I'll like to hear from you.
Some notes:
I still couln't get rid of having to check the environment in the main-component class, as if I do this in the mock class module:
const KeycloakServiceImpl =
environment.production ? KeycloakService : new MockKeycloakService()
app.module doesn't work anymore. and if I do this in main-component:
constructor(
<...>
keycloakService: KeyclockServiceImpl;
) { }
The build fails with a "KeyclockServiceImpl refers to a value but is being used as a type here";
I had to export all classes or the build fails
export { KeycloakServiceImpl, KeycloakService, MockKeycloakService };
Related
I'm using pino-logger in my NestJS project to log the activities in my application, and I'm logging the object along with ReqId so I can trace the whole activity inside one request. I'd like to use the same "ReqId" in another place as well, but I'm unsure of how to move it outside of the module, so for that, I'm thinking to save that generated ReqId into the CacheManager but not sure how to inject CacheManager class inside genReqId function. Please look over the code below.
app.module.ts
#Module({
imports: [
LoggerModule.forRoot({
pinoHttp: {
genReqId: (req: any) => {
// I'm thinking to use CacheManager here but I'm not sure how to inject CacheManager class here
return req.headers.req_id || uuid(); // from here it generate the request ID and I want to export this ID and use in side an another class
},
base: undefined,
quietReqLogger: true,
timestamp: false,
},
}),
],
})
export class AppModule {}
you need To create sharable service and import it Imports
#Injectable()
export class RequestIdService {
private reqId: string;
setRequestId(reqId: string) {
this.reqId = reqId;
}
getRequestId() {
return this.reqId;
}
}
than import it to logger module
imports: [
LoggerModule.forRoot({
pinoHttp: {
genReqId: (req: any) => {
this.requestIdService.setRequestId(req.headers.req_id || uuid());
return this.requestIdService.getRequestId();
},
base: undefined,
quietReqLogger: true,
timestamp: false,
},
}),
],
providers: [RequestIdService],
```
use that service by
import { RequestIdService } from './request-id.service';
this.requestIdService.getRequestId()
I want to add an App Settings section into my App where It will contain some consts and pre defined values.
I've already read this answer which uses OpaqueToken But it is deprecated in Angular. This article explains the differences but it didn't provide a full example , and my attempts were unsuccessful.
Here is what I've tried ( I don't know if it's the right way) :
//ServiceAppSettings.ts
import {InjectionToken, OpaqueToken} from "#angular/core";
const CONFIG = {
apiUrl: 'http://my.api.com',
theme: 'suicid-squad',
title: 'My awesome app'
};
const FEATURE_ENABLED = true;
const API_URL = new InjectionToken<string>('apiUrl');
And this is the component where I want to use those consts :
//MainPage.ts
import {...} from '#angular/core'
import {ServiceTest} from "./ServiceTest"
#Component({
selector: 'my-app',
template: `
<span>Hi</span>
` , providers: [
{
provide: ServiceTest,
useFactory: ( apiUrl) => {
// create data service
},
deps: [
new Inject(API_URL)
]
}
]
})
export class MainPage {
}
But it doesn't work and I get errors.
Question:
How can I consume "app.settings" values the Angular way?
plunker
NB Sure I can create Injectable service and put it in the provider of the NgModule , But as I said I want to do it with InjectionToken , the Angular way.
It's not advisable to use the environment.*.ts files for your API URL configuration. It seems like you should because this mentions the word "environment".
Using this is actually compile-time configuration. If you want to change the API URL, you will need to re-build. That's something you don't want to have to do ... just ask your friendly QA department :)
What you need is runtime configuration, i.e. the app loads its configuration when it starts up.
Some other answers touch on this, but the difference is that the configuration needs to be loaded as soon as the app starts, so that it can be used by a normal service whenever it needs it.
To implement runtime configuration:
Add a JSON config file to the /src/assets/ folder (so that is copied on build)
Create an AppConfigService to load and distribute the config
Load the configuration using an APP_INITIALIZER
1. Add Config file to /src/assets
You could add it to another folder, but you'd need to tell the Angular CLI that it is an asset, by updating the angular.json configuration file. Start off using the assets folder:
{
"apiBaseUrl": "https://development.local/apiUrl"
}
2. Create AppConfigService
This is the service which will be injected whenever you need the config value:
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class AppConfigService {
private appConfig: any;
constructor(private http: HttpClient) { }
loadAppConfig() {
return this.http.get('/assets/config.json')
.toPromise()
.then(data => {
this.appConfig = data;
});
}
// This is an example property ... you can make it however you want.
get apiBaseUrl() {
if (!this.appConfig) {
throw Error('Config file not loaded!');
}
return this.appConfig.apiBaseUrl;
}
}
3. Load the configuration using an APP_INITIALIZER
To allow the AppConfigService to be injected safely, with config fully loaded, we need to load the config at app startup time. Importantly, the initialisation factory function needs to return a Promise so that Angular knows to wait until it finishes resolving before finishing startup:
import { APP_INITIALIZER } from '#angular/core';
import { AppConfigService } from './services/app-config.service';
#NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [AppConfigService],
useFactory: (appConfigService: AppConfigService) => {
return () => {
//Make sure to return a promise!
return appConfigService.loadAppConfig();
};
}
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
Now you can inject it wherever you need to and all the config will be ready to read:
#Component({
selector: 'app-test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit {
apiBaseUrl: string;
constructor(private appConfigService: AppConfigService) {}
ngOnInit(): void {
this.apiBaseUrl = this.appConfigService.apiBaseUrl;
}
}
I can't say it strongly enough, configuring your API urls as compile-time configuration is an anti-pattern. Use runtime configuration.
If you are using angular-cli, there is yet another option:
Angular CLI provides environment files in src/environments (default ones are environment.ts (dev) and environment.prod.ts (production)).
Note that you need to provide the config parameters in all environment.* files, e.g.,
environment.ts:
export const environment = {
production: false,
apiEndpoint: 'http://localhost:8000/api/v1'
};
environment.prod.ts:
export const environment = {
production: true,
apiEndpoint: '__your_production_server__'
};
and use them in your service (the correct environment file is chosen automatically):
api.service.ts
// ... other imports
import { environment } from '../../environments/environment';
#Injectable()
export class ApiService {
public apiRequest(): Observable<MyObject[]> {
const path = environment.apiEndpoint + `/objects`;
// ...
}
// ...
}
Read more on application environments on Github (Angular CLI version 6) or in the official Angular guide (version 7).
I figured out how to do this with InjectionTokens (see example below), and if your project was built using the Angular CLI you can use the environment files found in /environments for static application wide settings like an API endpoint, but depending on your project's requirements you will most likely end up using both since environment files are just object literals, while an injectable configuration using InjectionToken's can use the environment variables and since it's a class can have logic applied to configure it based on other factors in the application, such as initial HTTP request data, subdomain, etc.
Injection Tokens Example
/app/app-config.module.ts
import { NgModule, InjectionToken } from '#angular/core';
import { environment } from '../environments/environment';
export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');
export class AppConfig {
apiEndpoint: string;
}
export const APP_DI_CONFIG: AppConfig = {
apiEndpoint: environment.apiEndpoint
};
#NgModule({
providers: [{
provide: APP_CONFIG,
useValue: APP_DI_CONFIG
}]
})
export class AppConfigModule { }
/app/app.module.ts
import { BrowserModule } from '#angular/platform-browser';
import { NgModule } from '#angular/core';
import { AppConfigModule } from './app-config.module';
#NgModule({
declarations: [
// ...
],
imports: [
// ...
AppConfigModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
Now you can just DI it into any component, service, etc:
/app/core/auth.service.ts
import { Injectable, Inject } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Router } from '#angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import { APP_CONFIG, AppConfig } from '../app-config.module';
import { AuthHttp } from 'angular2-jwt';
#Injectable()
export class AuthService {
constructor(
private http: Http,
private router: Router,
private authHttp: AuthHttp,
#Inject(APP_CONFIG) private config: AppConfig
) { }
/**
* Logs a user into the application.
* #param payload
*/
public login(payload: { username: string, password: string }) {
return this.http
.post(`${this.config.apiEndpoint}/login`, payload)
.map((response: Response) => {
const token = response.json().token;
sessionStorage.setItem('token', token); // TODO: can this be done else where? interceptor
return this.handleResponse(response); // TODO: unset token shouldn't return the token to login
})
.catch(this.handleError);
}
// ...
}
You can then also type check the config using the exported AppConfig.
I found that using an APP_INITIALIZER for this doesn't work in situations where other service providers require the configuration to be injected. They can be instantiated before APP_INITIALIZER is run.
I've seen other solutions that use fetch to read a config.json file and provide it using an injection token in a parameter to platformBrowserDynamic() prior to bootstrapping the root module. But fetch isn't supported in all browsers and in particular WebView browsers for the mobile devices I target.
The following is a solution that works for me for both PWA and mobile devices (WebView). Note: I've only tested in Android so far; working from home means I don't have access to a Mac to build.
In main.ts:
import { enableProdMode } from '#angular/core';
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { APP_CONFIG } from './app/lib/angular/injection-tokens';
function configListener() {
try {
const configuration = JSON.parse(this.responseText);
// pass config to bootstrap process using an injection token
platformBrowserDynamic([
{ provide: APP_CONFIG, useValue: configuration }
])
.bootstrapModule(AppModule)
.catch(err => console.error(err));
} catch (error) {
console.error(error);
}
}
function configFailed(evt) {
console.error('Error: retrieving config.json');
}
if (environment.production) {
enableProdMode();
}
const request = new XMLHttpRequest();
request.addEventListener('load', configListener);
request.addEventListener('error', configFailed);
request.open('GET', './assets/config/config.json');
request.send();
This code:
kicks off an async request for the config.json file.
When the request completes, parses the JSON into a Javascript object
provides the value using the APP_CONFIG injection token, prior to bootstrapping.
And finally bootstraps the root module.
APP_CONFIG can then be injected into any additional providers in app-module.ts and it will be defined. For example, I can initialise the FIREBASE_OPTIONS injection token from #angular/fire with the following:
{
provide: FIREBASE_OPTIONS,
useFactory: (config: IConfig) => config.firebaseConfig,
deps: [APP_CONFIG]
}
I find this whole thing a surprisingly difficult (and hacky) thing to do for a very common requirement. Hopefully in the near future there will be a better way, such as, support for async provider factories.
The rest of the code for completeness...
In app/lib/angular/injection-tokens.ts:
import { InjectionToken } from '#angular/core';
import { IConfig } from '../config/config';
export const APP_CONFIG = new InjectionToken<IConfig>('app-config');
and in app/lib/config/config.ts I define the interface for my JSON config file:
export interface IConfig {
name: string;
version: string;
instance: string;
firebaseConfig: {
apiKey: string;
// etc
}
}
Config is stored in assets/config/config.json:
{
"name": "my-app",
"version": "#{Build.BuildNumber}#",
"instance": "localdev",
"firebaseConfig": {
"apiKey": "abcd"
...
}
}
Note: I use an Azure DevOps task to insert Build.BuildNumber and substitute other settings for different deployment environments as it is being deployed.
Poor man's configuration file:
Add to your index.html as first line in the body tag:
<script lang="javascript" src="assets/config.js"></script>
Add assets/config.js:
var config = {
apiBaseUrl: "http://localhost:8080"
}
Add config.ts:
export const config: AppConfig = window['config']
export interface AppConfig {
apiBaseUrl: string
}
Here's my solution, loads from .json to allow changes without rebuilding
import { Injectable, Inject } from '#angular/core';
import { Http } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { Location } from '#angular/common';
#Injectable()
export class ConfigService {
private config: any;
constructor(private location: Location, private http: Http) {
}
async apiUrl(): Promise<string> {
let conf = await this.getConfig();
return Promise.resolve(conf.apiUrl);
}
private async getConfig(): Promise<any> {
if (!this.config) {
this.config = (await this.http.get(this.location.prepareExternalUrl('/assets/config.json')).toPromise()).json();
}
return Promise.resolve(this.config);
}
}
and config.json
{
"apiUrl": "http://localhost:3000/api"
}
Quite a few articles are recommending that you get Angular config settings using an AppConfigService such as this one.
But I found that sometimes this just didn't work.
It was simpler, and more reliable to have a "config.json" file, then create a class which just read it in, and returned a value, eg my config file would look like this:
{
"appName": "Mike's app",
"version": "1.23.4",
"logging_URL" : "https://someWebsite.azurewebsites.net/logs"
}
And I'd access the values using this:
import config from '../../assets/config.json';
#Injectable({
providedIn: 'root'
})
export class AppConfigService {
get appName() {
return config.appName;
}
get appVersion() {
return config.version;
}
get loggingUrl() {
return config.logging_URL;
}
}
(A few months later...)
After patting myself on the back for making a simpler Angular solution, I realised this has a big drawback. If you use an AppConfigService, and you're using CI/CD, then you can get your build process to update the config .json file, and the Angular app will use these settings.
With my version, yes, it's simpler, but there is no config setting file to overwrite. For automated build processes, this might not be desirable.
Here's my two solutions for this
1. Store in json files
Just make a json file and get in your component by $http.get() method. If I was need this very low then it's good and quick.
2. Store by using data services
If you want to store and use in all components or having large usage then it's better to use data service. Like this :
Just create static folder inside src/app folder.
Create a file named as fuels.ts into static folder. You can store other static files here also. Let define your data like this. Assuming you having fuels data.
__
export const Fuels {
Fuel: [
{ "id": 1, "type": "A" },
{ "id": 2, "type": "B" },
{ "id": 3, "type": "C" },
{ "id": 4, "type": "D" },
];
}
Create a file name static.services.ts
__
import { Injectable } from "#angular/core";
import { Fuels } from "./static/fuels";
#Injectable()
export class StaticService {
constructor() { }
getFuelData(): Fuels[] {
return Fuels;
}
}`
Now You can make this available for every module
just import in app.module.ts file like this and change in providers
import { StaticService } from './static.services';
providers: [StaticService]
Now use this as StaticService in any module.
That's All.
We had this problem years ago before I had joined and had in place a solution that used local storage for user and environment information. Angular 1.0 days to be exact. We were formerly dynamically creating a js file at runtime that would then place the generated api urls into a global variable. We're a little more OOP driven these days and don't use local storage for anything.
I created a better solution for both determining environment and api url creation.
How does this differ?
The app will not load unless the config.json file is loaded. It uses factory functions to create a higher degree of SOC. I could encapsulate this into a service, but I never saw any reason when the only similarity between the different sections of the file are that they exist together in the file. Having a factory function allows me to pass the function directly into a module if it's capable of accepting a function. Last, I have an easier time setting up InjectionTokens when factory functions are available to utilize.
Downsides?
You're out of luck using this setup (and most of the other answers) if the module you want to configure doesn't allow a factory function to be passed into either forRoot() or forChild(), and there's no other way to configure the package by using a factory function.
Instructions
Using fetch to retrieve a json file, I store the object in window and raise a custom event. - remember to install whatwg-fetch and add it to your polyfills.ts for IE compatibility
Have an event listener listening for the custom event.
The event listener receives the event, retrieves the object from window to pass to an observable, and clears out what was stored in window.
Bootstrap Angular
-- This is where my solution starts to really differ --
Create a file exporting an interface whose structure represents your config.json -- it really helps with type consistency and the next section of code requires a type, and don't specify {} or any when you know you can specify something more concrete
Create the BehaviorSubject that you will pass the parsed json file into in step 3.
Use factory functions to reference the different sections of your config to maintain SOC
Create InjectionTokens for the providers needing the result of your factory functions
-- and/or --
Pass the factory functions directly into the modules capable of accepting a function in either its forRoot() or forChild() methods.
-- main.ts
I check window["environment"] is not populated before creating an event listener to allow the possiblilty of a solution where window["environment"] is populated by some other means before the code in main.ts ever executes.
import { enableProdMode } from '#angular/core';
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { configurationSubject } from './app/utils/environment-resolver';
var configurationLoadedEvent = document.createEvent('Event');
configurationLoadedEvent.initEvent('config-set', true, true);
fetch("../../assets/config.json")
.then(result => { return result.json(); })
.then(data => {
window["environment"] = data;
document.dispatchEvent(configurationLoadedEvent);
}, error => window.location.reload());
/*
angular-cli only loads the first thing it finds it needs a dependency under /app in main.ts when under local scope.
Make AppModule the first dependency it needs and the rest are done for ya. Event listeners are
ran at a higher level of scope bypassing the behavior of not loading AppModule when the
configurationSubject is referenced before calling platformBrowserDynamic().bootstrapModule(AppModule)
example: this will not work because configurationSubject is the first dependency the compiler realizes that lives under
app and will ONLY load that dependency, making AppModule an empty object.
if(window["environment"])
{
if (window["environment"].production) {
enableProdMode();
}
configurationSubject.next(window["environment"]);
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
}
*/
if(!window["environment"]) {
document.addEventListener('config-set', function(e){
if (window["environment"].production) {
enableProdMode();
}
configurationSubject.next(window["environment"]);
window["environment"] = undefined;
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
});
}
--- environment-resolvers.ts
I assign a value to the BehaviorSubject using window["environment"] for redundancy. You could devise a solution where your config is preloaded already and window["environment"] is already populated by the time any of your Angular's app code is ran, including the code in main.ts
import { BehaviorSubject } from "rxjs";
import { IConfig } from "../config.interface";
const config = <IConfig>Object.assign({}, window["environment"]);
export const configurationSubject = new BehaviorSubject<IConfig>(config);
export function resolveEnvironment() {
const env = configurationSubject.getValue().environment;
let resolvedEnvironment = "";
switch (env) {
// case statements for determining whether this is dev, test, stage, or prod
}
return resolvedEnvironment;
}
export function resolveNgxLoggerConfig() {
return configurationSubject.getValue().logging;
}
-- app.module.ts - Stripped down for easier understanding
Fun fact! Older versions of NGXLogger required you to pass in an object into LoggerModule.forRoot(). In fact, the LoggerModule still does! NGXLogger kindly exposes LoggerConfig which you can override allowing you to use a factory function for setup.
import { resolveEnvironment, resolveNgxLoggerConfig, resolveSomethingElse } from './environment-resolvers';
import { LoggerConfig } from 'ngx-logger';
#NgModule({
modules: [
SomeModule.forRoot(resolveSomethingElse)
],
providers:[
{
provide: ENVIRONMENT,
useFactory: resolveEnvironment
},
{
provide: LoggerConfig,
useFactory: resolveNgxLoggerConfig
}
]
})
export class AppModule
Addendum
How did I solve the creation of my API urls?
I wanted to be able to understand what each url did via a comment and wanted typechecking since that's TypeScript's greatest strength compared to javascript (IMO). I also wanted to create an experience for other devs to add new endpoints, and apis that was as seamless as possible.
I created a class that takes in the environment (dev, test, stage, prod, "", and etc) and passed this value to a series of classes[1-N] whose job is to create the base url for each API collection. Each ApiCollection is responsible for creating the base url for each collection of APIs. Could be our own APIs, a vendor's APIs, or even an external link. That class will pass the created base url into each subsequent api it contains. Read the code below to see a bare bones example. Once setup, it's very simple for another dev to add another endpoint to an Api class without having to touch anything else.
TLDR; basic OOP principles and lazy getters for memory optimization
#Injectable({
providedIn: 'root'
})
export class ApiConfig {
public apis: Apis;
constructor(#Inject(ENVIRONMENT) private environment: string) {
this.apis = new Apis(environment);
}
}
export class Apis {
readonly microservices: MicroserviceApiCollection;
constructor(environment: string) {
this.microservices = new MicroserviceApiCollection(environment);
}
}
export abstract class ApiCollection {
protected domain: any;
constructor(environment: string) {
const domain = this.resolveDomain(environment);
Object.defineProperty(ApiCollection.prototype, 'domain', {
get() {
Object.defineProperty(this, 'domain', { value: domain });
return this.domain;
},
configurable: true
});
}
}
export class MicroserviceApiCollection extends ApiCollection {
public member: MemberApi;
constructor(environment) {
super(environment);
this.member = new MemberApi(this.domain);
}
resolveDomain(environment: string): string {
return `https://subdomain${environment}.actualdomain.com/`;
}
}
export class Api {
readonly base: any;
constructor(baseUrl: string) {
Object.defineProperty(this, 'base', {
get() {
Object.defineProperty(this, 'base',
{ value: baseUrl, configurable: true});
return this.base;
},
enumerable: false,
configurable: true
});
}
attachProperty(name: string, value: any, enumerable?: boolean) {
Object.defineProperty(this, name,
{ value, writable: false, configurable: true, enumerable: enumerable || true });
}
}
export class MemberApi extends Api {
/**
* This comment will show up when referencing this.apiConfig.apis.microservices.member.memberInfo
*/
get MemberInfo() {
this.attachProperty("MemberInfo", `${this.base}basic-info`);
return this.MemberInfo;
}
constructor(baseUrl: string) {
super(baseUrl + "member/api/");
}
}
I find this Angular How-to: Editable Config Files from Microsoft Dev blogs being the best solution. You can configure dev build settings or prod build settings.
If running in a container, this article https://pumpingco.de/blog/environment-variables-angular-docker/ shows how to use envsubst on the entry point command to fix this static compile issue on the fly via environment variable.
I'm learning nest.js and I have an issue I may not completely understand.
In our company we have dev-gateway which checks MY_URL/.well-known/apollo/server-health endpoint to be sure services are up before creating and then download schema from MY_URL. MY_URL is variable we pass to configuration.
So I need to have GET http://MY_URL/.well-known/apollo/server-health to return { status: pass } and POST http://MY_URL/ to return schema/be graphql endpoint.
If path in GraphQLFederationModule config is equal to / it works, but if I have path defined as /graphql then:
GET http://MY_URL/.well-known/apollo/server-health returns { status: pass } and I think it's issue, I wanted graphql service under /graphql path
GET http://MY_URL/graphql/.well-known/apollo/server-health is graphql endpoint and it return error (lack of query) and I think it should return { status: pass }
GET http://MY_URL/graphql returns graphql enpoint which is OK
I prepared some minimal working version and I'm using:
"#apollo/federation": "^0.25.1",
"#nestjs/common": "^7.6.15",
"#nestjs/core": "^7.6.15",
"#nestjs/graphql": "^7.10.3",
"apollo-server-express": "^2.22.2",
"graphql": "^15.5.0",
"graphql-tools": "^7.0.4",
import { NestFactory } from '#nestjs/core';
import { Module } from '#nestjs/common';
import {
GraphQLFederationModule,
Query,
Resolver,
ResolveReference,
Directive, Field, ID, ObjectType
} from '#nestjs/graphql';
import { Controller, Get } from '#nestjs/common';
#Controller('')
export class AppController {
#Get()
healthCheck() {
return 'Hello World!';
}
}
#ObjectType()
#Directive('#key(fields: "_id")')
export class AdSpot {
#Field((type) => ID)
_id: string;
#Field((type) => String)
name: string
}
#Resolver((of) => AdSpot)
export class CatResolver {
#Query((returns) => [AdSpot], { name: 'adSpots' })
async getAdSpots() {
return [];
}
}
#Module({
providers: [CatResolver],
})
export class CatsModule {}
#Module({
imports: [
CatsModule,
GraphQLFederationModule.forRoot({
include: [CatsModule],
path: '/graphql',
autoSchemaFile: true,
sortSchema: true,
playground: true,
disableHealthCheck: false,
}),
],
controllers: [AppController],
})
export class AppModule {}
async function bootstrap() {
try {
const app = await NestFactory.create(AppModule);
await app.listen(3010);
} catch (err) {
console.log('-------------------------------------');
console.log(err);
}
}
bootstrap();
What am I doing wrong? Did I miss some configuration or is it a bug?
Use the relative path i.e
typePaths: ['**/*.graphql'],
In Your case
#Module({
imports: [
CatsModule,
GraphQLFederationModule.forRoot({
include: [CatsModule],
typePaths: ['**/*.graphql'],
autoSchemaFile: true,
sortSchema: true,
playground: true,
disableHealthCheck: false,
}),
],
controllers: [AppController],
})
I have a simple service with an Injection token that is used to provide some kind of configuration for the service. It's all working as expected. Although I wasn't able to test all the possible scenarios in single spec file. For some reason I'm not allowed to override the Injection token defined in providers.
token.config.ts
export interface MyConfig = {
test: boolean;
}
export const ConfigToken = new InjectionToken<MyConfig>('MyConfig');
token.service.ts
#Injectable()
export class TokenRefreshService {
public autoRefreshStarted = false;
constructor(
#Inject(ConfigToken) private config: MyConfig
) {}
public initAutoRefresh(): void {
if ( this.config.test === true ) {
this.autoRefreshStarted = true;
}
}
}
token.service.spec.ts
describe('TokenRefreshService (with-auto-refresh)', () => {
let service: TokenRefreshService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
{
provide: ConfigToken,
useValue: {}
},
TokenRefreshService
]
});
service = TestBed.get( TokenRefreshService );
});
it('should create an instance', () => {
expect( service ).toBeDefined();
});
it('should not start auto-refresh', () => {
service.initAutoRefresh();
expect( service.autoRefreshStarted ).toBeFalsy();
});
it('should start auto-refresh', () => {
TestBed.overrideProvider( ConfigToken, { useValue: { test: true } }); /// doesn't override the config token at all ///
service.initAutoRefresh();
expect( service.autoRefreshStarted ).toBeTruthy();
});
});
I would like to test scenarions when no config is provided to service or when config with an incorrect data is provided and so on. Therefore I really need a way to somehow override the injection token that is passed to the TokenService. But no matter what I tried, it just keeps returning the same data that are defined in the TestBed.configureTestingModule.
Am I doing something wrong? Or is there an easier way to do this?
Replace
imports: [
{
provide: ConfigToken,
useValue: {}
},
TokenRefreshService
]
with:
providers: [
{
provide: ConfigToken,
useValue: {}
},
TokenRefreshService
]
I guess importing a service never works
I want to add an App Settings section into my App where It will contain some consts and pre defined values.
I've already read this answer which uses OpaqueToken But it is deprecated in Angular. This article explains the differences but it didn't provide a full example , and my attempts were unsuccessful.
Here is what I've tried ( I don't know if it's the right way) :
//ServiceAppSettings.ts
import {InjectionToken, OpaqueToken} from "#angular/core";
const CONFIG = {
apiUrl: 'http://my.api.com',
theme: 'suicid-squad',
title: 'My awesome app'
};
const FEATURE_ENABLED = true;
const API_URL = new InjectionToken<string>('apiUrl');
And this is the component where I want to use those consts :
//MainPage.ts
import {...} from '#angular/core'
import {ServiceTest} from "./ServiceTest"
#Component({
selector: 'my-app',
template: `
<span>Hi</span>
` , providers: [
{
provide: ServiceTest,
useFactory: ( apiUrl) => {
// create data service
},
deps: [
new Inject(API_URL)
]
}
]
})
export class MainPage {
}
But it doesn't work and I get errors.
Question:
How can I consume "app.settings" values the Angular way?
plunker
NB Sure I can create Injectable service and put it in the provider of the NgModule , But as I said I want to do it with InjectionToken , the Angular way.
It's not advisable to use the environment.*.ts files for your API URL configuration. It seems like you should because this mentions the word "environment".
Using this is actually compile-time configuration. If you want to change the API URL, you will need to re-build. That's something you don't want to have to do ... just ask your friendly QA department :)
What you need is runtime configuration, i.e. the app loads its configuration when it starts up.
Some other answers touch on this, but the difference is that the configuration needs to be loaded as soon as the app starts, so that it can be used by a normal service whenever it needs it.
To implement runtime configuration:
Add a JSON config file to the /src/assets/ folder (so that is copied on build)
Create an AppConfigService to load and distribute the config
Load the configuration using an APP_INITIALIZER
1. Add Config file to /src/assets
You could add it to another folder, but you'd need to tell the Angular CLI that it is an asset, by updating the angular.json configuration file. Start off using the assets folder:
{
"apiBaseUrl": "https://development.local/apiUrl"
}
2. Create AppConfigService
This is the service which will be injected whenever you need the config value:
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class AppConfigService {
private appConfig: any;
constructor(private http: HttpClient) { }
loadAppConfig() {
return this.http.get('/assets/config.json')
.toPromise()
.then(data => {
this.appConfig = data;
});
}
// This is an example property ... you can make it however you want.
get apiBaseUrl() {
if (!this.appConfig) {
throw Error('Config file not loaded!');
}
return this.appConfig.apiBaseUrl;
}
}
3. Load the configuration using an APP_INITIALIZER
To allow the AppConfigService to be injected safely, with config fully loaded, we need to load the config at app startup time. Importantly, the initialisation factory function needs to return a Promise so that Angular knows to wait until it finishes resolving before finishing startup:
import { APP_INITIALIZER } from '#angular/core';
import { AppConfigService } from './services/app-config.service';
#NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [AppConfigService],
useFactory: (appConfigService: AppConfigService) => {
return () => {
//Make sure to return a promise!
return appConfigService.loadAppConfig();
};
}
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
Now you can inject it wherever you need to and all the config will be ready to read:
#Component({
selector: 'app-test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit {
apiBaseUrl: string;
constructor(private appConfigService: AppConfigService) {}
ngOnInit(): void {
this.apiBaseUrl = this.appConfigService.apiBaseUrl;
}
}
I can't say it strongly enough, configuring your API urls as compile-time configuration is an anti-pattern. Use runtime configuration.
If you are using angular-cli, there is yet another option:
Angular CLI provides environment files in src/environments (default ones are environment.ts (dev) and environment.prod.ts (production)).
Note that you need to provide the config parameters in all environment.* files, e.g.,
environment.ts:
export const environment = {
production: false,
apiEndpoint: 'http://localhost:8000/api/v1'
};
environment.prod.ts:
export const environment = {
production: true,
apiEndpoint: '__your_production_server__'
};
and use them in your service (the correct environment file is chosen automatically):
api.service.ts
// ... other imports
import { environment } from '../../environments/environment';
#Injectable()
export class ApiService {
public apiRequest(): Observable<MyObject[]> {
const path = environment.apiEndpoint + `/objects`;
// ...
}
// ...
}
Read more on application environments on Github (Angular CLI version 6) or in the official Angular guide (version 7).
I figured out how to do this with InjectionTokens (see example below), and if your project was built using the Angular CLI you can use the environment files found in /environments for static application wide settings like an API endpoint, but depending on your project's requirements you will most likely end up using both since environment files are just object literals, while an injectable configuration using InjectionToken's can use the environment variables and since it's a class can have logic applied to configure it based on other factors in the application, such as initial HTTP request data, subdomain, etc.
Injection Tokens Example
/app/app-config.module.ts
import { NgModule, InjectionToken } from '#angular/core';
import { environment } from '../environments/environment';
export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');
export class AppConfig {
apiEndpoint: string;
}
export const APP_DI_CONFIG: AppConfig = {
apiEndpoint: environment.apiEndpoint
};
#NgModule({
providers: [{
provide: APP_CONFIG,
useValue: APP_DI_CONFIG
}]
})
export class AppConfigModule { }
/app/app.module.ts
import { BrowserModule } from '#angular/platform-browser';
import { NgModule } from '#angular/core';
import { AppConfigModule } from './app-config.module';
#NgModule({
declarations: [
// ...
],
imports: [
// ...
AppConfigModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
Now you can just DI it into any component, service, etc:
/app/core/auth.service.ts
import { Injectable, Inject } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Router } from '#angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import { APP_CONFIG, AppConfig } from '../app-config.module';
import { AuthHttp } from 'angular2-jwt';
#Injectable()
export class AuthService {
constructor(
private http: Http,
private router: Router,
private authHttp: AuthHttp,
#Inject(APP_CONFIG) private config: AppConfig
) { }
/**
* Logs a user into the application.
* #param payload
*/
public login(payload: { username: string, password: string }) {
return this.http
.post(`${this.config.apiEndpoint}/login`, payload)
.map((response: Response) => {
const token = response.json().token;
sessionStorage.setItem('token', token); // TODO: can this be done else where? interceptor
return this.handleResponse(response); // TODO: unset token shouldn't return the token to login
})
.catch(this.handleError);
}
// ...
}
You can then also type check the config using the exported AppConfig.
I found that using an APP_INITIALIZER for this doesn't work in situations where other service providers require the configuration to be injected. They can be instantiated before APP_INITIALIZER is run.
I've seen other solutions that use fetch to read a config.json file and provide it using an injection token in a parameter to platformBrowserDynamic() prior to bootstrapping the root module. But fetch isn't supported in all browsers and in particular WebView browsers for the mobile devices I target.
The following is a solution that works for me for both PWA and mobile devices (WebView). Note: I've only tested in Android so far; working from home means I don't have access to a Mac to build.
In main.ts:
import { enableProdMode } from '#angular/core';
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { APP_CONFIG } from './app/lib/angular/injection-tokens';
function configListener() {
try {
const configuration = JSON.parse(this.responseText);
// pass config to bootstrap process using an injection token
platformBrowserDynamic([
{ provide: APP_CONFIG, useValue: configuration }
])
.bootstrapModule(AppModule)
.catch(err => console.error(err));
} catch (error) {
console.error(error);
}
}
function configFailed(evt) {
console.error('Error: retrieving config.json');
}
if (environment.production) {
enableProdMode();
}
const request = new XMLHttpRequest();
request.addEventListener('load', configListener);
request.addEventListener('error', configFailed);
request.open('GET', './assets/config/config.json');
request.send();
This code:
kicks off an async request for the config.json file.
When the request completes, parses the JSON into a Javascript object
provides the value using the APP_CONFIG injection token, prior to bootstrapping.
And finally bootstraps the root module.
APP_CONFIG can then be injected into any additional providers in app-module.ts and it will be defined. For example, I can initialise the FIREBASE_OPTIONS injection token from #angular/fire with the following:
{
provide: FIREBASE_OPTIONS,
useFactory: (config: IConfig) => config.firebaseConfig,
deps: [APP_CONFIG]
}
I find this whole thing a surprisingly difficult (and hacky) thing to do for a very common requirement. Hopefully in the near future there will be a better way, such as, support for async provider factories.
The rest of the code for completeness...
In app/lib/angular/injection-tokens.ts:
import { InjectionToken } from '#angular/core';
import { IConfig } from '../config/config';
export const APP_CONFIG = new InjectionToken<IConfig>('app-config');
and in app/lib/config/config.ts I define the interface for my JSON config file:
export interface IConfig {
name: string;
version: string;
instance: string;
firebaseConfig: {
apiKey: string;
// etc
}
}
Config is stored in assets/config/config.json:
{
"name": "my-app",
"version": "#{Build.BuildNumber}#",
"instance": "localdev",
"firebaseConfig": {
"apiKey": "abcd"
...
}
}
Note: I use an Azure DevOps task to insert Build.BuildNumber and substitute other settings for different deployment environments as it is being deployed.
Poor man's configuration file:
Add to your index.html as first line in the body tag:
<script lang="javascript" src="assets/config.js"></script>
Add assets/config.js:
var config = {
apiBaseUrl: "http://localhost:8080"
}
Add config.ts:
export const config: AppConfig = window['config']
export interface AppConfig {
apiBaseUrl: string
}
Here's my solution, loads from .json to allow changes without rebuilding
import { Injectable, Inject } from '#angular/core';
import { Http } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { Location } from '#angular/common';
#Injectable()
export class ConfigService {
private config: any;
constructor(private location: Location, private http: Http) {
}
async apiUrl(): Promise<string> {
let conf = await this.getConfig();
return Promise.resolve(conf.apiUrl);
}
private async getConfig(): Promise<any> {
if (!this.config) {
this.config = (await this.http.get(this.location.prepareExternalUrl('/assets/config.json')).toPromise()).json();
}
return Promise.resolve(this.config);
}
}
and config.json
{
"apiUrl": "http://localhost:3000/api"
}
Quite a few articles are recommending that you get Angular config settings using an AppConfigService such as this one.
But I found that sometimes this just didn't work.
It was simpler, and more reliable to have a "config.json" file, then create a class which just read it in, and returned a value, eg my config file would look like this:
{
"appName": "Mike's app",
"version": "1.23.4",
"logging_URL" : "https://someWebsite.azurewebsites.net/logs"
}
And I'd access the values using this:
import config from '../../assets/config.json';
#Injectable({
providedIn: 'root'
})
export class AppConfigService {
get appName() {
return config.appName;
}
get appVersion() {
return config.version;
}
get loggingUrl() {
return config.logging_URL;
}
}
(A few months later...)
After patting myself on the back for making a simpler Angular solution, I realised this has a big drawback. If you use an AppConfigService, and you're using CI/CD, then you can get your build process to update the config .json file, and the Angular app will use these settings.
With my version, yes, it's simpler, but there is no config setting file to overwrite. For automated build processes, this might not be desirable.
Here's my two solutions for this
1. Store in json files
Just make a json file and get in your component by $http.get() method. If I was need this very low then it's good and quick.
2. Store by using data services
If you want to store and use in all components or having large usage then it's better to use data service. Like this :
Just create static folder inside src/app folder.
Create a file named as fuels.ts into static folder. You can store other static files here also. Let define your data like this. Assuming you having fuels data.
__
export const Fuels {
Fuel: [
{ "id": 1, "type": "A" },
{ "id": 2, "type": "B" },
{ "id": 3, "type": "C" },
{ "id": 4, "type": "D" },
];
}
Create a file name static.services.ts
__
import { Injectable } from "#angular/core";
import { Fuels } from "./static/fuels";
#Injectable()
export class StaticService {
constructor() { }
getFuelData(): Fuels[] {
return Fuels;
}
}`
Now You can make this available for every module
just import in app.module.ts file like this and change in providers
import { StaticService } from './static.services';
providers: [StaticService]
Now use this as StaticService in any module.
That's All.
We had this problem years ago before I had joined and had in place a solution that used local storage for user and environment information. Angular 1.0 days to be exact. We were formerly dynamically creating a js file at runtime that would then place the generated api urls into a global variable. We're a little more OOP driven these days and don't use local storage for anything.
I created a better solution for both determining environment and api url creation.
How does this differ?
The app will not load unless the config.json file is loaded. It uses factory functions to create a higher degree of SOC. I could encapsulate this into a service, but I never saw any reason when the only similarity between the different sections of the file are that they exist together in the file. Having a factory function allows me to pass the function directly into a module if it's capable of accepting a function. Last, I have an easier time setting up InjectionTokens when factory functions are available to utilize.
Downsides?
You're out of luck using this setup (and most of the other answers) if the module you want to configure doesn't allow a factory function to be passed into either forRoot() or forChild(), and there's no other way to configure the package by using a factory function.
Instructions
Using fetch to retrieve a json file, I store the object in window and raise a custom event. - remember to install whatwg-fetch and add it to your polyfills.ts for IE compatibility
Have an event listener listening for the custom event.
The event listener receives the event, retrieves the object from window to pass to an observable, and clears out what was stored in window.
Bootstrap Angular
-- This is where my solution starts to really differ --
Create a file exporting an interface whose structure represents your config.json -- it really helps with type consistency and the next section of code requires a type, and don't specify {} or any when you know you can specify something more concrete
Create the BehaviorSubject that you will pass the parsed json file into in step 3.
Use factory functions to reference the different sections of your config to maintain SOC
Create InjectionTokens for the providers needing the result of your factory functions
-- and/or --
Pass the factory functions directly into the modules capable of accepting a function in either its forRoot() or forChild() methods.
-- main.ts
I check window["environment"] is not populated before creating an event listener to allow the possiblilty of a solution where window["environment"] is populated by some other means before the code in main.ts ever executes.
import { enableProdMode } from '#angular/core';
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { configurationSubject } from './app/utils/environment-resolver';
var configurationLoadedEvent = document.createEvent('Event');
configurationLoadedEvent.initEvent('config-set', true, true);
fetch("../../assets/config.json")
.then(result => { return result.json(); })
.then(data => {
window["environment"] = data;
document.dispatchEvent(configurationLoadedEvent);
}, error => window.location.reload());
/*
angular-cli only loads the first thing it finds it needs a dependency under /app in main.ts when under local scope.
Make AppModule the first dependency it needs and the rest are done for ya. Event listeners are
ran at a higher level of scope bypassing the behavior of not loading AppModule when the
configurationSubject is referenced before calling platformBrowserDynamic().bootstrapModule(AppModule)
example: this will not work because configurationSubject is the first dependency the compiler realizes that lives under
app and will ONLY load that dependency, making AppModule an empty object.
if(window["environment"])
{
if (window["environment"].production) {
enableProdMode();
}
configurationSubject.next(window["environment"]);
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
}
*/
if(!window["environment"]) {
document.addEventListener('config-set', function(e){
if (window["environment"].production) {
enableProdMode();
}
configurationSubject.next(window["environment"]);
window["environment"] = undefined;
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
});
}
--- environment-resolvers.ts
I assign a value to the BehaviorSubject using window["environment"] for redundancy. You could devise a solution where your config is preloaded already and window["environment"] is already populated by the time any of your Angular's app code is ran, including the code in main.ts
import { BehaviorSubject } from "rxjs";
import { IConfig } from "../config.interface";
const config = <IConfig>Object.assign({}, window["environment"]);
export const configurationSubject = new BehaviorSubject<IConfig>(config);
export function resolveEnvironment() {
const env = configurationSubject.getValue().environment;
let resolvedEnvironment = "";
switch (env) {
// case statements for determining whether this is dev, test, stage, or prod
}
return resolvedEnvironment;
}
export function resolveNgxLoggerConfig() {
return configurationSubject.getValue().logging;
}
-- app.module.ts - Stripped down for easier understanding
Fun fact! Older versions of NGXLogger required you to pass in an object into LoggerModule.forRoot(). In fact, the LoggerModule still does! NGXLogger kindly exposes LoggerConfig which you can override allowing you to use a factory function for setup.
import { resolveEnvironment, resolveNgxLoggerConfig, resolveSomethingElse } from './environment-resolvers';
import { LoggerConfig } from 'ngx-logger';
#NgModule({
modules: [
SomeModule.forRoot(resolveSomethingElse)
],
providers:[
{
provide: ENVIRONMENT,
useFactory: resolveEnvironment
},
{
provide: LoggerConfig,
useFactory: resolveNgxLoggerConfig
}
]
})
export class AppModule
Addendum
How did I solve the creation of my API urls?
I wanted to be able to understand what each url did via a comment and wanted typechecking since that's TypeScript's greatest strength compared to javascript (IMO). I also wanted to create an experience for other devs to add new endpoints, and apis that was as seamless as possible.
I created a class that takes in the environment (dev, test, stage, prod, "", and etc) and passed this value to a series of classes[1-N] whose job is to create the base url for each API collection. Each ApiCollection is responsible for creating the base url for each collection of APIs. Could be our own APIs, a vendor's APIs, or even an external link. That class will pass the created base url into each subsequent api it contains. Read the code below to see a bare bones example. Once setup, it's very simple for another dev to add another endpoint to an Api class without having to touch anything else.
TLDR; basic OOP principles and lazy getters for memory optimization
#Injectable({
providedIn: 'root'
})
export class ApiConfig {
public apis: Apis;
constructor(#Inject(ENVIRONMENT) private environment: string) {
this.apis = new Apis(environment);
}
}
export class Apis {
readonly microservices: MicroserviceApiCollection;
constructor(environment: string) {
this.microservices = new MicroserviceApiCollection(environment);
}
}
export abstract class ApiCollection {
protected domain: any;
constructor(environment: string) {
const domain = this.resolveDomain(environment);
Object.defineProperty(ApiCollection.prototype, 'domain', {
get() {
Object.defineProperty(this, 'domain', { value: domain });
return this.domain;
},
configurable: true
});
}
}
export class MicroserviceApiCollection extends ApiCollection {
public member: MemberApi;
constructor(environment) {
super(environment);
this.member = new MemberApi(this.domain);
}
resolveDomain(environment: string): string {
return `https://subdomain${environment}.actualdomain.com/`;
}
}
export class Api {
readonly base: any;
constructor(baseUrl: string) {
Object.defineProperty(this, 'base', {
get() {
Object.defineProperty(this, 'base',
{ value: baseUrl, configurable: true});
return this.base;
},
enumerable: false,
configurable: true
});
}
attachProperty(name: string, value: any, enumerable?: boolean) {
Object.defineProperty(this, name,
{ value, writable: false, configurable: true, enumerable: enumerable || true });
}
}
export class MemberApi extends Api {
/**
* This comment will show up when referencing this.apiConfig.apis.microservices.member.memberInfo
*/
get MemberInfo() {
this.attachProperty("MemberInfo", `${this.base}basic-info`);
return this.MemberInfo;
}
constructor(baseUrl: string) {
super(baseUrl + "member/api/");
}
}
I find this Angular How-to: Editable Config Files from Microsoft Dev blogs being the best solution. You can configure dev build settings or prod build settings.
If running in a container, this article https://pumpingco.de/blog/environment-variables-angular-docker/ shows how to use envsubst on the entry point command to fix this static compile issue on the fly via environment variable.