Platform: @ngrx/data Allows to provide custom DataService for all entity types.

Created on 16 Jun 2019  路  4Comments  路  Source: ngrx/platform

As @ngrx/data is not supports by self entity pagination, and is no way to simple take the total entity count from server side response, I tried to write own DataService which will allows me to take that number from response (eg. in case of json-server it is in X-Total-Count header) and to smuggle it as a additional property:

@Injectable()
export class MyDataService<T> extends DefaultDataService<T> {
    constructor(entityName: string, http: HttpClient, httpUrlGenerator: HttpUrlGenerator, config: DefaultDataServiceConfig) {
        super(entityName, http, httpUrlGenerator, config);
    }

    protected execute(method: "DELETE" | "GET" | "POST" | "PUT", url: string, data?: any, options?: any): Observable<any> {
        options = {
            ...options,
            observe: "response"
        };
        return super.execute(method, url, data, options).pipe(
            map((response: HttpResponse<any>) => {
                if(response.headers && response.headers['X-Total-Count']) {
                    response.body.totalCount = parseInt(response.headers['X-Total-Count'], 10);
                    return response.body;
                } else {
                    return response.body;
                }
            })
        );
    }
}

The problem is that I want to use that DataService for each type of my entities, but this class has no way to get the entityName like a DefaultDataService is taking it from own factory. So only one existing solution is to create custom XYZDataService class per each type of entity, what in complex case, when I have many types of entities this can be a really painful.

Also the EntityDataService is expecting instances of DataService during the registration, what means that we need to create all instances of DataService upfront, on module initialization, event in case when some entity type will be not used, this is very inefficient.

As I see there is also not way to replace DefaultDataServiceFactory.

Proposal

This will be very useful, convenient and efficient to have the way to register custom DataServiceFactory on EntityDataService.

Eg.:

abstract class DataServiceFactory {
    create<T>(entityName: string): EntityCollectionDataService<T> 
}

class MyDataServiceFactory {
    constructor(private http: HttpClient, private httpUrlGenerator: HttpUrlGenerator, private config: DefaultDataServiceConfig) {
    }
    create<T>(entityName: string): EntityCollectionDataService<T> {
       return new MyDataService(entityName, http, httpUrlGenerator, config);
    }  
}

In such way I will be still able to deliver different DataService per entityType:

    create<T>(entityName: string): EntityCollectionDataService<T> {
       if(entityName === 'Hero') return MyHeroDataService(http, httpUrlGenerator, config);
       return new MyDataService(entityName, http, httpUrlGenerator, config);
    }  

Probably enough will be to modify the constructor of EntityDataService and inject optional second parameter of DataServiceFactory (there is a todo probably for same problem :) )and in getService add additional condition.

@Injectable()
 export class EntityDataService {
   ...
   constructor(
        protected defaultDataServiceFactory: DefaultDataServiceFactory
        @Optional() protected dataServiceFactory: DataServiceFactory  ) {}

  getService<T>(entityName: string): EntityCollectionDataService<T> {
     entityName = entityName.trim();
     let service = this.services[entityName];
     if (!service) {
       if (this.dataServiceFactory) {
             service = this.dataServiceFactory.create(entityName);
       } else {
            service = this.defaultDataServiceFactory.create(entityName);
       }
       this.services[entityName] = service;
     }
     return service;
   }

I can create PR.

Data

Most helpful comment

I've managed to replace DefaultDataServiceFactory with custom implementation so that now I can use pagination sending TotalCount additional data from service, f.e. response can be:

{
  "entities": "{},{}",
  "total": 15
}

So, I think this can solve your problem, @majo44

Custom dataservice factory

@Injectable()
export class ExtendedDataServiceFactory {
    constructor(
        protected http: HttpClient,
        protected httpUrlGenerator: HttpUrlGenerator,
        @Optional() protected config?: DefaultDataServiceConfig
      ) {
        config = config || {};
        httpUrlGenerator.registerHttpResourceUrls(config.entityHttpResourceUrls);
      }
      /**
       * Create a default {EntityCollectionDataService} for the given entity type
       * @param entityName Name of the entity type for this data service
       */
      create<T>(entityName: string): EntityCollectionDataService<T> {
        return new ExtendedDataservice<T>(this.http, entityName, this.httpUrlGenerator, this.config);
      }
}

Custom dataservice:

@Injectable({
  providedIn: 'root'
})
export class ExtendedDataservice<T> extends DefaultDataService<T> {

  constructor(http: HttpClient, entityName: string, httpUrlGenerator: HttpUrlGenerator, config?: DefaultDataServiceConfig) {
    super(entityName, http, httpUrlGenerator, config);
  }


  getWithQuery(params: string | QueryParams): Observable<T[]> {
    return super.getWithQuery(params).pipe(
      tap(res => console.log(res)),
      map((res: any) => res.entities)
    );
  }

}

And register it like this:

@NgModule({
  imports: [
    CommonModule,
    EntityDataModule.forRoot({ entityMetadata: entityMetadata()})
  ],
  declarations: [],
  providers: [
    {
      provide: DefaultDataServiceConfig,
      useValue: defaultDataServiceConfig
    },
    {
      provide: DefaultDataServiceFactory,
      useClass: ExtendedDataServiceFactory
    }
  ]
})
export class EntityStoreModule {
  constructor(entityDataService: EntityDataService) {
  }
}

All 4 comments

@majo44, I just created some documentation https://github.com/ngrx/platform/pull/1921 about how to create customize EntityCollection level property, the motivation that I did that is also to add pagination, I am working on some other PRs to make it simpler. One of those PR is https://github.com/ngrx/platform/pull/1944, there is a tag work as the option which will live in the full lifecycle of the ngrx/data context, I am working on to make it also available on DataService.

@majo44, I just created some documentation #1921 about how to create customize EntityCollection level property, the motivation that I did that is also to add pagination, I am working on some other PRs to make it simpler. One of those PR is #1944, there is a tag work as the option which will live in the full lifecycle of the ngrx/data context, I am working on to make it also available on DataService.

still don't know how to custom http url for entity.

Say I have entities group by sidebar menu. The backend api urls are '/api/menu1/hero', '/api/menu2/book'.
if use 'EntityCollectionDataService', just

export class HeroService extends DefaultDataService<Hero> {
  constructor(
    http: HttpClient, httpUrlGenerator: HttpUrlGenerator,
  ) {
    super('Hero', http, httpUrlGenerator, {
      root: 'api/menu1,
    });
  }
}

because 'EntityCollectionDataService' doesn't dispatch actions, so I turn to use EntityCollectionService.
But how to config the root url?

I've managed to replace DefaultDataServiceFactory with custom implementation so that now I can use pagination sending TotalCount additional data from service, f.e. response can be:

{
  "entities": "{},{}",
  "total": 15
}

So, I think this can solve your problem, @majo44

Custom dataservice factory

@Injectable()
export class ExtendedDataServiceFactory {
    constructor(
        protected http: HttpClient,
        protected httpUrlGenerator: HttpUrlGenerator,
        @Optional() protected config?: DefaultDataServiceConfig
      ) {
        config = config || {};
        httpUrlGenerator.registerHttpResourceUrls(config.entityHttpResourceUrls);
      }
      /**
       * Create a default {EntityCollectionDataService} for the given entity type
       * @param entityName Name of the entity type for this data service
       */
      create<T>(entityName: string): EntityCollectionDataService<T> {
        return new ExtendedDataservice<T>(this.http, entityName, this.httpUrlGenerator, this.config);
      }
}

Custom dataservice:

@Injectable({
  providedIn: 'root'
})
export class ExtendedDataservice<T> extends DefaultDataService<T> {

  constructor(http: HttpClient, entityName: string, httpUrlGenerator: HttpUrlGenerator, config?: DefaultDataServiceConfig) {
    super(entityName, http, httpUrlGenerator, config);
  }


  getWithQuery(params: string | QueryParams): Observable<T[]> {
    return super.getWithQuery(params).pipe(
      tap(res => console.log(res)),
      map((res: any) => res.entities)
    );
  }

}

And register it like this:

@NgModule({
  imports: [
    CommonModule,
    EntityDataModule.forRoot({ entityMetadata: entityMetadata()})
  ],
  declarations: [],
  providers: [
    {
      provide: DefaultDataServiceConfig,
      useValue: defaultDataServiceConfig
    },
    {
      provide: DefaultDataServiceFactory,
      useClass: ExtendedDataServiceFactory
    }
  ]
})
export class EntityStoreModule {
  constructor(entityDataService: EntityDataService) {
  }
}

Thanks @VladMstv! If someone wants to follow-up with a PR for the docs that would be great

Was this page helpful?
0 / 5 - 0 ratings