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
.
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.
-
register a single data service by entity name with the
registerService()
method. -
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 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
.
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.
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);
}
}