0

I have an Angular application where I want to mount a bunch of variables from a JSON file into a configService that the angular app gets from its own web server, so I can inject it in a container as a configMap.

Then I want this service and the config member property of the service to be accessible throughout the application.

This is what I have as of now:

@Injectable({
  providedIn: 'root'
})
export class ConfigService {

  configUrl = '/config/cfg.json';
  public config: Config | undefined;
  constructor(private httpClient: HttpClient) {
  }

  getConfig(): Observable<Config> {
    return this.httpClient.get<Config>(window.location.origin + this.configUrl)
      .pipe(
        catchError(this.handleError<Config>("getConfig", this.config = { apiBaseUrl: "", adminLoginUrl: "", clientLoginUrl: "" }))
      )
  }
  getConfigObject() : Config | undefined{
    if (this.config?.apiBaseUrl == undefined) {
      this.getConfig().subscribe((data: Config) => this.config = {
        apiBaseUrl: data.apiBaseUrl,
        adminLoginUrl: data.adminLoginUrl,
        clientLoginUrl: data.clientLoginUrl
      })

    }
    return this.config;
  }
//error handling omitted
}

When calling subscribe on the getConfig() method, I eventually get the data but I can't use it to immediately afterward call a function on that apiBaseUrl, it's then "undefined". When I try to call getConfigObject() it doesn't return the object, at least not at that moment in time.

What I want to do is to use this config.apiBaseUrl when I call my backend api. So I can do this in another service/component/module:

@Injectable({
  providedIn: 'root'
})
export class InvoiceService {

  config: Config | undefined;
  invoices: InvoiceItem[] | undefined;
  confSub!: Subscription;
  invSub!: Subscription;
//Idea 1, set the local config object in the constructor, does not work:
constructor(private configSvc: ConfigService, private httpClient: HttpClient) {
    this.config = this.configSvc.getConfigObject()
  }

getInvoices(): Observable<InvoiceItem[]> {
    this.config = this.configSvc.config
    console.log(this.config?.apiBaseUrl + "api/invoices");
    //This above log results in just "api/invoices" in the console so the apiBaseUrl isn't loaded yet.
    return this.httpClient.get<InvoiceItem[]>(this.config?.apiBaseUrl + "api/invoices")
      .pipe(
        catchError(this.handleError("getInvoices", this.invoices = []))
      );
  }

//idea 2: Get the configObject in the ngOnInit() (which I have, maybe wrongfully, added to a service) by calling the method that returns an observable. 
  ngOnInit(): void {
    this.confSub = this.configSvc.getConfig()
      .subscribe({
        next: (data: Config) => this.config = data,
        error: (err: Error) => console.log("Could not get config, " + err)
      });
  }

I somehow got it working yesterday by nesting the calls to configSvc.getConfig() and invoiceService.getInvoices() but that resulted in spamming the invoices endpoint and isn't a solution that I'd expect to be using.

According to the angular documentation, an injectable service is supposed to be a singleton which makes me expect that the ngOnInit() method calling this.getConfig() would populate the instance variable "config" with values and then this instance would be accessible and usable by injecting it in the constructor as private configSvc: ConfigService and then by calling this.configSvc.config.apiBaseUrl but that doesn't work either, it returns undefined.

I'm using the latest stable angular version.

What am I missing?

Edit: I am aware that inside:

this.confSub = this.configSvc.getConfig()
      .subscribe({
        next: (data: Config) => {
          //HERE
          this.config = data,
        error: (err: Error) => console.log("Could not get config, " + err)
      });

the config data is accessible and I could, in theory, call the backend to get invoices but I can't then return that as an observable from the getInvoices function. This doesn't work:

return this.configSvc.getConfig()
      .subscribe({
        next: (data: Config) => {
          this.config = data;
          return this.httpClient.get<Config>(window.location.origin + this.configUrl)
            .pipe(
              catchError(this.handleError<Config>("getConfig", this.config = { apiBaseUrl: "", adminLoginUrl: "", clientLoginUrl: "" }))
      )
        },
        error: (err: Error) => console.log("Could not get config, " + err)
      });

As it would return the config observable and not the InvoiceItem[] observable. I'm going to need the backend api url in multiple places so I just want to be able to inject it and use it like this:

constructor(private httpClient: HttpClient, private configSvc: ConfigService) {}
...
return this.httpClient.get<something>(this.configSvc.config?.apiBaseUrl + "/some/endpoint")

Like a singleton object that is mounted at startup and stays alive and accessible throughout the life cycle. Nesting calls to configService every time I want to get something from a backend endpoint seems unnecessary, or I don't quite understand how this works.

Arizon
  • 56
  • 1
  • 10
  • Does this answer your question? [How do I return the response from an Observable/http/async call in angular?](https://stackoverflow.com/questions/43055706/how-do-i-return-the-response-from-an-observable-http-async-call-in-angular) – Batajus Dec 22 '21 at 09:46
  • @Batajus Thanks. I'm aware that inside the subscribe (on the getConfig) I can access the apiBaseUrl but I don't know how to return an Observable from inside the subscribe callback. See my edit above. – Arizon Dec 22 '21 at 11:07
  • I also think you may want to use a BehviorSubject vs just a Subject - https://stackoverflow.com/questions/43348463/what-is-the-difference-between-subject-and-behaviorsubject – Edwin M. Cruz Dec 22 '21 at 13:07

1 Answers1

0

EDIT #2: I ended up using the APP_INITIALIZER to get this working. It is described here and this is the way to go: APP_INITIALIZER config loader

Like Edvin M. Cruz is mentioning, I made this workaround solution using Subject to get an Observable inside the nested function:

  getInvoices(): Observable<InvoiceItem[]> {
    var subject = new Subject<InvoiceItem[]>();
    this.configSvc.getConfig().subscribe({
        next: (data: Config) => {

            this.httpClient.get<InvoiceItem[]>(data.apiBaseUrl +"api/invoices")
            .pipe(
              catchError(this.handleError("getInvoices", this.invoices = []))
            ).subscribe({
              next: (invs: InvoiceItem[]) => {
                subject.next(invs);
              }
            });
        }
      });
    return subject.asObservable()
  }

I still feel like there should be a way to make the ConfigService a singleton instance that I could inject anywhere and get the backend api url from it.

EDIT: This is how I solved it.

Maybe this is not best practice, but it works and it's loading the config just once when the client loads the application, and it's available to other services throughout the application. I'm open to suggestions to improve it if anyone wants to contribute.

@Injectable({
  providedIn: 'root'
})
export class ConfigService {

  configUrl = '/config/cfg.json';
  public config: Config | undefined;
  constructor(private httpClient: HttpClient) {
    this.getConfig().subscribe({
      next: (data: Config) => {
        this.config = data
      },
      error: (err) => this.handleError<Config>("constructor",this.config = { apiBaseUrl: "", adminLoginUrl: "", clientLoginUrl: "" })
    })
  }

  getConfig(): Observable<Config> {
    return this.httpClient.get<Config>(window.location.origin + this.configUrl)
      .pipe(
        catchError(this.handleError<Config>("getConfig", this.config = { apiBaseUrl: "", adminLoginUrl: "", clientLoginUrl: "" }))
      )
  }
}

And where I need the apiBaseUrl in another service, I do this(configService has now been eagerly resolved during startup and the config value is accessible and is only fetched once per client app load):

@Injectable({
  providedIn: 'root'
})
export class InvoiceService {

  config: Config | undefined;
  constructor(private configSvc: ConfigService, private http: HttpClient) { }

  getInvoices(): Observable<InvoiceItem[]> {

    return this.http.get<InvoiceItem[]>(this.configSvc?.config?.apiBaseUrl + "api/invoices")
    .pipe(
      catchError(this.handleError("getInvoiceItems", this.invoices = []))
    );
//...
}

This was my reasoning: I am ok by deviating from the pattern of returning an observable from a service in this particular case since it's a one time per application case where the app config is loaded form its own web server. In all other services (like the invoice example above) I return an observable like one should do. By doing it this way, I can mount a ConfigMap volume containing the current environment's backend url into the container in kubernetes:

apiVersion: v1
data:
  cfg.json: |
    {
        "apiBaseUrl": "http://backend.site.url:5000/",
        "adminLoginUrl": "https://login-admin.example.com",
        "clientLoginUrl": "https://login-client.example.com"
    }
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: frontend-config

And then mount it:

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: app-name
  name: app-name
spec:
  replicas: 3
  selector:
    matchLabels:
      app: app-name
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: app-name
    spec:
      containers:
      - image: docker.io/repo/image:tag
        imagePullPolicy: Always
        name: app-name
        resources: {}
        volumeMounts:
        - name: config
          mountPath: /usr/share/nginx/html/config
      volumes:
      - name: config
        configMap:
          name: frontend-config
          items:
          - key: cfg.json
            path: cfg.json
status: {}

Arizon
  • 56
  • 1
  • 10
  • Maybe you can use some ideas mentioned here to initialize the data in your config service: https://stackoverflow.com/questions/56092083/async-await-in-angular-ngoninit/58474585#58474585. – Edwin M. Cruz Dec 22 '21 at 14:55