Skip to content
This repository was archived by the owner on Feb 26, 2024. It is now read-only.

add feature to choose the property name of the primary key of collection #159

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/app/hero-in-mem-data-override-id.service.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
73 changes: 61 additions & 12 deletions src/in-mem/backend.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<boolean>;
Expand Down Expand Up @@ -381,7 +386,48 @@ export abstract class BackendService {
* @param id
*/
protected findById<T extends { id: any }>(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<T extends { id: any }>(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<T extends { id: any }>(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);
}

/**
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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)) {
Expand All @@ -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);
Expand All @@ -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
}
}

Expand All @@ -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);
Expand Down Expand Up @@ -682,6 +728,9 @@ export abstract class BackendService {
this.db = d;
this.dbReadySubject.next(true);
});

this.setFieldId(this.defineId());

return this.dbReady;
}

Expand Down
42 changes: 42 additions & 0 deletions src/in-mem/http-backend.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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', () => {
Expand Down