Skip to content
This repository has been archived by the owner on May 8, 2020. It is now read-only.

Commit

Permalink
Merge pull request #120 from ubiquits/feature/rest-methods
Browse files Browse the repository at this point in the history
feat(controllers): Add patchOne method, implement all supporting meth…
  • Loading branch information
zakhenry committed Jul 18, 2016
2 parents 3a25e62 + 054ca52 commit de8711d
Show file tree
Hide file tree
Showing 12 changed files with 315 additions and 42 deletions.
10 changes: 10 additions & 0 deletions docs/guide/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ deleteOne(request: Request, routeParams: RouteParamMap):Response;
@Route('DELETE', '/')
deleteMany(request: Request, routeParams: RouteParamMap):Response;
```
### `patchOne`
```typescript
@Route('PATCH', '/:id')
patchOne(request: Request, routeParams: RouteParamMap):Response;
```
### `patchMany` *(planned)*
```typescript
@Route('PATCH', '/')
patchMany(request: Request, routeParams: RouteParamMap):Response;
```

You may note the absence of `POST` methods. This is intentional, as a well designed REST api that has distributed primary
key generation (UUIDs) should never require a method that generates the id server-side. This is a core tenet of an
Expand Down
34 changes: 34 additions & 0 deletions src/browser/stores/http.store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,40 @@ describe('Http store', () => {

})));

it('Checks if a single model exists with http', async(inject([TestHttpStore, MockBackend], (s: TestHttpStore, b: MockBackend) => {

let connection: MockConnection;
b.connections.subscribe((c: MockConnection) => connection = c);

const model = new TestModel({id: 321});

const testPromise = s.hasOne(model)
.then((res) => {

expect(res)
.toBe(true);

// make next request for failure, using 404 not found this time
const promise = s.hasOne(model);

connection.mockRespond(new Response(new ResponseOptions({
status: 404,
})));

return promise;

}).then((res) => {
expect(res).toBe(false);
});

connection.mockRespond(new Response(new ResponseOptions({
status: 204,
})));

return testPromise;

})));

it('Deletes a single model with http', async(inject([TestHttpStore, MockBackend], (s: TestHttpStore, b: MockBackend) => {

let connection: MockConnection;
Expand Down
24 changes: 14 additions & 10 deletions src/browser/stores/http.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ export abstract class HttpStore<T extends AbstractModel> extends AbstractStore<T
};

/**
* Retrieve one model from the REST api
* @param id
* @returns {IPromise<void>|Promise<T>}
* @inheritdoc
*/
public findOne(id: identifier): Promise<T> {

Expand All @@ -55,9 +53,7 @@ export abstract class HttpStore<T extends AbstractModel> extends AbstractStore<T
}

/**
* Find many models
* @param query
* @returns {IPromise<void>|Promise<T>}
* @inheritdoc
*/
public findMany(query?:any):Promise<Collection<T>> {
return this.http.get(this.endpoint())
Expand All @@ -68,9 +64,7 @@ export abstract class HttpStore<T extends AbstractModel> extends AbstractStore<T
}

/**
* Save a model
* @param model
* @returns {Promise<void>|IPromise<void>|Promise<T>}
* @inheritdoc
*/
public saveOne(model:T):Promise<T> {
//@todo consider toJson method if custom serializing is needed?
Expand All @@ -85,7 +79,6 @@ export abstract class HttpStore<T extends AbstractModel> extends AbstractStore<T

/**
* @inheritdoc
* @param model
*/
public deleteOne(model: T): Promise<T> {

Expand All @@ -95,6 +88,17 @@ export abstract class HttpStore<T extends AbstractModel> extends AbstractStore<T
.then(() => model); //@todo flag model as existing
}

/**
* @inheritdoc
*/
public hasOne(model: T): Promise<boolean> {
return this.http.head(this.endpoint(model.getIdentifier()))
.toPromise()
.then((res: Response) => this.checkStatus(res))
.then(() => true)
.catch(() => false)
}

/**
* Extract model from the payload
* @param res
Expand Down
9 changes: 9 additions & 0 deletions src/common/models/collection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,13 @@ describe('Collection', () => {
.toThrowError(`Item with id [3] not in collection`);
});

it('can check if an entity is present in the collection', () => {

expect(collection.contains(data[1]))
.toBe(true);
expect(collection.contains(new BasicModel({id: 10})))
.toBe(false);

});

});
27 changes: 25 additions & 2 deletions src/common/models/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
/** End Typedoc Module Declaration */
import { AbstractModel, identifier } from './model';

import * as _ from 'lodash';
/**
* Collection holds an array of [[AbstractModel|models]]. It provides common collection manipulation
* methods for the controllers, services etc to work with models in an abstracted manner
Expand All @@ -20,7 +20,7 @@ export class Collection<T extends AbstractModel> extends Array<T> {
* @param id
* @returns {T}
*/
public findById(id: identifier): AbstractModel {
public findById(id: identifier): T {

const found = this.find((model) => model.getIdentifier() === id);

Expand All @@ -31,4 +31,27 @@ export class Collection<T extends AbstractModel> extends Array<T> {
return found;
}

/**
* Remove an item from the collection
* @param model
*/
public remove(model:T):void {
_.pull(this, model);
}

/**
* Check if the collection contains a given model
* @param model
* @returns {boolean}
*/
public contains(model: T): boolean {

try {
this.findById(model.getIdentifier());
return true;
} catch (e) {
}
return false;
}

}
42 changes: 32 additions & 10 deletions src/common/stores/mock.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,20 @@ export abstract class MockStore<T extends AbstractModel> extends AbstractStore<T
*/
protected chanceInstance: ChanceInstance;

constructor(modelStatic: ModelStatic<T>, injector:Injector) {
protected modelCollection: Collection<T>;

constructor(modelStatic: ModelStatic<T>, injector: Injector) {
super(modelStatic, injector);

this.initializeMockCollection();
}

/**
* Start the mock store off with some dummy data
*/
protected initializeMockCollection(): void {
const models = _.times(10, () => this.getMock());
this.modelCollection = new Collection(models);
}

/**
Expand All @@ -44,23 +56,24 @@ export abstract class MockStore<T extends AbstractModel> extends AbstractStore<T
* Get an instance of the model
* @param id
*/
protected abstract getMock(id?:identifier):T;
protected abstract getMock(id?: identifier): T;

/**
* @inheritdoc
*/
public findOne(id?: identifier): Promise<T> {
return Promise.resolve(this.getMock(id));
try {
return Promise.resolve(this.modelCollection.findById(id));
} catch (e){
return this.saveOne(this.getMock(id))
}
}

/**
* @inheritdoc
*/
public findMany(query?:any): Promise<Collection<T>> {

const models = _.times(10, () => this.getMock());

return Promise.resolve(new Collection(models));
public findMany(query?: any): Promise<Collection<T>> {
return Promise.resolve(this.modelCollection);
}

/**
Expand All @@ -69,20 +82,29 @@ export abstract class MockStore<T extends AbstractModel> extends AbstractStore<T
* As saving does not make sense for a mock store, this just stubs the interface by returning
* the model in a resolved promise
*/
public saveOne(model:T): Promise<T> {
public saveOne(model: T): Promise<T> {
this.modelCollection.push(model);
return Promise.resolve(model);
}

/**
* Mock seleting model by id
* Mock selecting model by id
*
* As deleting does not make sense for a mock store, this just stubs the interface by returning
* the model in a resolved promise
* @param model
* @returns {Promise<void>}
*/
public deleteOne(model: T): Promise<T> {
this.modelCollection.remove(model);
return Promise.resolve(model);
}

/**
* @inheritdoc
*/
public hasOne(model: T): Promise<boolean> {
return Promise.resolve(this.modelCollection.contains(model));
}

}
10 changes: 10 additions & 0 deletions src/common/stores/store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { ValidationException } from '../../server/exeptions/exceptions';
import { Primary } from '../models/types/primary.decorator';
import Spy = jasmine.Spy;
import { Collection } from '../models/collection';

@Injectable()
class StubService {
Expand Down Expand Up @@ -187,6 +188,10 @@ describe('Mock Store', () => {
.then((ship) => {
expect(shipRef)
.toEqual(ship);
return c.shipStore.findMany();
})
.then((allModels:Collection<Ship>) => {
expect(allModels.findById(1234)).toEqual(shipRef);
});

})));
Expand All @@ -203,6 +208,11 @@ describe('Mock Store', () => {
.then((ship: Ship) => {
expect(ship instanceof Ship)
.toBe(true);

return c.shipStore.findMany();
})
.then((allModels:Collection<Ship>) => {
expect(() => allModels.findById(1234)).toThrowError('Item with id [1234] not in collection');
});

})));
Expand Down
8 changes: 7 additions & 1 deletion src/common/stores/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export abstract class AbstractStore<T extends AbstractModel> {
* class-validator Validator instance
* @see https://github.com/pleerock/class-validator
*/
protected validator:Validator;
protected validator: Validator;

constructor(protected modelStatic: ModelStatic<T>, protected injector: Injector) {

Expand Down Expand Up @@ -53,6 +53,12 @@ export abstract class AbstractStore<T extends AbstractModel> {
*/
public abstract saveOne(model: T): Promise<T>;

/**
* Check if a model exists in the database
* @param model
*/
public abstract hasOne(model: T): Promise<boolean>;

/**
* Delete the model from the store.
* @param model
Expand Down
Loading

0 comments on commit de8711d

Please sign in to comment.