diff --git a/src/app/hero-in-mem-data-override-id.service.ts b/src/app/hero-in-mem-data-override-id.service.ts new file mode 100644 index 0000000..88cfd95 --- /dev/null +++ b/src/app/hero-in-mem-data-override-id.service.ts @@ -0,0 +1,73 @@ +/** + * This is an example of a Hero-oriented InMemoryDbService. + * + * For demonstration purposes, it can return the database + * synchronously as an object (default), + * as an observable, or as a promise. + * + * Add the following line to `AppModule.imports` + * InMemoryWebApiModule.forRoot(HeroInMemDataService) // or HeroInMemDataOverrideService + */ +import { Injectable } from '@angular/core'; +import { InMemoryDbService, RequestInfo } from '../in-mem/interfaces'; + +// tslint:disable:no-unused-variable +import { Observable } from 'rxjs/Observable'; +import { of } from 'rxjs/observable/of'; +import 'rxjs/add/operator/delay'; +// tslint:enable:no-unused-variable + +@Injectable() +export class HeroInMemDataOverrideIdService implements InMemoryDbService { + defineId(collectionName: string) { + return 'uuid'; + } + createDb(reqInfo?: RequestInfo) { + + const heroes = [ + { uuid: '00000000-0000-0000-0000-000000000001', name: 'Windstorm' }, + { uuid: '00000000-0000-0000-0000-000000000002', name: 'Bombasto' }, + { uuid: '00000000-0000-0000-0000-000000000003', name: 'Magneta' }, + { uuid: '00000000-0000-0000-0000-000000000004', name: 'Tornado' } + ]; + + const nobodies: any[] = []; + + // entities with string ids that look like numbers + const stringers = [ + { uuid: '00000000-0000-0000-0000-000000000010', name: 'Bob String' }, + { uuid: '00000000-0000-0000-0000-000000000020', name: 'Jill String' } + ]; + + // default returnType + let returnType = 'object'; + // let returnType = 'observable'; + // let returnType = 'promise'; + + // demonstrate POST commands/resetDb + // this example clears the collections if the request body tells it to do so + if (reqInfo) { + const body = reqInfo.utils.getJsonBody(reqInfo.req) || {}; + if (body.clear === true) { + heroes.length = 0; + nobodies.length = 0; + stringers.length = 0; + } + + // 'returnType` can be 'object' | 'observable' | 'promise' + returnType = body.returnType || 'object'; + } + const db = { heroes, nobodies, stringers }; + + switch (returnType) { + case ('observable'): + return of(db).delay(10); + case ('promise'): + return new Promise(resolve => { + setTimeout(() => resolve(db), 10); + }); + default: + return db; + } + } +} diff --git a/src/in-mem/backend.service.ts b/src/in-mem/backend.service.ts index 2d24a50..1dc2094 100644 --- a/src/in-mem/backend.service.ts +++ b/src/in-mem/backend.service.ts @@ -28,6 +28,10 @@ import { UriInfo } from './interfaces'; +export interface IDictionary { + [index: string]: string; +} + /** * Base class for in-memory web api back-ends * Simulate the behavior of a RESTy web api @@ -36,6 +40,7 @@ import { * http://www.restapitutorial.com/lessons/httpmethods.html */ export abstract class BackendService { + protected fieldIds = {} as IDictionary; protected config: InMemoryBackendConfigArgs = new InMemoryBackendConfig(); protected db: Object; protected dbReadySubject: BehaviorSubject; @@ -381,7 +386,48 @@ export abstract class BackendService { * @param id */ protected findById(collection: T[], id: any): T { - return collection.find((item: T) => item.id === id); + return collection.find((item: T) => this.getItemId(item) === id); + } + + /** + * define for your in-mem database what is the name of your id field + * return the name of the id field + */ + protected defineId(collectionName = 'default_id'): string { + const defineId = this.bind('defineId'); + if (defineId) { + const id = defineId(collectionName); + if ((id !== undefined) && id !== '') { return id; } + } + return 'id'; + } + + /** + * return the key (by id property name) of item for a collection name + * @param item + * @param collectionName + */ + protected getItemId(item: T, collectionName = 'default_id'): any { + return item[this.fieldIds[collectionName]]; + } + + /** + * set the key by property name of a item for a collection + * @param item + * @param id + * @param collectionName + */ + protected setItemId(item: T, id: any, collectionName = 'default_id'): any { + return item[this.fieldIds[collectionName]] = id; + } + + /** + * set the property name of the key of a collection + * @param id + * @param collectionName + */ + protected setFieldId(id: any, collectionName = 'default_id') { + this.fieldIds[collectionName] = this.defineId(collectionName); } /** @@ -414,7 +460,7 @@ export abstract class BackendService { let maxId = 0; collection.reduce((prev: any, item: any) => { - maxId = Math.max(maxId, typeof item.id === 'number' ? item.id : maxId); + maxId = Math.max(maxId, typeof this.getItemId(item) === 'number' ? this.getItemId(item) : maxId); }, undefined); return maxId + 1; } @@ -493,7 +539,7 @@ export abstract class BackendService { protected abstract getRequestMethod(req: any): string; protected indexOf(collection: any[], id: number) { - return collection.findIndex((item: any) => item.id === id); + return collection.findIndex((item: any) => this.getItemId(item) === id); } /** Parse the id as a number. Return original value if not a number. */ @@ -588,9 +634,9 @@ export abstract class BackendService { const item = this.getJsonBody(req); // tslint:disable-next-line:triple-equals - if (item.id == undefined) { + if (this.getItemId(item) == undefined) { try { - item.id = id || this.genId(collection, collectionName); + this.setItemId(item, id || this.genId(collection, collectionName)); } catch (err) { const emsg: string = err.message || ''; if (/id type is non-numeric/.test(emsg)) { @@ -603,10 +649,10 @@ export abstract class BackendService { } } - if (id && id !== item.id) { + if (id && id !== this.getItemId(item)) { return this.createErrorResponseOptions(url, STATUS.BAD_REQUEST, `Request id does not match item.id`); } else { - id = item.id; + id = this.getItemId(item); } const existingIx = this.indexOf(collection, id); const body = this.bodify(item); @@ -621,8 +667,8 @@ export abstract class BackendService { } else { collection[existingIx] = item; return this.config.post204 ? - { headers, status: STATUS.NO_CONTENT } : // successful; no content - { headers, body, status: STATUS.OK }; // successful; return entity + { headers, status: STATUS.NO_CONTENT } : // successful; no content + { headers, body, status: STATUS.OK }; // successful; return entity } } @@ -631,14 +677,14 @@ export abstract class BackendService { protected put({ collection, collectionName, headers, id, req, url }: RequestInfo): ResponseOptions { const item = this.getJsonBody(req); // tslint:disable-next-line:triple-equals - if (item.id == undefined) { + if (this.getItemId(item) == undefined) { return this.createErrorResponseOptions(url, STATUS.NOT_FOUND, `Missing '${collectionName}' id`); } - if (id && id !== item.id) { + if (id && id !== this.getItemId(item)) { return this.createErrorResponseOptions(url, STATUS.BAD_REQUEST, `Request for '${collectionName}' id does not match item.id`); } else { - id = item.id; + id = this.getItemId(item); } const existingIx = this.indexOf(collection, id); const body = this.bodify(item); @@ -682,6 +728,9 @@ export abstract class BackendService { this.db = d; this.dbReadySubject.next(true); }); + + this.setFieldId(this.defineId()); + return this.dbReady; } diff --git a/src/in-mem/http-backend.service.spec.ts b/src/in-mem/http-backend.service.spec.ts index b07e740..473c870 100644 --- a/src/in-mem/http-backend.service.spec.ts +++ b/src/in-mem/http-backend.service.spec.ts @@ -18,6 +18,7 @@ import { HttpHeroService } from '../app/http-hero.service'; import { HeroInMemDataService } from '../app/hero-in-mem-data.service'; import { HeroInMemDataOverrideService } from '../app/hero-in-mem-data-override.service'; +import { HeroInMemDataOverrideIdService } from '../app/hero-in-mem-data-override-id.service'; import { HeroServiceCoreSpec } from '../app/hero.service.spec'; class Nobody { id: string; name: string; } @@ -392,6 +393,47 @@ describe('Http Backend Service', () => { }; }); + //////////////// + describe('raw Angular Http w/ override id service', () => { + + let http: Http; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpModule, + HttpInMemoryWebApiModule.forRoot(HeroInMemDataOverrideIdService, { delay }) + ] + }); + + http = TestBed.get(Http); + }); + + it('can get heroes', async(() => { + http.get('api/heroes') + .map(res => res.json() as Hero[]) + .subscribe( + heroes => { + // console.log(heroes); + expect(heroes.length).toBeGreaterThan(0, 'should have heroes'); + }, + failure + ); + })); + + it('can get heroes by uuid', async(() => { + http.get('api/heroes/00000000-0000-0000-0000-000000000002') + .map(res => res.json() as Hero) + .subscribe( + hero => { + // console.log(heroes); + expect(hero.name).toEqual('Bombasto', 'should find hero Bombasto'); + }, + failure + ); + })); + }); + //////////////// describe('Http HeroService', () => {