Skip to content

Latest commit

 

History

History
205 lines (152 loc) · 9.47 KB

entity-dataservice.md

File metadata and controls

205 lines (152 loc) · 9.47 KB

Entity DataService

The ngrx-data library expects to persist entity data with calls to a REST-like web api with endpoints for each entity type.

The EntityDataService maintains a registry of service classes dedicated to persisting data for a specific entity type.

When the ngrx-data library sees an action for an entity persistence operation, it asks the EntityDataService for the registered data service that makes HTTP calls for that entity type, and calls the appropriate service method.

A data service is an instance of a class that implements the EntityCollectionDataService<T> interface. This interface supports a basic set of CRUD operations that return Observables:

Method Meaning
add(entity: T) Add a new entity
delete(id: any) Delete an entity by primary key value
getAll() Get all instances of this entity type
getById(id: any) Get an entity by its primary key
getWithQuery(queryParams: QueryParams | string) Get entities that satisfy the query
update(update: Update<T>) Update an existing entity

QueryParams is a parameter-name/value map You can also supply the query string itself. HttpClient safely encodes both into an encoded query string.

Update<T> is an object with a strict subset of the entity properties. It must include the properties that participate in the primary key (e.g., id). The update property values are the properties-to-update; unmentioned properties should retain their current values.

The default data service methods return the Observables returned by the corresponding Angular HttpClient methods.

If you create your own data service alternatives, they should return similar Observables.

Register data services

The EntityDataService registry is empty by default.

You can add custom data services to it by creating instances of those classes and registering them with EntityDataService in one of two ways.

  1. register a single data service by entity name with the registerService() method.

  2. register several data services at the same time with by calling registerServices with an entity-name/service map.

You can create and import a module that registers your custom data services as show in the EntityDataService tests

If you decide to register an entity data service, be sure to do so before you ask ngrx-data to perform a persistence operation for that entity.

Otherwise, the ngrx-data library will create and register an instance of the default data service DefaultDataService<T> for that entity type.

The DefaultDataService

The demo app doesn't register any entity data services. It relies entirely on a DefaultDataService<T>, created for the entity type by the injected DefaultDataServiceFactory.

A DefaultDataService<T> makes REST-like calls to the server's web api with Angular's HttpClient.

It composes HTTP URLs from a root path (see "Configuration" below) and the entity name.

For example,

  • if the persistence action is to delete a hero with id=42 and
  • the root path is 'api' and
  • the entity name is 'Hero', then
  • the DELETE request URL will be 'api/hero/42'.

When the persistence operation concerns multiple entities, the DefaultDataService substitutes the plural of the entity type name for the resource name.

The QUERY_ALL action to get all heroes would result in an HTTP GET request to the URL 'api/heroes'.

The DefaultDataService doesn't know how to pluralize the entity type name. It doesn't even know how to create the base resource names. It relies on an injected HttpUrlGenerator service those. And the default implementation of that generator relies on the Pluralizer service to get the collection resource name. The Entity Metadata guide explains how to configure the default Pluralizer .

Configure the DefaultDataService

The collection-level data services construct their own URLs for HTTP calls. They typically rely on shared configuration information such as the root of every resource URL.

The shared configuration values are almost always specific to the application and may vary according the runtime environment.

The ngrx-data library defines a DefaultDataServiceConfig class for conveying shared configuration to an entity collection data service.

The most important configuration property, root, returns the root of every web api URL, the parts that come before the entity resource name.

For a DefaultDataService<T>, the default value is 'api', which results in URLs such as api/heroes.

The timeout property sets the maximum time (in ms) before the ng-lib persistence operation abandons hope of receiving a server reply and cancels the operation. The default value is 0, which means that requests do not timeout.

The delete404OK flag tells the data service what to do if the server responds to a DELETE request with a 404 - Not Found.

In general, not finding the resource to delete is harmless and you can save yourself the headache of ignoring a DELETE 404 error by setting this flag to true, which is the default for the DefaultDataService<T>.

When running a demo app locally, the server may respond more quickly than it will in production. You can simulate real-world by setting the getDelay and saveDelay properties.

Further customization

While the ngrx-data library provides a configuration object to modify certain aspects of the DefaultDataService, you may want to further customize what happens when you save or retrieve data. A good example is performing a map on the items returned to convert strings to dates, or to add additional properties to a specific entity. This is possible by registering a custom service with the EntityDataService provider.

To illustrate this we'll use an example where we add a date property to an entity when it was loaded from the API into the application state. If we use the Tour of Heroes as our example, let's add a assume there's a new property on Hero called dateLoaded:

export class Hero {
  id: number;
  name: string;
  saying: string;
  dateLoaded: Date;
}

Then we need to create a class that implements the EntityCollectionDataService<T> interface. One way to do this is to extend the existing DefaultDataSerivce<T> class like so (for the Hero class in our case):

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
  EntityCollectionDataService,
  DefaultDataService,
  HttpUrlGenerator,
  QueryParams
} from 'ngrx-data';
import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators';
import { Hero } from '../core';

@Injectable()
export class HeroEntityDataService extends DefaultDataService<Hero> {
  constructor(http: HttpClient, httpUrlGenerator: HttpUrlGenerator) {
    super('Hero', http, httpUrlGenerator);
  }

  getAll(): Observable<Hero[]> {
    return super.getAll().pipe(
      map(heroes => {
        return heroes.map(hero => this.mapHero(hero));
      })
    );
  }

  getById(id: string | number): Observable<Hero> {
    return super.getById(id).pipe(map(hero => this.mapHero(hero)));
  }

  getWithQuery(params: string | QueryParams): Observable<Hero[]> {
    return super.getWithQuery(params).pipe(
      map(heroes => {
        return heroes.map(hero => this.mapHero(hero));
      })
    );
  }

  private mapHero(hero: Hero): Hero {
    hero.dateLoaded = new Date();
    return hero;
  }
}

Using this technique let's us leverage all the base functionality of DefaultDataService<T>, and only override what we really need. In this example we just want to hook into the various get operations to perform our map on the entities. You could choose not to extend anything and write your own complete implementation too. It all depends on the needs of your application.

Finally, we need to tell ngrx-data about this new provider, and we can use the registerService() method on the EntityDataService provider in our store module:

import { NgModule } from '@angular/core';
import {
  EntityMetadataMap,
  NgrxDataModule,
  EntityDataService
} from 'ngrx-data';
import { HeroEntityDataService } from './hero-entity-data-service.service';

export const entityMetadata: EntityMetadataMap = {
  Hero: {},
  Villain: {}
};

// because the plural of "hero" is not "heros"
export const pluralNames = { Hero: 'Heroes' };

@NgModule({
  imports: [
    NgrxDataModule.forRoot({
      entityMetadata: entityMetadata,
      pluralNames: pluralNames
    })
  ],
  providers: [HeroEntityDataService]
})
export class EntityStoreModule {
  constructor(
    heroEntityDataService: HeroEntityDataService,
    entityDataService: EntityDataService
  ) {
    entityDataService.registerService('Hero', heroEntityDataService);
  }
}