diegomvh / angular-odata

Client side OData typescript library for Angular
https://www.npmjs.com/package/angular-odata
MIT License
50 stars 14 forks source link

No easy way to provide config asynchronously #78

Closed rcpp0 closed 1 year ago

rcpp0 commented 1 year ago

Hello, in the latest versions of angular, using the environment.ts is no longer the preferred option to configure your app and therefore many people like myself use the http client to get an appconfig.json file asynchronously and inject it using the APP_INITIALIZER token.

In this regard, it is not possible to provide platform-dependent config to the ODataModule using ODataModule.forRoot().

I resolved the issue by inspiring myself from the "forRoot()" implementation from your code to inject the ODATA_CONFIGURATIONS token using a factory rather than a static value, using the following template in the imports section of my AppModule : 

export function createSettings(configs: ApiConfig[]) {
  return new ODataSettings(...configs);
}

const oDataConfigFactory = (appConfigService: AppconfigService) => {
  return [{serviceRootUrl: appConfigService.config.odataApiUrl}]
}
imports([
// ...
{
      ngModule: ODataModule,
      providers: [
        {
          provide: ODATA_CONFIGURATIONS,
          useFactory: oDataConfigFactory,
          deps: [AppconfigService]
        },
        {
          provide: ODataSettings,
          useFactory: createSettings,
          deps: [ODATA_CONFIGURATIONS],
        },
        ODataClient,
        ODataServiceFactory,
      ],
    }
// ...
]

So, not really an issue but more like a feature request, it would be great if you included a native way to do this.

Other than that, cool library, very useful ;)

diegomvh commented 1 year ago

Hi @rcpp0 and thanks for your idea for a new feature I changed the code and added "Config Loaders" for synchronous and asynchronous loading of the configuration. The inspiration come from angular-auth-oidc-client.

The idea is to be able to take the configuration from the config attribute or declare a provider in loader to retrieve the configuration using a factory that returns a synchronous or asynchronous loader.

/* Static Config */
@NgModule({
  imports: [
    ...
    ODataModule.forRoot({
      config: Object.assign(MiApiConfig, {
        serviceRootUrl: environment.apiUrl
      })
      /* Or config array */
    }),
    ...
/* Sync Loader */
export function odataConfigLoaderFactory() {
  return new ODataConfigSyncLoader(
    Object.assign(MiApiConfig, {
      serviceRootUrl: environment.apiUrl
    })
  );
}
@NgModule({
  imports: [
    ...
    ODataModule.forRoot({
      loader: {
        provide: ODataConfigLoader,
        useFactory: odataConfigLoaderFactory
      },
    }),
    ...
/* Async Loader */
export const odataHttpConfigLoaderFactory = (httpClient: HttpClient) => {
  const config$ = httpClient.get<any>(`https://...`).pipe(
    map((customConfig: any) => {
      return {
        serviceRootUrl: customConfig.apiUrl,
        /* Your config mapping here */
      };
    })
  );

  return new ODataConfigAsyncLoader(config$);
};
@NgModule({
  imports: [
    ...
    ODataModule.forRoot({
      loader: {
        provide: ODataConfigLoader,
        useFactory: odataHttpConfigLoaderFactory,
        deps: [HttpClient],
      },
    }),
    ...

I also did an update of the demo project to see how the new feature works. I hope to release the new version soon.

Again, thank you very much for the idea and for giving the library a chance.

rcpp0 commented 1 year ago

Thanks for that fast update !

Looking into the changes you’ve made, I was thinking it was a bit overengineered but after looking into the issue of asynchronous DI your solution might actually be the safest. The solution I originally used was inspired by some guides I found online stating that Provider factories could work with Promises as a result type but i couldn’t find any confirmation from Angular’s official documentation, although Observables seem to be a no go.

All I can say is that using a promise as a provider factory result does in fact work in my project.

Anyway, the approach you implemented, as in wrapping the observable responsible for loading the config in an object and observing it when necessary seems on par with this Angular dev post on an open thread about this subject : https://github.com/angular/angular/issues/23279 (see bryanrideshark’s post).

The last post on the thread also proposes another solution for dealing with this issue but I didn’t look into it in depth. Anyway, thanks for your work !