3

My Question is similar to this DI with cyclic dependency with custom HTTP and ConfigService but not exactly same and solution provided for this question does not solve the issue for me.

I am using httpInterceptor and also need configService to pull environment info at run time. Here is how it looks like:

//app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, APP_INITIALIZER, Injector } from '@angular/core';
import { Router } from '@angular/router';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpModule, Http, Request, RequestOptionsArgs, Response,     XHRBackend, RequestOptions, ConnectionBackend, Headers } from '@angular/http';

import { CacheService, CacheStoragesEnum } from 'ng2-cache/ng2-cache';

import { AppConfig }       from './app.config';

import { AppComponent } from './app.component';
import { HttpInterceptorService}  from './services/http-interceptor/http-interceptor.service';

@NgModule({
  declarations: [
    AppComponent
  ],

  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    routing,
    ReactiveFormsModule
  ],
  providers: [    
    CacheService,
    AppConfig,        
    { provide: APP_INITIALIZER, useFactory: (config: AppConfig) => config.load(), deps: [AppConfig]},
    { provide: Http, useFactory: (backend: ConnectionBackend, defaultOptions: RequestOptions, router: Router, cacheService: CacheService,  appConfig: AppConfig) => return new HttpInterceptorService(backend, defaultOptions, router, cacheService, appConfig), deps: [XHRBackend, RequestOptions, Router, CacheService, AppConfig]}
],
bootstrap: [AppComponent]
})
export class AppModule { }

app.config.ts

//app.config.ts
import { Inject, Injectable, Injector } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Rx';

@Injectable()
export class AppConfig {

    private config: Object = null;
    private env:    Object = null;
    private http: Http;

    constructor(private injector : Injector) {
      setTimeout(() => {
        console.log('initialize http in appConfig');
        this.http = this.injector.get(Http);          
      });          
    }

    /**
     * Use to get the data found in the second file (config file)
     */
    public getConfig(key: any) {
        return this.config[key];
    }

    /**
     * Use to get the data found in the first file (env file)
     */
    public getEnv(key: any) {
        return this.env[key];
    }

        /**
     * This method:
     *   a) Loads "env.json" to get the current working environment (e.g.: 'production', 'development')
     *   b) Loads "config.[env].json" to get all env's variables (e.g.: 'config.development.json')
     */
    public load() {
        console.log('---------------------- in appConfig load method --------------------');
        return new Promise((resolve, reject) => {
            this.http.get('app/env.json').map( res => res.json() ).catch((error: any):any => {
                console.log('Configuration file "env.json" could not be read');
                resolve(true);
                return Observable.throw(error.json().error || 'Server error');
            }).subscribe( (envResponse) => {
                this.env = envResponse;
                console.log('-------------------------------------------------');
                console.log(envResponse);

                let request:any = null;

                switch (envResponse['env']) {
                    case 'production': {
                        request = this.http.get('app/config.' + envResponse['env'] + '.json');
                    } break;

                    case 'test': {
                        request = this.http.get('app/config.' + envResponse['env'] + '.json');
                    } break;

                    case 'default': {
                        console.error('Environment file is not set or invalid');
                        resolve(true);
                    } break;
                }

                if (request) {
                    request
                        .map( res => res.json() )
                        .catch((error: any) => {
                            console.error('Error reading ' + envResponse['env'] + ' configuration file');
                            resolve(error);
                            return Observable.throw(error.json().error || 'Server error');
                        })
                        .subscribe((responseData) => {
                            this.config = responseData;
                            resolve(true);
                        });
                } else {
                    console.error('Env config file "env.json" is not valid');
                    resolve(true);
                }
            });

        });
    }
}

And here is interceptor

//http-interceptor.service.ts
import {Http, Request, RequestOptionsArgs, Response, XHRBackend, RequestOptions, URLSearchParams,ConnectionBackend, Headers} from '@angular/http';
import {Router} from '@angular/router';
import { Observable } from 'rxjs/Observable';
import {CacheService, CacheStoragesEnum} from 'ng2-cache/ng2-cache';

import { AppConfig } from '../../app.config';

const TOKEN_EXPIRY = '1440';
const CACHE_EXPIRY = 10 * 60 * 1000;
const CACHE_RECYCLE = 4 * 60 * 1000;

export class HttpInterceptorService extends Http {

  private webUrl : string;
  private serviceUrl : string;
    constructor(backend: ConnectionBackend,
                defaultOptions: RequestOptions,
                private _router: Router,
                private _cacheService: CacheService,
              private config: AppConfig) {
      super(backend, defaultOptions);
      this._cacheService = this._cacheService.useStorage(CacheStoragesEnum.LOCAL_STORAGE);
    }

    request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
        return this.getToken(url).flatMap(tokenRecord =>super.request(url, this.getRequestOptionArgs(tokenRecord, options)));

    }


    get(url: string, options?: RequestOptionsArgs): Observable<Response> {
      return this.getToken(url).flatMap(tokenRecord => super.get(url, this.getRequestOptionArgs(tokenRecord, options)))
    }

    post(url: string, body: string, options?: RequestOptionsArgs): Observable<Response> {
      return this.getToken(url).flatMap(tokenRecord => super.post(url, body, this.getRequestOptionArgs(tokenRecord, options)))
    }

    put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
        return this.getToken(url).flatMap(tokenRecord => super.put(url, body, this.getRequestOptionArgs(tokenRecord, options)));
    }

    delete(url: string, options?: RequestOptionsArgs): Observable<Response> {
        return this.getToken(url).flatMap(tokenRecord => super.delete(url, this.getRequestOptionArgs(tokenRecord, options)));
    }

    getRequestOptionArgs(tokenRecord, options?: RequestOptionsArgs) : RequestOptionsArgs {
        if (options == null) {
            options = new RequestOptions();
        }

        if (options.headers == null) {
            options.headers = new Headers();
        }

        options.headers.append('Content-Type', 'application/json');
        options.headers.append('Authorization', tokenRecord.token);

        return options;
    }

    getToken (url) {
      let domain;

      this.webUrl = this.config.getConfig('webUrl');
      this.serviceUrl = this.config.getConfig('serviceUrl');

      if (this.serviceUrl.indexOf("://") > -1) {
        domain = this.serviceUrl.split('/')[2];
      } else {
        domain = this.serviceUrl.split('/')[0];
      }
      domain = domain.split(':')[0];

      let serviceHost = domain;

      let headers = new Headers(); 
      headers.append('Accept', 'application/json, text/plain, */*'); 
      let params = new URLSearchParams();

      params.append('service', serviceHost);
      params.append('expiry', TOKEN_EXPIRY);
      let options = new RequestOptions({ headers:headers, search:params, withCredentials:true });

      let cacheName = "webCache";
      let cacheKeyBase = "/token";
      let cacheKey = cacheKeyBase + "/" + serviceHost;

      let existingCache =  this._cacheService.get(cacheKey);
      if(existingCache) {
        return Observable.of(existingCache);

      }
      return super.get(this.webUrl+'/a/token', options)
      .map((response:Response) => {
        response = response.json();

        let tokenType = response['tokenType'];
        let tokenVal = response['access_token'];
        let token = tokenType + " " + tokenVal;
        let tokenRecord = {
            token:token,
            tokenType:response['tokenType'],
            expiresAt: new Date(response['expires_at'] * 1000),
            expiresIn: response['expires_in'],
            aquiredOn: new Date()
        };

        this._cacheService.set(cacheKey, tokenRecord, {maxAge: CACHE_EXPIRY});

        return tokenRecord;
      }).catch(this.handleError);

    }

    private handleError(error:Response){
      return Observable.throw(error || 'Server error');
    }

}

Here is the error I am getting

Unhandled Promise rejection: Cannot read property 'componentTypes' of undefined ; Zone: <root> ; Task: Promise.then ; Value: TypeError: Cannot read property 'componentTypes' of undefined(…) TypeError: Cannot read property 'componentTypes' of undefined
at setupRouter 

Please let me know if I need to provide more information.

Community
  • 1
  • 1
skgyan
  • 139
  • 2
  • 10

1 Answers1

2

I figured out a way to solve the issue, this how I have injected appConfig

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, APP_INITIALIZER, Injector } from '@angular/core';
import { Router } from '@angular/router';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpModule, Http, Request, RequestOptionsArgs, Response,     XHRBackend, RequestOptions, ConnectionBackend, Headers } from '@angular/http';

import { CacheService, CacheStoragesEnum } from 'ng2-cache/ng2-cache';

import { AppConfig }       from './app.config';

import { AppComponent } from './app.component';
import { HttpInterceptorService}  from './services/http-interceptor/http-interceptor.service';

export function createAppModule(conf) {
@NgModule({
  declarations: [
    AppComponent
  ],

  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    routing,
    ReactiveFormsModule
  ],
  providers: [    
    CacheService,
   { provide: Config, useValue: conf },
    {
      provide: AppConfig,
      useFactory: (_config: Config) => {
        return new AppConfig(_config);
      }, deps: [Config]
    },
    { provide: Http, useFactory: (backend: ConnectionBackend, defaultOptions: RequestOptions, router: Router, cacheService: CacheService,  appConfig: AppConfig) => return new HttpInterceptorService(backend, defaultOptions, router, cacheService, appConfig), deps: [XHRBackend, RequestOptions, Router, CacheService, AppConfig]}
],
bootstrap: [AppComponent]
})
class AppModule { }
return AppModule;
}

I made the changes in main.ts to use AppModule and pulling config.json, here how it looks like

import './polyfills.ts';
import { ReflectiveInjector, Injectable, OpaqueToken, Injector, NgModule, enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import {
  Http, CookieXSRFStrategy, XSRFStrategy, RequestOptions, BaseRequestOptions,
  ResponseOptions, BaseResponseOptions, XHRBackend, BrowserXhr, Response
} from '@angular/http';

import { createAppModule } from './app/app.module';

function getHttp(): Http {
  let providers = [
    {
      provide: Http, useFactory: (backend: XHRBackend, options: RequestOptions) => {
        return new Http(backend, options);
      },
      deps: [XHRBackend, RequestOptions]
    },
    BrowserXhr,
    { provide: RequestOptions, useClass: BaseRequestOptions },
    { provide: ResponseOptions, useClass: BaseResponseOptions },
    XHRBackend
  ];
  return ReflectiveInjector.resolveAndCreate(providers).get(Http);
}

getHttp().get('/assets/config.json').toPromise()
  .then((res: Response) => {
    let conf = res.json();
    platformBrowserDynamic().bootstrapModule(createAppModule(conf));
  })
  .catch(error => { console.error(error) });

There might be better ways of doing this but I found this working for me but anyone has better approach and any suggestion then please provide your comments.

skgyan
  • 139
  • 2
  • 10
  • 3
    any follow up on this? I'm running into the same issue with Angular 6. – jetset May 31 '18 at 22:36
  • @ChamikaSandamal I was not able to resolve this. Instead I ended up encrypting my secrets and committing them to source control. Before I deploy or start up my development server I decrypt the secrets for the environment I'm running in. Here's a gist that outlines the approach I'm using: https://gist.github.com/lucasklaassen/739181e621dcb187a1d0e08c82e8d9c6 – jetset Sep 12 '18 at 19:54