Skip to content

Commit

Permalink
feat(database): Implement hooks to get database load status so models…
Browse files Browse the repository at this point in the history
… can be seeded.

 feat(exceptions): Implement exceptions with docs & tests.
 feat(schema hydration): Implement @StoredProperty decorator for migrating all properties
  • Loading branch information
zakhenry committed Jun 20, 2016
1 parent 89269f1 commit 630f98c
Show file tree
Hide file tree
Showing 16 changed files with 549 additions and 82 deletions.
2 changes: 1 addition & 1 deletion docs/guide/controllers.md
@@ -1,6 +1,6 @@
---
title: Controllers
description: Take requests and shove them where they need to go. Throw the results somewhere.
description: Take requests and shove them where they need to go. Send the results somewhere
date: 2016-06-01
collection: guide
collectionSort: 1
Expand Down
67 changes: 67 additions & 0 deletions docs/guide/exceptions.md
@@ -0,0 +1,67 @@
---
title: Exceptions
description: Something gone terribly wrong? Thow an exception
date: 2016-06-20
collection: guide
collectionSort: 1
layout: guide.hbs
---

Ubiquits provides a number of built-in `HttpExceptions` that should be thrown rather than `Error` when an unexpected
event occurs. This allows the call stack handler to inspect the exception status code, and return that to the client so
it can handle the error in a meaningful way.

The exceptions that are available are:

* BadRequestException (code 400)
* UnauthorizedException (code 401)
* PaymentRequiredException (code 402)
* ForbiddenException (code 403)
* NotFoundException (code 404)
* MethodNotAllowedException (code 405)
* NotAcceptableException (code 406)
* ProxyAuthenticationRequiredException (code 407)
* RequestTimeoutException (code 408)
* ConflictException (code 409)
* GoneException (code 410)
* LengthRequiredException (code 411)
* PreconditionFailedException (code 412)
* PayloadTooLargeException (code 413)
* URITooLongException (code 414)
* UnsupportedMediaTypeException (code 415)
* RangeNotSatisfiableException (code 416)
* ExpectationFailedException (code 417)
* UnprocessableEntityException (code 422)
* TooManyRequestsException (code 429)
* UnavailableForLegalReasonsException (code 451)
* InternalServerErrorException (code 500)
* NotImplementedException (code 501)
* ServiceUnavailableException (code 503)
* InsufficientStorageException (code 507)


## Example

In a database store, the NotFoundException is thrown when the record is not present in the database:

```typescript
public findOne(id: identifier): Promise<T> {
return this.orm.findByPrimary(<number|string>id)
.then((modelData: Instance<any>): T => {
if (!modelData){
throw new NotFoundException(`Model not found for id [${id}]`);
}
return new this.modelStatic(modelData.get());
});
}
```

The exception is eventually caught by the stack handler, and will return the following response:

```
HTTP/1.1 404 Not Found
{
"message": "Model not found for id [72eed629-c4ab-4520-a987-4ea26b134d8c]"
}
```
8 changes: 6 additions & 2 deletions docs/index.md
Expand Up @@ -17,13 +17,17 @@ description: Full stack isomorphic typescript framework.
- [ ] 100% Code coverage
- [ ] on initialization generate passwords and certificate for auth
- [ ] Integrate code generations from Angular CLI
- [ ] Migration to TypeORM
- [ ] Docker integration into toolchain for local dev

### Developer Preview [June 17th]
### Developer Preview [June 20th]
- [x] Model hydration and mocking
- [x] `ResourceController` implementation with CRUD routes
- [x] Custom middleware registration
- [x] Demo model schema sync and seed
- [x] Http exceptions
- [ ] Post-initialization cli tour
- [ ] Full stack demo in quickstart
- [x] Full stack demo in quickstart
- [x] FAQ page in docs
- [x] Documented contribution guidelines

Expand Down
3 changes: 2 additions & 1 deletion src/common/models/collection.spec.ts
@@ -1,6 +1,7 @@
import { it, describe, beforeEach, expect } from '@angular/core/testing';
import { Model, primary } from './model';
import { Model } from './model';
import { Collection } from './collection';
import { primary } from '../types/primary.decorator';

class BasicModel extends Model {

Expand Down
3 changes: 2 additions & 1 deletion src/common/models/model.spec.ts
@@ -1,10 +1,11 @@
import { it, describe, expect, beforeEach } from '@angular/core/testing';
import { UUID, Model, primary } from './model';
import { UUID, Model } from './model';
import { castDate } from '../types/date.decorator';
import * as moment from 'moment';
import { Collection } from './collection';
import { hasOne, hasMany } from '../relations';
import Moment = moment.Moment;
import { primary } from '../types/primary.decorator';

class ChildModel extends Model {

Expand Down
12 changes: 3 additions & 9 deletions src/common/models/model.ts
@@ -1,4 +1,5 @@
import { Collection } from './collection';
import { DataTypeAbstract } from 'sequelize';

export interface EntityNest extends Map<string, Model|Collection<Model>> {

Expand All @@ -24,6 +25,7 @@ export interface ModelStatic<T extends Model> {
identifierKey: string;
schema: ModelSchema;
modelName: string;
storedProperties: Map<string, string>;
}

export interface TypeCaster {
Expand All @@ -39,6 +41,7 @@ export abstract class Model {

public static identifierKey: string;
public static schema: ModelSchema = {};
public static storedProperties: Map<string, string>;
public static modelName: string;

/**
Expand All @@ -50,8 +53,6 @@ export abstract class Model {
/**
* References maintained from initial hydration
*/
protected __rawData: Object;
protected __original: Object;

constructor(data?: any) {
this.hydrate(data);
Expand All @@ -64,8 +65,6 @@ export abstract class Model {
*/
protected hydrate(data: Object) {

this.__rawData = Object.assign({}, data);

if (this.__typeCasts) {
for (const [key, caster] of this.__typeCasts) {
if (data.hasOwnProperty(key)) {
Expand All @@ -82,8 +81,6 @@ export abstract class Model {
}
}

this.__original = Object.assign({}, data);

Object.assign(this, data);
return this;
}
Expand All @@ -95,7 +92,4 @@ export abstract class Model {

}

export function primary(target: any, propertyKey: string) {
target.constructor.identifierKey = propertyKey;
}

2 changes: 2 additions & 0 deletions src/common/types/index.ts
@@ -1 +1,3 @@
export * from './date.decorator';
export * from './primary.decorator';
export * from './storedProperty.decorator';
10 changes: 10 additions & 0 deletions src/common/types/primary.decorator.ts
@@ -0,0 +1,10 @@

export function primary(target: any, propertyKey: string) {
if (!target.constructor.storedProperties) {
target.constructor.storedProperties = new Map();
}
let type = Reflect.getMetadata("design:type", target, propertyKey);
target.constructor.storedProperties.set(propertyKey, type);

target.constructor.identifierKey = propertyKey;
}
8 changes: 8 additions & 0 deletions src/common/types/storedProperty.decorator.ts
@@ -0,0 +1,8 @@
export function StoredProperty(target: any, propertyKey: string): void {

if (!target.constructor.storedProperties) {
target.constructor.storedProperties = new Map();
}
let type = Reflect.getMetadata("design:type", target, propertyKey);
target.constructor.storedProperties.set(propertyKey, type);
}
33 changes: 24 additions & 9 deletions src/server/controllers/abstract.controller.ts
Expand Up @@ -6,8 +6,9 @@ import { PromiseFactory } from '../../common/util/serialPromise';
import { Response } from './response';
import { Request } from './request';
import { initializeMiddlewareRegister } from '../middleware/middleware.decorator';
import { HttpException, InternalServerErrorException } from '../exeptions/exceptions';

export const httpMethods:HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
export const httpMethods: HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];

export interface MethodDefinition {
method: HttpMethod;
Expand Down Expand Up @@ -81,7 +82,7 @@ export abstract class AbstractController {

initializeMiddlewareRegister(this);

if (methodSignature){
if (methodSignature) {
let current: MiddlewareRegistry = this.registeredMiddleware.methods.get(methodSignature);

if (!current) {
Expand Down Expand Up @@ -113,12 +114,13 @@ export abstract class AbstractController {

callStack.push(this[methodSignature]);

if (this.registeredMiddleware){
if (this.registeredMiddleware) {
const methodMiddlewareFactories = this.registeredMiddleware.methods.get(methodSignature);

//wrap method registered factories with the class defined ones [beforeAll, before, after, afterAll]
//wrap method registered factories with the class defined ones [beforeAll, before, after,
// afterAll]
const beforeMiddleware = this.registeredMiddleware.all.before.concat(methodMiddlewareFactories.before);
const afterMiddleware = methodMiddlewareFactories.after.concat(this.registeredMiddleware.all.after);
const afterMiddleware = methodMiddlewareFactories.after.concat(this.registeredMiddleware.all.after);

if (methodMiddlewareFactories) {
callStack.unshift(...beforeMiddleware.map((middleware: MiddlewareFactory) => middleware(this.injector)));
Expand All @@ -134,11 +136,24 @@ export abstract class AbstractController {
callStackHandler: (request: Request, response: Response): Promise<Response> => {
return callStack.reduce((current: Promise<Response>, next: PromiseFactory<Response>): Promise<Response> => {

return current.then((response: Response): Promise<Response> => {
return Promise.resolve(next.call(this, request, response));
});
return current.then((response: Response): Promise<Response> => {
return Promise.resolve(next.call(this, request, response));
});

}, Promise.resolve(response)) //initial value
.catch((e) => {

}, Promise.resolve(response)); //initial value
this.logger.debug('Error encountered', e);

if (!(e instanceof HttpException)) {
e = new InternalServerErrorException(e.message);
}
response.status(e.getStatusCode());
response.data({
message: e.toString()
});
return response;
});
}
});

Expand Down
78 changes: 78 additions & 0 deletions src/server/exeptions/exceptions.spec.ts
@@ -0,0 +1,78 @@
import { it, expect, describe } from '@angular/core/testing';
import {
InsufficientStorageException,
ServiceUnavailableException,
NotImplementedException,
InternalServerErrorException,
UnavailableForLegalReasonsException,
TooManyRequestsException,
UnprocessableEntityException,
ExpectationFailedException,
RangeNotSatisfiableException,
UnsupportedMediaTypeException,
URITooLongException,
PayloadTooLargeException,
PreconditionFailedException,
LengthRequiredException,
GoneException,
ConflictException,
RequestTimeoutException,
ProxyAuthenticationRequiredException,
NotAcceptableException,
MethodNotAllowedException,
NotFoundException,
ForbiddenException,
PaymentRequiredException,
UnauthorizedException,
BadRequestException,
HttpException
} from './exceptions';

describe('Exceptions', () => {

const exceptions = [
{exception: BadRequestException, code: 400},
{exception: UnauthorizedException, code: 401},
{exception: PaymentRequiredException, code: 402},
{exception: ForbiddenException, code: 403},
{exception: NotFoundException, code: 404},
{exception: MethodNotAllowedException, code: 405},
{exception: NotAcceptableException, code: 406},
{exception: ProxyAuthenticationRequiredException, code: 407},
{exception: RequestTimeoutException, code: 408},
{exception: ConflictException, code: 409},
{exception: GoneException, code: 410},
{exception: LengthRequiredException, code: 411},
{exception: PreconditionFailedException, code: 412},
{exception: PayloadTooLargeException, code: 413},
{exception: URITooLongException, code: 414},
{exception: UnsupportedMediaTypeException, code: 415},
{exception: RangeNotSatisfiableException, code: 416},
{exception: ExpectationFailedException, code: 417},
{exception: UnprocessableEntityException, code: 422},
{exception: TooManyRequestsException, code: 429},
{exception: UnavailableForLegalReasonsException, code: 451},
{exception: InternalServerErrorException, code: 500},
{exception: NotImplementedException, code: 501},
{exception: ServiceUnavailableException, code: 503},
{exception: InsufficientStorageException, code: 507},
];

exceptions.forEach((check) => {

it(`creates instance of ${check.exception.prototype.constructor.name} with status code ${check.code}`, () => {

const exceptionInstance = new check.exception;

expect(exceptionInstance instanceof Error).toBe(true);
expect(exceptionInstance instanceof HttpException).toBe(true);
expect(exceptionInstance.getStatusCode()).toEqual(check.code);
expect(exceptionInstance.name).toEqual(exceptionInstance.constructor.name);

});

});



});

0 comments on commit 630f98c

Please sign in to comment.