diff --git a/.gitignore b/.gitignore
index b651894..a885083 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,5 @@ typings/
!/.config-sample/config.js
.idea/workspace.xml
.idea/dictionaries/
-npm-debug.log
\ No newline at end of file
+npm-debug.log
+coverage/
\ No newline at end of file
diff --git a/.idea/runConfigurations/Mocha.xml b/.idea/runConfigurations/Mocha.xml
new file mode 100644
index 0000000..7d1d43e
--- /dev/null
+++ b/.idea/runConfigurations/Mocha.xml
@@ -0,0 +1,16 @@
+
+
+ project
+
+ $PROJECT_DIR$
+ true
+
+
+
+ bdd
+ -r reflect-metadata -r ./mochaInit
+ PATTERN
+ ./{,!(node_modules)/**/}/*.spec.js
+
+
+
\ No newline at end of file
diff --git a/.idea/ts-orm.iml b/.idea/ts-orm.iml
index 6106421..684f57e 100644
--- a/.idea/ts-orm.iml
+++ b/.idea/ts-orm.iml
@@ -1,7 +1,11 @@
-
+
+
+
+
+
diff --git a/.istanbul.yml b/.istanbul.yml
new file mode 100644
index 0000000..839a515
--- /dev/null
+++ b/.istanbul.yml
@@ -0,0 +1,23 @@
+instrumentation:
+ excludes: ['*.spec.js', '*.spec.class.js']
+ include-all-sources: true
+reporting:
+ watermarks:
+ statements: [80, 100]
+ lines: [80, 100]
+ functions: [80, 100]
+ branches: [80, 100]
+check:
+ each:
+ statements: -3
+ lines: -1
+ branches: -13
+ functions: -1
+ # Theres a bug in istanbuhl around the check-coverage command and sourcemaps,
+ # replace the above with this when it's fixed
+ #global:
+ #statements: 100
+ #lines: 100
+ #branches: 100
+ #functions: 100
+
diff --git a/.npmignore b/.npmignore
index 3369955..bf8294f 100644
--- a/.npmignore
+++ b/.npmignore
@@ -3,4 +3,14 @@ typings/
*.ts
!*.d.ts
.idea/
-npm-debug.log
\ No newline at end of file
+npm-debug.log
+coverage/
+mocks/
+*.spec.d.ts
+*.spec.js
+*.spec.js.map
+*.spec.class.d.ts
+*.spec.class.js
+*.spec.class.js.map
+*.sh
+.istanbul.yml
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index f48d21d..42bc430 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,7 @@
language: node_js
node_js:
- '5.10'
+before_install: export npm_config_loglevel=silent
before_deploy: npm version --no-git-tag-version `git -c core.longpaths=true describe --abbrev=0 --tags`
deploy:
- provider: npm
@@ -9,3 +10,7 @@ deploy:
on:
tags: true
email: "waterfoul@gmail.com"
+cache:
+ directories:
+ - node_modules
+ - typings
\ No newline at end of file
diff --git a/README.md b/README.md
index 04b5161..c1ac420 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,9 @@ js dependancy injection. It can also be used on the frontend with a little setup
and will talk via an api.
[![Build Status](https://travis-ci.org/tanjentjs/ts-orm.svg?branch=master)](https://travis-ci.org/tanjentjs/ts-orm)
+[![Coverage Status](https://coveralls.io/repos/github/tanjentjs/ts-orm/badge.svg?branch=master)](https://coveralls.io/github/tanjentjs/ts-orm?branch=master)
+[![NPM Downloads](https://img.shields.io/npm/dm/tanjentjs-ts-orm.svg)](https://www.npmjs.com/package/tanjentjs-ts-orm)
+[![Slack Status](https://tanjentjs-slack.herokuapp.com/badge.svg)](https://tanjentjs-slack.herokuapp.com/)
Installing
==========
@@ -26,12 +29,19 @@ Please submit all requests for features and bug requests via the github
Testing
=======
-TBD
+* Tests will be placed along side the file in question in a file labeled .spec.ts
+* Tests will be written using Mocha with the describe statement containing the name of the file including the directory
+ (ex. 'node/connect' for 'node/connect.ts')
+* If you need to create classes for testing (ex. testing an abstract class) place them in a .spec.class.ts
+* All public interfaces must be tested
+* In order to fix a bug you must write a test first, this is to avoid regressions
+* The test reporter should always show 100% coverage. If something doesn't make sense to test you can ignore it using special comments
Directory structure
=======
* shared - contains the files shared across all vendors
* node - contains the files for use via nodejs
+* mocks - contains mock objects for use in testing
Development
===========
diff --git a/angular2/DataConnection.spec.class.ts b/angular2/DataConnection.spec.class.ts
new file mode 100644
index 0000000..9d8064d
--- /dev/null
+++ b/angular2/DataConnection.spec.class.ts
@@ -0,0 +1,35 @@
+import {DataConnection} from './DataConnection';
+import {register} from '../shared/DataObject';
+import { Injector } from '@angular/core';
+import * as Contracts from './DataContract.spec.class';
+
+@register('test')
+export class NoProp extends DataConnection {
+ constructor(injector?: Injector) { super(injector); }
+ protected getContract() {
+ return Contracts.NoProp;
+ }
+}
+
+@register('test')
+export class NoInject extends DataConnection {
+ protected getContract() {
+ return Contracts.NoProp;
+ }
+}
+
+@register('test')
+export class StringProp extends DataConnection {
+ constructor(injector?: Injector) { super(injector); }
+ protected getContract() {
+ return Contracts.StringProp;
+ }
+}
+
+@register('test')
+export class DateProp extends DataConnection {
+ constructor(injector?: Injector) { super(injector); }
+ protected getContract() {
+ return Contracts.DateProp;
+ }
+}
diff --git a/angular2/DataConnection.spec.ts b/angular2/DataConnection.spec.ts
new file mode 100644
index 0000000..ebe5a6e
--- /dev/null
+++ b/angular2/DataConnection.spec.ts
@@ -0,0 +1,183 @@
+import {ReflectiveInjector, enableProdMode} from '@angular/core';
+import {HTTP_PROVIDERS, BaseRequestOptions, Http, Response, ResponseOptions} from '@angular/http';
+import {MockBackend, MockConnection} from '@angular/http/testing';
+import {AuthHandler} from './AuthHandler';
+
+import * as chai from 'chai';
+import * as moment from 'moment';
+
+import {NoProp, DateProp, StringProp, NoInject} from './DataConnection.spec.class';
+import {NoProp as NoPropContract, StringProp as StringPropContract} from './DataContract.spec.class';
+
+enableProdMode();
+
+describe('angular2/DataConnection', () => {
+
+ let injector: ReflectiveInjector;
+ let http: Http;
+ let backend: MockBackend;
+
+ beforeEach(() => {
+ injector = ReflectiveInjector.resolveAndCreate([
+ HTTP_PROVIDERS,
+ {
+ deps: [MockBackend, BaseRequestOptions],
+ provide: Http,
+ useFactory: (mbackend: MockBackend, defaultOptions: BaseRequestOptions) => {
+ return new Http(mbackend, defaultOptions);
+ }
+ },
+ NoProp,
+ StringProp,
+ DateProp,
+ NoInject,
+ AuthHandler,
+ MockBackend,
+ BaseRequestOptions
+ ]);
+ http = injector.get(Http);
+ backend = injector.get(MockBackend);
+ });
+
+ it('dies without the injector', () => {
+ chai.expect(() => injector.get(NoInject)).to.throw(Error);
+ });
+
+ describe('noProp', function () {
+ let noProp: NoProp;
+
+ beforeEach(() => {
+ noProp = injector.get(NoProp);
+ });
+
+ it('fetches data', () => {
+ backend.connections.subscribe((c: MockConnection) => {
+ chai.expect(c.request.url).to.equal('/object/test.NoProp/42');
+ c.mockRespond(new Response(new ResponseOptions({body: '{}'})));
+ });
+ return chai.expect(noProp.fetch(42)).to.eventually.be.an.instanceof(NoPropContract);
+ });
+
+ it('creates', () => {
+ return chai.expect(noProp.create()).be.an.instanceof(NoPropContract);
+ });
+
+ it('searches', () => {
+ backend.connections.subscribe((c: MockConnection) => {
+ chai.expect(c.request.url).to.equal('/object/test.NoProp');
+ chai.expect(c.request.getBody()).to.equal('{"stuff":"things"}');
+
+ c.mockRespond(new Response(new ResponseOptions({body: '[{}]'})));
+ });
+ return noProp.search({'stuff': 'things'}).then((result: any) => {
+ chai.expect(result).to.be.an('array');
+ chai.expect(result.length).to.equal(1);
+ chai.expect(result[0]).to.be.an.instanceof(NoPropContract);
+ });
+ });
+
+ it('deletes', () => {
+ let requestNum = 0;
+ backend.connections.subscribe((c: MockConnection) => {
+ chai.expect(c.request.url).to.equal('/object/test.NoProp/42');
+ if (requestNum === 0) {
+ chai.expect(c.request.method).to.equal(0); // GET
+ c.mockRespond(new Response(new ResponseOptions({body: '{"id":"42"}'})));
+ } else if (requestNum === 1) {
+ chai.expect(c.request.method).to.equal(3); // DELETE
+ c.mockRespond(new Response(new ResponseOptions({body: ''})));
+ }
+ requestNum++;
+ });
+ return chai.expect(noProp.fetch(42).then((r) => {
+ return r.delete();
+ })).to.eventually.be.fulfilled;
+ });
+ });
+
+ describe('stringProp', function () {
+ let stringProp: StringProp;
+
+ beforeEach(() => {
+ stringProp = injector.get(StringProp);
+ });
+
+ it('saves a new object', () => {
+ const cur: StringPropContract = stringProp.create();
+
+ cur.stringy = 'stuff';
+
+ backend.connections.subscribe((c: MockConnection) => {
+ chai.expect(c.request.url).to.equal('/object/test.StringProp');
+ chai.expect(c.request.method).to.equal(2); // PUT
+ chai.expect(c.request.getBody()).to.equal('{"stringy":"stuff"}');
+ c.mockRespond(new Response(new ResponseOptions({body: '{"id":"45"}'})));
+ });
+
+ return chai.expect(cur.save()).to.eventually.have.property('id', '45');
+ });
+
+ it('saves an existing object', () => {
+
+ let requestNum = 0;
+ backend.connections.subscribe((c: MockConnection) => {
+ chai.expect(c.request.url).to.equal('/object/test.StringProp/45');
+ if (requestNum === 0) {
+ chai.expect(c.request.method).to.equal(0); // GET
+ c.mockRespond(new Response(new ResponseOptions({body: '{"id":"45"}'})));
+ } else if (requestNum === 1) {
+ chai.expect(c.request.method).to.equal(2); // PUT
+ chai.expect(c.request.getBody()).to.equal('{"id":"45","stringy":"stuff"}');
+ c.mockRespond(new Response(new ResponseOptions({body: '{"id":"45"}'})));
+ }
+ requestNum++;
+ });
+ return stringProp.fetch(45).then((cur: StringPropContract) => {
+ cur.stringy = 'stuff';
+
+ return chai.expect(cur.save()).to.eventually.be.fulfilled;
+ });
+ });
+ });
+
+ describe('dateProp', function () {
+ let dateProp: DateProp;
+ const dateString = '2016-07-14T19:08:43.279Z';
+
+ beforeEach(() => {
+ dateProp = injector.get(DateProp);
+ });
+
+ it('serializes dates', function () {
+ const cur = dateProp.create();
+
+ cur.dateThing = moment(dateString);
+
+ backend.connections.subscribe((c: MockConnection) => {
+ chai.expect(c.request.url).to.equal('/object/test.DateProp');
+ chai.expect(c.request.method).to.equal(2); // PUT
+ chai.expect(c.request.getBody()).to.equal('{"dateThing":"' + dateString + '"}');
+ c.mockRespond(new Response(new ResponseOptions({body: '{"id":"41"}'})));
+ });
+
+ return chai.expect(cur.save()).to.eventually.have.property('id', '41');
+ });
+
+ it('deserializes dates', () => {
+ backend.connections.subscribe((c: MockConnection) => {
+ chai.expect(c.request.url).to.equal('/object/test.DateProp/41');
+ c.mockRespond(new Response(new ResponseOptions({body: '{"id":"41","dateThing":"' + dateString + '"}'})));
+ });
+ return dateProp.fetch(41).then((c) => {
+ chai.expect(c.dateThing.toISOString()).to.equal(dateString);
+ chai.expect(moment.isMoment(c.dateThing)).to.be.true;
+ });
+ });
+ });
+
+ afterEach(() => {
+ backend.resolveAllConnections();
+ backend.verifyNoPendingRequests();
+ });
+
+});
diff --git a/angular2/DataObject.ts b/angular2/DataConnection.ts
similarity index 50%
rename from angular2/DataObject.ts
rename to angular2/DataConnection.ts
index 6083cc3..7f6a579 100644
--- a/angular2/DataObject.ts
+++ b/angular2/DataConnection.ts
@@ -1,72 +1,10 @@
-import { IDataConnection, IDataContract } from '../shared/DataObject';
-import { Http, Headers, Response, RequestOptions } from '@angular/http';
+import { IDataConnection } from '../shared/DataObject';
+import { Http, Response } from '@angular/http';
import { Injector } from '@angular/core';
-import * as moment from 'moment';
import * as _ from 'lodash';
-import {Types} from '../shared/Types';
-
-import {field} from './field';
import {AuthHandler} from './AuthHandler';
-
-import 'rxjs/add/operator/map';
-import 'rxjs/add/operator/toPromise';
-import { Observable } from 'rxjs/Observable';
-
-export abstract class DataContract implements IDataContract {
- private get fields(): string[] {
- return Reflect.getMetadata('ORM:fields', this);
- }
-
- @field()
- public id: number;
- @field(Types.dateTimeTz)
- public createdAt: moment.Moment;
- @field(Types.dateTimeTz)
- public updatedAt: moment.Moment;
-
- constructor(
- private http: Http,
- private auth: AuthHandler,
- private baseUri: string,
- private data: any = {}
- ) {}
-
- public save(): Promise {
- // TODO: optimistic locking
- let request: Observable;
- if (this.id) {
- request = this.http.put(
- this.baseUri + '/' + this.id,
- JSON.stringify(this.data),
- this.auth.setOptions(getOptions())
- );
- } else {
- request = this.http.put(
- this.baseUri,
- JSON.stringify(this.data),
- this.auth.setOptions(getOptions())
- );
- }
- return request
- .map((res: Response) => this.auth.handleResponse(res))
- .map((res: Response) => {
- this.data = res.json();
- })
- .toPromise();
- }
-
- public delete(): Promise {
- return this.http
- .delete(
- this.baseUri + '/' + this.id,
- this.auth.setOptions(getOptions())
- )
- .map((res: Response) => this.auth.handleResponse(res))
- .toPromise()
- .then(() => { /* */ });
- }
-}
+import {DataContract, getOptions} from './DataContract';
export abstract class DataConnection implements IDataConnection {
private http: Http = this.injector && this.injector.get(Http);
@@ -96,7 +34,7 @@ export abstract class DataConnection implements IDataCon
this.http,
this.auth,
this.baseUri,
- body || { }
+ body
);
}).toPromise();
}
@@ -140,12 +78,3 @@ export abstract class DataConnection implements IDataCon
data?: any
) => T;
}
-
-function getOptions(): RequestOptions {
- const headers: Headers = new Headers();
- headers.set('accept', 'application/json');
-
- return new RequestOptions({
- headers: headers
- });
-}
diff --git a/angular2/DataContract.spec.class.ts b/angular2/DataContract.spec.class.ts
new file mode 100644
index 0000000..ae7cb45
--- /dev/null
+++ b/angular2/DataContract.spec.class.ts
@@ -0,0 +1,15 @@
+import {DataContract} from './DataContract';
+import {field} from './field';
+import {Types} from '../shared/Types';
+
+import * as moment from 'moment';
+
+export class NoProp extends DataContract {}
+export class StringProp extends DataContract {
+ @field()
+ public stringy: string;
+}
+export class DateProp extends DataContract {
+ @field(Types.dateTimeTz)
+ public dateThing: moment.Moment;
+}
diff --git a/angular2/DataContract.ts b/angular2/DataContract.ts
new file mode 100644
index 0000000..b73c488
--- /dev/null
+++ b/angular2/DataContract.ts
@@ -0,0 +1,73 @@
+import { IDataContract } from '../shared/DataObject';
+import { Http, Response, RequestOptions, Headers } from '@angular/http';
+import * as moment from 'moment';
+
+import {Types} from '../shared/Types';
+
+import {field} from './field';
+import {AuthHandler} from './AuthHandler';
+
+import 'rxjs/add/operator/map';
+import 'rxjs/add/operator/toPromise';
+import { Observable } from 'rxjs/Observable';
+
+export abstract class DataContract implements IDataContract {
+ @field()
+ public id: number;
+ @field(Types.dateTimeTz)
+ public createdAt: moment.Moment;
+ @field(Types.dateTimeTz)
+ public updatedAt: moment.Moment;
+
+ constructor(
+ private http: Http,
+ private auth: AuthHandler,
+ private baseUri: string,
+ private data: any = {}
+ ) {}
+
+ public save(): Promise {
+ // TODO: optimistic locking
+ let request: Observable;
+ if (this.id) {
+ request = this.http.put(
+ this.baseUri + '/' + this.id,
+ JSON.stringify(this.data),
+ this.auth.setOptions(getOptions())
+ );
+ } else {
+ request = this.http.put(
+ this.baseUri,
+ JSON.stringify(this.data),
+ this.auth.setOptions(getOptions())
+ );
+ }
+ return request
+ .map((res: Response) => this.auth.handleResponse(res))
+ .map((res: Response) => {
+ this.data = res.json();
+ return this;
+ })
+ .toPromise();
+ }
+
+ public delete(): Promise {
+ return this.http
+ .delete(
+ this.baseUri + '/' + this.id,
+ this.auth.setOptions(getOptions())
+ )
+ .map((res: Response) => this.auth.handleResponse(res))
+ .toPromise()
+ .then(() => { /* */ });
+ }
+}
+
+export function getOptions(): RequestOptions {
+ const headers: Headers = new Headers();
+ headers.set('accept', 'application/json');
+
+ return new RequestOptions({
+ headers: headers
+ });
+}
diff --git a/angular2/field.ts b/angular2/field.ts
index 05da388..4637793 100644
--- a/angular2/field.ts
+++ b/angular2/field.ts
@@ -5,25 +5,23 @@ import {Types} from '../shared/Types';
export function field(type?: Types): (target: any, key: string) => any {
return function field(target: any, key: string) {
- let val = target[key];
-
// property getter
const getter = function () {
- if (!val) {
+ if (!this['_' + key]) {
switch (type) {
case(Types.dateTimeTz):
- val = moment(this.data[key]);
+ this['_' + key] = moment(this.data[key]);
break;
default:
- val = this.data[key];
+ this['_' + key] = this.data[key];
}
}
- return val;
+ return this['_' + key];
};
// property setter
const setter = function (newVal) {
- val = newVal;
+ this['_' + key] = newVal;
switch (type) {
case(Types.dateTimeTz):
this.data[key] = newVal.toISOString();
diff --git a/angular2/index.ts b/angular2/index.ts
index 39c7e7d..adaddf6 100644
--- a/angular2/index.ts
+++ b/angular2/index.ts
@@ -1,4 +1,5 @@
-export {DataConnection, DataContract} from './DataObject';
+export {DataContract} from './DataContract';
+export {DataConnection} from './DataConnection';
export {field} from './field';
export {register} from '../shared/DataObject';
export {AuthHandler} from './AuthHandler';
diff --git a/mochaInit.ts b/mochaInit.ts
new file mode 100644
index 0000000..a892de8
--- /dev/null
+++ b/mochaInit.ts
@@ -0,0 +1,22 @@
+import * as chai from 'chai';
+import * as chaiAsPromised from 'chai-as-promised';
+
+chai.use(chaiAsPromised);
+require('source-map-support').install();
+
+/**
+ * This makes the "loading non-allowed module" warnings from mockery fatal
+ */
+const origwarn = console.warn;
+console.warn = function(...args: any[]) {
+ if (
+ args.length === 1 &&
+ args[0].startsWith &&
+ args[0].startsWith('WARNING: loading non-allowed module:')
+ ) {
+ chai.expect(args[0].replace('WARNING: loading non-allowed module: ', ''))
+ .to.equal('', 'Loading non-allowed module');
+ } else {
+ origwarn.apply(console, args);
+ }
+};
diff --git a/mocks/Sequelize.ts b/mocks/Sequelize.ts
new file mode 100644
index 0000000..d880d96
--- /dev/null
+++ b/mocks/Sequelize.ts
@@ -0,0 +1,133 @@
+import * as sinon from 'sinon';
+import * as sequelize from 'sequelize';
+import * as _ from 'lodash';
+
+export class Sequelize implements sequelize.SequelizeStaticAndInstance {
+ public Utils: typeof sequelize.Utils;
+ public Promise;
+ public QueryTypes: sequelize.QueryTypes;
+ public Validator: any;
+ public Model: sequelize.Model;
+ public Transaction: sequelize.TransactionStatic;
+ public Deferrable: sequelize.Deferrable;
+ public Instance: sequelize.Instance;
+ public Error: sequelize.BaseError;
+ public ValidationError: sequelize.ValidationError;
+ public ValidationErrorItem: sequelize.ValidationErrorItem;
+ public DatabaseError: sequelize.DatabaseError;
+ public TimeoutError: sequelize.TimeoutError;
+ public UniqueConstraintError: sequelize.UniqueConstraintError;
+ public ExclusionConstraintError: sequelize.ExclusionConstraintError;
+ public ForeignKeyConstraintError: sequelize.ForeignKeyConstraintError;
+ public ConnectionError: sequelize.ConnectionError;
+ public ConnectionRefusedError: sequelize.ConnectionRefusedError;
+ public AccessDeniedError: sequelize.AccessDeniedError;
+ public HostNotFoundError: sequelize.HostNotFoundError;
+ public HostNotReachableError: sequelize.HostNotReachableError;
+ public InvalidConnectionError: sequelize.InvalidConnectionError;
+ public ConnectionTimedOutError: sequelize.ConnectionTimedOutError;
+
+ public fn: sinon.SinonStub = sinon.stub();
+ public col: sinon.SinonStub = sinon.stub();
+ public cast: sinon.SinonStub = sinon.stub();
+ public literal: sinon.SinonStub = sinon.stub();
+ public asIs: sinon.SinonStub = sinon.stub();
+ public and: sinon.SinonStub = sinon.stub();
+ public or: sinon.SinonStub = sinon.stub();
+ public json: sinon.SinonStub = sinon.stub();
+ public where: sinon.SinonStub = sinon.stub();
+ public condition: sinon.SinonStub = sinon.stub();
+ public define: sinon.SinonStub = sinon.stub();
+
+ constructor(
+ public database,
+ public username,
+ public password,
+ public options
+ ) {}
+}
+
+_.forEach(sequelize, (val, key) => {
+ if (key === key.toUpperCase()) {
+ Sequelize[key] = val;
+ }
+});
+
+export class Model implements sequelize.Model {
+ public Instance: sinon.SinonStub = sinon.stub();
+ public removeAttribute: sinon.SinonStub = sinon.stub();
+ public sync: sinon.SinonStub = sinon.stub();
+ public drop: sinon.SinonStub = sinon.stub();
+ public schema: sinon.SinonStub = sinon.stub();
+ public getTableName: sinon.SinonStub = sinon.stub();
+ public scope: sinon.SinonStub = sinon.stub();
+ public findAll: sinon.SinonStub = sinon.stub();
+ public all: sinon.SinonStub = sinon.stub();
+ public findById: sinon.SinonStub = sinon.stub();
+ public findByPrimary: sinon.SinonStub = sinon.stub();
+ public findOne: sinon.SinonStub = sinon.stub();
+ public find: sinon.SinonStub = sinon.stub();
+ public aggregate: sinon.SinonStub = sinon.stub();
+ public count: sinon.SinonStub = sinon.stub();
+ public findAndCount: sinon.SinonStub = sinon.stub();
+ public findAndCountAll: sinon.SinonStub = sinon.stub();
+ public max: sinon.SinonStub = sinon.stub();
+ public min: sinon.SinonStub = sinon.stub();
+ public sum: sinon.SinonStub = sinon.stub();
+ public build: sinon.SinonStub = sinon.stub();
+ public bulkBuild: sinon.SinonStub = sinon.stub();
+ public create: sinon.SinonStub = sinon.stub();
+ public findOrInitialize: sinon.SinonStub = sinon.stub();
+ public findOrBuild: sinon.SinonStub = sinon.stub();
+ public findOrCreate: sinon.SinonStub = sinon.stub();
+ public upsert: sinon.SinonStub = sinon.stub();
+ public insertOrUpdate: sinon.SinonStub = sinon.stub();
+ public bulkCreate: sinon.SinonStub = sinon.stub();
+ public truncate: sinon.SinonStub = sinon.stub();
+ public destroy: sinon.SinonStub = sinon.stub();
+ public restore: sinon.SinonStub = sinon.stub();
+ public update: sinon.SinonStub = sinon.stub();
+ public describe: sinon.SinonStub = sinon.stub();
+ public unscoped: sinon.SinonStub = sinon.stub();
+ public addHook: sinon.SinonStub = sinon.stub();
+ public hook: sinon.SinonStub = sinon.stub();
+ public removeHook: sinon.SinonStub = sinon.stub();
+ public hasHook: sinon.SinonStub = sinon.stub();
+ public hasHooks: sinon.SinonStub = sinon.stub();
+ public beforeValidate: sinon.SinonStub = sinon.stub();
+ public afterValidate: sinon.SinonStub = sinon.stub();
+ public beforeCreate: sinon.SinonStub = sinon.stub();
+ public afterCreate: sinon.SinonStub = sinon.stub();
+ public beforeDestroy: sinon.SinonStub = sinon.stub();
+ public beforeDelete: sinon.SinonStub = sinon.stub();
+ public afterDestroy: sinon.SinonStub = sinon.stub();
+ public afterDelete: sinon.SinonStub = sinon.stub();
+ public beforeUpdate: sinon.SinonStub = sinon.stub();
+ public afterUpdate: sinon.SinonStub = sinon.stub();
+ public beforeBulkCreate: sinon.SinonStub = sinon.stub();
+ public afterBulkCreate: sinon.SinonStub = sinon.stub();
+ public beforeBulkDestroy: sinon.SinonStub = sinon.stub();
+ public beforeBulkDelete: sinon.SinonStub = sinon.stub();
+ public afterBulkDestroy: sinon.SinonStub = sinon.stub();
+ public afterBulkDelete: sinon.SinonStub = sinon.stub();
+ public beforeBulkUpdate: sinon.SinonStub = sinon.stub();
+ public afterBulkUpdate: sinon.SinonStub = sinon.stub();
+ public beforeFind: sinon.SinonStub = sinon.stub();
+ public beforeFindAfterExpandIncludeAll: sinon.SinonStub = sinon.stub();
+ public beforeFindAfterOptions: sinon.SinonStub = sinon.stub();
+ public afterFind: sinon.SinonStub = sinon.stub();
+ public beforeDefine: sinon.SinonStub = sinon.stub();
+ public afterDefine: sinon.SinonStub = sinon.stub();
+ public beforeInit: sinon.SinonStub = sinon.stub();
+ public afterInit: sinon.SinonStub = sinon.stub();
+ public beforeBulkSync: sinon.SinonStub = sinon.stub();
+ public afterBulkSync: sinon.SinonStub = sinon.stub();
+ public beforeSync: sinon.SinonStub = sinon.stub();
+ public afterSync: sinon.SinonStub = sinon.stub();
+ public hasOne: sinon.SinonStub = sinon.stub();
+ public belongsTo: sinon.SinonStub = sinon.stub();
+ public hasMany: sinon.SinonStub = sinon.stub();
+ public belongsToMany: sinon.SinonStub = sinon.stub();
+
+ constructor(public tableName: string) {}
+}
diff --git a/mocks/connect.ts b/mocks/connect.ts
new file mode 100644
index 0000000..a6b3dbc
--- /dev/null
+++ b/mocks/connect.ts
@@ -0,0 +1,22 @@
+import * as sinon from 'sinon';
+
+export const model = {
+ create: sinon.stub(),
+ findAll: sinon.stub(),
+ findById: sinon.stub(),
+ sync: sinon.stub()
+};
+
+export function reset(connect: any) {
+ model.create = sinon.stub();
+
+ model.findById = sinon.stub();
+ model.findById.returns(Promise.resolve(null));
+
+ model.findAll = sinon.stub();
+ model.findAll.returns(Promise.resolve([]));
+
+ model.sync = sinon.stub();
+
+ connect.connection.define.returns(model);
+}
diff --git a/node/DataConnection.spec.class.ts b/node/DataConnection.spec.class.ts
new file mode 100644
index 0000000..b0703be
--- /dev/null
+++ b/node/DataConnection.spec.class.ts
@@ -0,0 +1,47 @@
+import {DataConnection} from './DataConnection';
+import {register} from '../shared/DataObject';
+import * as Contracts from './DataContract.spec.class';
+
+@register('test')
+export class NoProp extends DataConnection {
+ protected getContract() {
+ return Contracts.NoProp;
+ }
+}
+
+@register('test')
+export class StringProp extends DataConnection {
+ protected getContract() {
+ return Contracts.StringProp;
+ }
+}
+
+export class FloatProp extends DataConnection {
+ protected getContract() {
+ return Contracts.FloatProp;
+ }
+}
+
+export class IntProp extends DataConnection {
+ protected getContract() {
+ return Contracts.IntProp;
+ }
+}
+
+export class BigIntProp extends DataConnection {
+ protected getContract() {
+ return Contracts.BigIntProp;
+ }
+}
+
+export class DateProp extends DataConnection {
+ protected getContract() {
+ return Contracts.DateProp;
+ }
+}
+
+export class BadProp extends DataConnection {
+ protected getContract() {
+ return Contracts.BadProp;
+ }
+}
diff --git a/node/DataConnection.spec.ts b/node/DataConnection.spec.ts
new file mode 100644
index 0000000..aeae717
--- /dev/null
+++ b/node/DataConnection.spec.ts
@@ -0,0 +1,267 @@
+import * as mockery from 'mockery';
+import * as chai from 'chai';
+import * as classTypes from './DataConnection.spec.class';
+import * as sequelize from 'sequelize';
+import * as mockConnect from '../mocks/connect';
+
+import {Sequelize} from '../mocks/Sequelize';
+
+describe('node/DataConnection', function() {
+ let connect: any;
+ let classes: any;
+
+ before(function(){
+ mockery.enable();
+ mockery.registerMock('sequelize', Sequelize);
+ mockery.registerAllowables([
+ './connect',
+ './DataContract',
+ './DataConnection',
+ './DataContract.spec.class',
+ './DataConnection.spec.class',
+ '../shared/DataObject',
+ './field',
+ '../shared/field',
+ '../shared/Types',
+ './Types',
+ 'moment',
+ 'lodash',
+ 'bunyan',
+ 'os',
+ 'fs',
+ 'util',
+ 'assert',
+ 'events',
+ 'stream'
+ ]);
+
+ connect = require('./connect');
+ connect.connect('a', 'b', 'c');
+
+ classes = require('./DataConnection.spec.class');
+ });
+
+ beforeEach(function () {
+ mockConnect.reset(connect);
+
+ mockConnect.model.findById.withArgs(42).returns(Promise.resolve({
+ get: () => 42
+ }));
+ mockConnect.model.findAll.withArgs({
+ include: [{all: true}],
+ where: {
+ find: true
+ }
+ }).returns(Promise.resolve([
+ {
+ get: () => 40
+ },
+ {
+ get: () => 41
+ },
+ {
+ get: () => 42
+ }
+ ]));
+ });
+
+ describe('constructor', function() {
+ beforeEach(function () {
+ connect.connection.define.reset();
+ });
+
+ describe('noProp', function () {
+ let current: classTypes.NoProp;
+
+ beforeEach(function () {
+ current = new classes.NoProp();
+ });
+
+ it('defines the connection', function () {
+ const calls = connect.connection.define.getCalls();
+
+ chai.expect(calls.length).to.equal(1);
+ chai.expect(calls[0].args).to.deep.equal([
+ "NoProp",
+ {},
+ {
+ "freezeTableName": true
+ }
+ ]);
+ });
+ });
+
+ describe('stringProp', function () {
+ let current: classTypes.StringProp;
+
+ beforeEach(function () {
+ current = new classes.StringProp();
+ });
+
+ it('defines the connection', function () {
+ const calls = connect.connection.define.getCalls();
+
+ chai.expect(calls.length).to.equal(1);
+ chai.expect(calls[0].args).to.deep.equal([
+ "StringProp",
+ {
+ "stringy": {
+ type: sequelize.STRING
+ }
+ },
+ {
+ "freezeTableName": true
+ }
+ ]);
+ });
+ });
+
+ describe('floatProp', function () {
+ let current: classTypes.FloatProp;
+
+ beforeEach(function () {
+ current = new classes.FloatProp();
+ });
+
+ it('defines the connection', function () {
+ const calls = connect.connection.define.getCalls();
+
+ chai.expect(calls.length).to.equal(1);
+ chai.expect(calls[0].args).to.deep.equal([
+ "FloatProp",
+ {
+ "floaty": {
+ type: sequelize.FLOAT
+ }
+ },
+ {
+ "freezeTableName": true
+ }
+ ]);
+ });
+ });
+
+ describe('intProp', function () {
+ let current: classTypes.IntProp;
+
+ beforeEach(function () {
+ current = new classes.IntProp();
+ });
+
+ it('defines the connection', function () {
+ const calls = connect.connection.define.getCalls();
+
+ chai.expect(calls.length).to.equal(1);
+ chai.expect(calls[0].args).to.deep.equal([
+ "IntProp",
+ {
+ "inty": {
+ type: sequelize.INTEGER
+ }
+ },
+ {
+ "freezeTableName": true
+ }
+ ]);
+ });
+ });
+
+ describe('bigIntProp', function () {
+ let current: classTypes.BigIntProp;
+
+ beforeEach(function () {
+ current = new classes.BigIntProp();
+ });
+
+ it('defines the connection', function () {
+ const calls = connect.connection.define.getCalls();
+
+ chai.expect(calls.length).to.equal(1);
+ chai.expect(calls[0].args).to.deep.equal([
+ "BigIntProp",
+ {
+ "inty": {
+ type: sequelize.BIGINT
+ }
+ },
+ {
+ "freezeTableName": true
+ }
+ ]);
+ });
+ });
+
+ describe('dateProp', function () {
+ let current: classTypes.DateProp;
+
+ beforeEach(function () {
+ current = new classes.DateProp();
+ });
+
+ it('defines the connection', function () {
+ const calls = connect.connection.define.getCalls();
+
+ chai.expect(calls.length).to.equal(1);
+ chai.expect(calls[0].args).to.deep.equal([
+ "DateProp",
+ {
+ "dateThing": {
+ type: sequelize.DATE
+ }
+ },
+ {
+ "freezeTableName": true
+ }
+ ]);
+ });
+ });
+
+ describe('badProp', function () {
+
+ it('throws an error', function () {
+ chai.expect(() => {
+ const a = new classes.BadProp();
+ a.create();
+ }).to.throw(TypeError);
+ });
+ });
+ });
+
+ describe('noProp', function () {
+ let current: classTypes.NoProp;
+
+ beforeEach(function () {
+ current = new classes.NoProp();
+ });
+
+ it('creates', function () {
+ const thing = current.create();
+ chai.expect(thing).to.not.be.null;
+ });
+
+ it('rejects on bad ids', function () {
+ return chai.expect(current.fetch(40)).to.eventually.be.rejectedWith('Not Found');
+ });
+
+ it('resolves on good ids', function () {
+ return chai.expect(current.fetch(42)).to.eventually.be.fulfilled;
+ });
+
+ it('returns empty', function () {
+ return chai.expect(current.search({
+ find: false
+ })).to.eventually.deep.equal([]);
+ });
+
+ it('returns full', function () {
+ return chai.expect(current.search({
+ find: true
+ })).to.eventually.have.deep.property('[2].id', 42);
+ });
+ });
+
+ after(function() {
+ mockery.deregisterAll();
+ mockery.disable();
+ });
+});
diff --git a/node/DataConnection.ts b/node/DataConnection.ts
new file mode 100644
index 0000000..6870455
--- /dev/null
+++ b/node/DataConnection.ts
@@ -0,0 +1,143 @@
+// DO NOT REMOVE THIS IMPORT it is required for this file to function
+// tslint:disable-next-line:no-unused-variable
+import * as reflectMetadata from 'reflect-metadata';
+import * as sequelize from 'sequelize';
+import * as _ from 'lodash';
+
+import { Types } from '../shared/Types';
+import { IDataConnection } from '../shared/DataObject';
+import { connection } from './connect';
+import {DataContract} from './DataContract';
+
+export abstract class DataConnection implements IDataConnection {
+
+ private static syncedModels: {
+ [modelName: string]: sequelize.Model;
+ } = {};
+
+ private _dummyContract: T = null;
+ private get dummyContract(): T {
+ if (!this._dummyContract) {
+ this._dummyContract = new (this.getContract())(null, null);
+ }
+ return this._dummyContract;
+ }
+
+ private _fields: string[] = [];
+ private get fields(): string[] {
+ if (!this._fields.length) {
+ this._fields = Reflect.getMetadata('ORM:fields', this.dummyContract);
+ }
+ return this._fields;
+ }
+
+ // This is used in some of the decorators
+ // tslint:disable-next-line:no-unused-variable
+ private instance: any = null;
+
+ private get model(): sequelize.Model {
+ return DataConnection.syncedModels[( this.constructor).name];
+ }
+ private set model(val: sequelize.Model) {
+ DataConnection.syncedModels[( this.constructor).name] = val;
+ // TODO make updates more graceful
+ val.sync();
+ }
+
+ constructor(injector?: any) {
+ let className = ( this.constructor).name;
+
+ if (!this.model) {
+ const model: any = {};
+ const fields: string[] = this.fields;
+ _.forEach(fields, (fieldName) => {
+ const type: Types = Reflect.getMetadata(
+ "ORM:type",
+ this.dummyContract,
+ fieldName
+ );
+
+ switch (type) {
+ case Types.string:
+ model[fieldName] = {
+ type: sequelize.STRING
+ };
+ break;
+ case Types.float:
+ model[fieldName] = {
+ type: sequelize.FLOAT
+ };
+ break;
+ case Types.integer:
+ model[fieldName] = {
+ type: sequelize.INTEGER
+ };
+ break;
+ case Types.bigInt:
+ model[fieldName] = {
+ type: sequelize.BIGINT
+ };
+ break;
+ case Types.dateTimeTz:
+ model[fieldName] = {
+ type: sequelize.DATE
+ };
+ break;
+ default:
+ throw new TypeError(
+ 'Field of unknown type found! ' +
+ 'Field Name:' + fieldName + ' ' +
+ 'Field Type: ' + type
+ );
+ }
+ });
+ this.model = connection.define(
+ className,
+ model,
+ {
+ freezeTableName: true // Model tableName will be the same as the model name
+ }
+ );
+ }
+ }
+
+ public fetch(id: number): Promise {
+ return this.model.findById(id)
+ .then((sqlData: any): Promise | T => {
+ if ( sqlData === null ) {
+ return Promise.reject('Not Found');
+ } else {
+ return new (this.getContract())(sqlData, this.model);
+ }
+ });
+ }
+
+ public create(): T {
+ return new (this.getContract())(null, this.model);
+ }
+
+ public search(
+ criteria: sequelize.WhereOptions | Array
+ ): Promise {
+ return this.model
+ .findAll({
+ include: [{ all: true }],
+ where: criteria
+ })
+ .then((data: any[]) => {
+ let ret: T[] = [];
+ _.forEach(data, (value: any) => {
+ ret.push(new (this.getContract())(value, this.model));
+ });
+ return ret;
+ });
+ }
+
+ /**
+ * This feeds the data contract into the system
+ */
+ protected abstract getContract(): new(
+ instance: any,
+ model: sequelize.Model
+ ) => T;
+}
diff --git a/node/DataContract.spec.class.ts b/node/DataContract.spec.class.ts
new file mode 100644
index 0000000..07de4b4
--- /dev/null
+++ b/node/DataContract.spec.class.ts
@@ -0,0 +1,31 @@
+import {DataContract} from './DataContract';
+import {field} from './field';
+import {Types} from '../shared/Types';
+
+import * as moment from 'moment';
+
+export class NoProp extends DataContract {}
+export class StringProp extends DataContract {
+ @field()
+ public stringy: string;
+}
+export class FloatProp extends DataContract {
+ @field()
+ public floaty: number;
+}
+export class IntProp extends DataContract {
+ @field(Types.integer)
+ public inty: number;
+}
+export class BigIntProp extends DataContract {
+ @field(Types.bigInt)
+ public inty: number;
+}
+export class DateProp extends DataContract {
+ @field(Types.dateTimeTz)
+ public dateThing: moment.Moment;
+}
+export class BadProp extends DataContract {
+ @field( 'a')
+ public dateThing: moment.Moment;
+}
diff --git a/node/DataContract.spec.ts b/node/DataContract.spec.ts
new file mode 100644
index 0000000..a143d10
--- /dev/null
+++ b/node/DataContract.spec.ts
@@ -0,0 +1,240 @@
+import * as mockery from 'mockery';
+import * as chai from 'chai';
+import * as classTypes from './DataContract.spec.class';
+import * as moment from 'moment';
+import * as sinon from 'sinon';
+
+import {Sequelize, Model} from '../mocks/Sequelize';
+
+describe('node/DataContract', function() {
+ let connect: any;
+ let classes: any;
+
+ before(function(){
+ mockery.enable();
+ mockery.registerMock('sequelize', Sequelize);
+ mockery.registerAllowables([
+ './connect',
+ './DataContract',
+ './DataContract.spec.class',
+ './field',
+ '../shared/field',
+ '../shared/Types',
+ './Types',
+ 'moment',
+ 'lodash'
+ ]);
+
+ connect = require('./connect');
+ connect.connect('a', 'b', 'c');
+
+ classes = require('./DataContract.spec.class');
+ });
+
+ describe('existing', function() {
+ let model: Model;
+ let instance: any;
+
+ beforeEach(function () {
+ model = new Model('existing');
+ instance = {
+ destroy: sinon.stub(),
+ get: sinon.stub(),
+ save: sinon.stub(),
+ set: sinon.stub()
+ };
+ instance.destroy.returns(Promise.resolve());
+ instance.save.returns(Promise.resolve());
+ });
+
+ describe('noProp', function () {
+ let current: classTypes.NoProp;
+
+ beforeEach(function () {
+ current = new classes.NoProp(instance, model);
+ });
+
+ it('saves', function () {
+ current.save();
+ chai.expect(instance.save.getCalls().length).to.equal(1);
+ });
+
+ it('destroys', function () {
+ current.delete();
+ chai.expect(instance.destroy.getCalls().length).to.equal(1);
+ });
+ });
+
+ describe('stringProp', function() {
+ let current: classTypes.StringProp;
+
+ beforeEach(function () {
+ current = new classes.StringProp(instance, model);
+ });
+
+ it('updates the model', function () {
+ current.stringy = 'thingy';
+ const calls = instance.set.getCalls();
+ chai.expect(calls.length).to.equal(1);
+ chai.expect(calls[0].args).to.deep.equal(['stringy', 'thingy']);
+ });
+
+ it('fetches from the model', function () {
+ instance.get.withArgs('stringy').returns('thingy');
+ chai.expect(current.stringy).to.equal('thingy');
+ });
+ });
+
+ describe('dateProp', function() {
+ let current: classTypes.DateProp;
+ const isoDate = '2016-07-01T18:25:06.094Z';
+
+ beforeEach(function () {
+ current = new classes.DateProp(instance, model);
+ });
+
+ it('updates the model', function () {
+ current.dateThing = moment(isoDate);
+ const calls = instance.set.getCalls();
+ chai.expect(calls.length).to.equal(1);
+ chai.expect(calls[0].args).to.deep.equal(['dateThing', isoDate]);
+ });
+
+ it('fetches from the model', function () {
+ instance.get.withArgs('dateThing').returns(isoDate);
+ chai.expect(current.dateThing.toISOString()).to.equal(isoDate);
+ });
+
+ it('handles invalid dates', function () {
+ current.dateThing = moment.invalid();
+ const calls = instance.set.getCalls();
+ chai.expect(calls.length).to.equal(1);
+ chai.expect(calls[0].args).to.deep.equal(['dateThing', null]);
+ });
+
+ it('handles bad assigns', function () {
+ current.dateThing = 'Invalid Date';
+ const calls = instance.set.getCalls();
+ chai.expect(calls.length).to.equal(1);
+ chai.expect(calls[0].args).to.deep.equal(['dateThing', null]);
+ });
+ });
+ });
+
+ describe('new', function() {
+ let model: Model;
+
+ beforeEach(function () {
+ model = new Model('new');
+ });
+
+ describe('noProp', function () {
+ let current: classTypes.NoProp;
+
+ beforeEach(function () {
+ current = new classes.NoProp(null, model);
+ });
+
+ describe('serialize', function () {
+ it('empty object', function () {
+ chai.expect(current.serialize()).to.equal('{}');
+ });
+
+ it('id', function () {
+ current.id = 0;
+ chai.expect(current.serialize()).to.equal('{"id":0}');
+ });
+
+ it('dates in iso format', function () {
+ const isoDate = '2016-07-01T18:25:06.094Z';
+ const isoDate2 = '2016-07-02T18:25:06.094Z';
+ current.createdAt = moment(isoDate);
+ current.updatedAt = moment(isoDate2);
+ chai.expect(current.serialize()).to.equal(
+ '{' +
+ '"createdAt":"' + isoDate + '",' +
+ '"updatedAt":"' + isoDate2 + '"' +
+ '}'
+ );
+ });
+ });
+
+ describe('save', function () {
+ it('creates an object', function () {
+ model.create.returns(Promise.resolve({}));
+ current.save();
+
+ const calls = model.create.getCalls();
+ chai.expect(calls.length).to.equal(1);
+ chai.expect(calls[0].args).to.deep.equal([{}]);
+ });
+ });
+
+ describe('delete', function () {
+ it('returns a resolved promise', function () {
+ return current.delete();
+ });
+ });
+ });
+
+ describe('stringProp', function() {
+ let current: classTypes.StringProp;
+
+ beforeEach(function () {
+ current = new classes.StringProp(null, model);
+ });
+
+ describe('serialize', function () {
+ it('empty object', function () {
+ chai.expect(current.serialize()).to.equal('{}');
+ });
+
+ it('value', function () {
+ current.stringy = 'stuff';
+ chai.expect(current.serialize()).to.equal('{"stringy":"stuff"}');
+ });
+ });
+
+ describe('loadData', function () {
+ it('loads data', function () {
+ current.loadData({stringy: 'thingy'});
+ chai.expect(current.stringy).to.equal('thingy');
+ });
+ });
+ });
+
+ describe('dateProp', function() {
+ let current: classTypes.DateProp;
+
+ beforeEach(function () {
+ current = new classes.DateProp(null, model);
+ });
+
+ describe('serialize', function () {
+ it('empty object', function () {
+ chai.expect(current.serialize()).to.equal('{}');
+ });
+
+ it('value', function () {
+ const isoDate = '2016-07-01T18:25:06.094Z';
+ current.dateThing = moment(isoDate);
+ chai.expect(current.serialize())
+ .to.equal('{"dateThing":"' + isoDate + '"}');
+ });
+ });
+
+ describe('loadData', function () {
+ it('loads data', function () {
+ const isoDate = '2016-07-01T18:25:06.094Z';
+ current.loadData({dateThing: isoDate});
+ chai.expect(current.dateThing.toISOString()).to.equal(isoDate);
+ });
+ });
+ });
+ });
+
+ after(function() {
+ mockery.deregisterAll();
+ mockery.disable();
+ });
+});
diff --git a/node/DataContract.ts b/node/DataContract.ts
new file mode 100644
index 0000000..e892617
--- /dev/null
+++ b/node/DataContract.ts
@@ -0,0 +1,93 @@
+// DO NOT REMOVE THIS IMPORT it is required for this file to function
+// tslint:disable-next-line:no-unused-variable
+import * as reflectMetadata from 'reflect-metadata';
+import * as sequelize from 'sequelize';
+import * as moment from 'moment';
+import * as _ from 'lodash';
+
+import { field } from './field';
+import { Types } from '../shared/Types';
+import { IDataContract } from '../shared/DataObject';
+
+export abstract class DataContract implements IDataContract {
+ private get fields(): string[] {
+ return Reflect.getMetadata('ORM:fields', this);
+ }
+
+ @field()
+ public id: number;
+ @field(Types.dateTimeTz)
+ public createdAt: moment.Moment;
+ @field(Types.dateTimeTz)
+ public updatedAt: moment.Moment;
+
+ constructor(
+ private instance: any,
+ private model: sequelize.Model
+ ) {}
+
+ public loadData(data: any): void {
+ const fields: string[] = this.fields;
+ for (const i in fields) {
+ if (data[fields[i]] !== undefined) {
+ // TODO: validation
+ const type: Types = Reflect.getMetadata("ORM:type", this, fields[i]);
+ switch (type) {
+ case(Types.dateTimeTz):
+ this[fields[i]] = moment(data[fields[i]]);
+ break;
+ default:
+ this[fields[i]] = data[fields[i]];
+ }
+ }
+ }
+ }
+
+ public serialize(): string {
+ return JSON.stringify(this.toJSON());
+ }
+
+ public save(): Promise {
+ if (this.instance) {
+ return this.instance.save().then(() => this);
+ } else {
+ return this.model.create(this.getFields()).then((sqlData: any) => {
+ this.instance = sqlData;
+ return this;
+ });
+ }
+ }
+
+ public delete(): Promise {
+ if (this.instance) {
+ return this.instance.destroy();
+ } else {
+ return Promise.resolve();
+ }
+ }
+
+ private toJSON(): any {
+ const returnObj: any = this.getFields();
+ returnObj.id = this.id;
+ returnObj.createdAt = this.createdAt && this.createdAt.toISOString();
+ returnObj.updatedAt = this.updatedAt && this.updatedAt.toISOString();
+ return returnObj;
+ }
+
+ private getFields(): any {
+ const returnObj: any = {};
+ const fields: string[] = this.fields;
+ _.forEach(fields, (fieldName: string) => {
+ const value: any = this[fieldName];
+ const type: Types = Reflect.getMetadata("ORM:type", this, fieldName);
+ switch (type) {
+ case(Types.dateTimeTz):
+ returnObj[fieldName] = value && value.toISOString();
+ break;
+ default:
+ returnObj[fieldName] = value;
+ }
+ });
+ return returnObj;
+ }
+}
diff --git a/node/DataObject.ts b/node/DataObject.ts
deleted file mode 100644
index fc367e3..0000000
--- a/node/DataObject.ts
+++ /dev/null
@@ -1,241 +0,0 @@
-// DO NOT REMOVE THIS IMPORT it is required for this file to function
-// tslint:disable-next-line:no-unused-variable
-import * as reflectMetadata from 'reflect-metadata';
-import * as sequelize from 'sequelize';
-import * as moment from 'moment';
-import * as _ from 'lodash';
-import * as bunyan from 'bunyan';
-
-import { field } from './field';
-import { Types } from '../shared/Types';
-import { IDataConnection, IDataContract, registeredClasses } from '../shared/DataObject';
-import { connection } from './connect';
-
-const logger = bunyan.createLogger({name: "ORM/DataObject"});
-
-export function getInject() {
- return Array.from(registeredClasses.values());
-}
-
-export abstract class DataContract implements IDataContract {
- private get fields(): string[] {
- return Reflect.getMetadata('ORM:fields', this);
- }
-
- @field()
- public id: number;
- @field(Types.dateTimeTz)
- public createdAt: moment.Moment;
- @field(Types.dateTimeTz)
- public updatedAt: moment.Moment;
-
- constructor(
- private instance: any,
- private model: sequelize.Model
- ) {}
-
- public loadData(data: any): void {
- const fields: string[] = this.fields;
- for (const i in fields) {
- if (data[fields[i]] !== undefined) {
- // TODO: validation
- const type: Types = Reflect.getMetadata("ORM:type", this, fields[i]);
- switch (type) {
- case(Types.dateTimeTz):
- this[fields[i]] = moment(data[fields[i]]);
- break;
- default:
- this[fields[i]] = data[fields[i]];
- }
- }
- }
- }
-
- public serialize(): string {
- return JSON.stringify(this.toJSON());
- }
-
- public save(): Promise {
- if (this.instance) {
- return this.instance.save();
- } else {
- return this.model.create(this.getFields()).then((sqlData: any) => {
- this.instance = sqlData;
- });
- }
- }
-
- public delete(): Promise {
- if (this.instance) {
- return this.instance.destroy();
- } else {
- return Promise.resolve();
- }
- }
-
- private toJSON(): any {
- const returnObj: any = this.getFields();
- returnObj.id = this.id;
- returnObj.createdAt = this.createdAt && this.createdAt.toISOString();
- returnObj.updatedAt = this.updatedAt && this.updatedAt.toISOString();
- return returnObj;
- }
-
- private getFields(): any {
- const returnObj: any = {};
- const fields: string[] = this.fields;
- _.forEach(fields, (fieldName: string) => {
- const value: any = this[fieldName];
- const type: Types = Reflect.getMetadata("ORM:type", this, fieldName);
- switch (type) {
- case(Types.dateTimeTz):
- returnObj[fieldName] = value.toISOString();
- break;
- default:
- returnObj[fieldName] = value;
- }
- });
- return returnObj;
- }
-}
-
-export abstract class DataConnection implements IDataConnection {
-
- private static syncedModels: {
- [modelName: string]: sequelize.Model;
- } = {};
-
- private _dummyContract: T = null;
- private get dummyContract(): T {
- if (!this._dummyContract) {
- this._dummyContract = new (this.getContract())(null, null);
- }
- return this._dummyContract;
- }
-
- private _fields: string[] = [];
- private get fields(): string[] {
- if (!this._fields.length) {
- this._fields = Reflect.getMetadata('ORM:fields', this.dummyContract);
- }
- return this._fields;
- }
-
- private _promise: Promise = null;
- public get promise(): Promise {
- return this._promise;
- }
-
- // This is used in some of the decorators
- // tslint:disable-next-line:no-unused-variable
- private instance: any = null;
-
- private get model(): sequelize.Model {
- return DataConnection.syncedModels[( this.constructor).name];
- }
- private set model(val: sequelize.Model) {
- DataConnection.syncedModels[( this.constructor).name] = val;
- // TODO make updates more graceful
- val.sync();
- }
-
- constructor(injector?: any) {
- let className = ( this.constructor).name;
-
- if (!this.model) {
- const model: any = {};
- const fields: string[] = this.fields;
- _.forEach(fields, (fieldName) => {
- const type: Types = Reflect.getMetadata(
- "ORM:type",
- this.dummyContract,
- fieldName
- );
-
- switch (type) {
- case Types.string:
- model[fieldName] = {
- type: sequelize.STRING
- };
- break;
- case Types.float:
- model[fieldName] = {
- type: sequelize.FLOAT
- };
- break;
- case Types.integer:
- model[fieldName] = {
- type: sequelize.INTEGER
- };
- break;
- case Types.bigInt:
- model[fieldName] = {
- type: sequelize.BIGINT
- };
- break;
- case Types.dateTimeTz:
- model[fieldName] = {
- type: sequelize.DATE
- };
- break;
- default:
- logger.error('Field of unknown type found!', {
- fieldName: fieldName,
- fieldType: type
- });
- break;
- }
- });
- this.model = connection.define(
- className,
- model,
- {
- freezeTableName: true // Model tableName will be the same as the model name
- }
- );
- }
- }
-
- public fetch(id: number): Promise {
- return new Promise((res, rej) => {
- this.model.findById(id).then(res, rej);
- }).then((sqlData: any): Promise | T => {
- if ( sqlData === null ) {
- return Promise.reject('Not Found');
- } else {
- return new (this.getContract())(sqlData, this.model);
- }
- });
- }
-
- public create(): T {
- return new (this.getContract())(null, this.model);
- }
-
- public search(
- criteria: sequelize.WhereOptions | Array
- ): Promise {
- return new Promise((resolve, reject) => {
- this.model
- .findAll({
- include: [{ all: true }],
- where: criteria
- })
- .then((data: any[]) => {
- let ret: T[] = [];
- _.forEach(data, (value: any) => {
- ret.push(new (this.getContract())(value, this.model));
- });
- resolve(ret);
- }, reject);
- });
- }
-
- /**
- * This feeds the data contract into the system
- */
- protected abstract getContract(): new(
- instance: any,
- model: sequelize.Model
- ) => T;
-}
diff --git a/node/connect.spec.ts b/node/connect.spec.ts
new file mode 100644
index 0000000..857152b
--- /dev/null
+++ b/node/connect.spec.ts
@@ -0,0 +1,33 @@
+import * as mockery from 'mockery';
+import * as chai from 'chai';
+
+import {Sequelize} from '../mocks/Sequelize';
+
+describe('node/connect', function() {
+ let connect: any;
+
+ before(function(){
+ mockery.enable();
+ mockery.registerMock('sequelize', Sequelize);
+ mockery.registerAllowables([
+ './connect',
+ 'lodash'
+ ]);
+
+ connect = require('./connect');
+ });
+ it('connects to the database', function(){
+ const options = {
+ 'c': 'd'
+ };
+ connect.connect('a', 'b', 'c', options);
+ chai.expect(connect.connection.database).to.equal('a');
+ chai.expect(connect.connection.username).to.equal('b');
+ chai.expect(connect.connection.password).to.equal('c');
+ chai.expect(connect.connection.options).to.equal(options);
+ });
+ after(function() {
+ mockery.deregisterAll();
+ mockery.disable();
+ });
+});
diff --git a/node/field.ts b/node/field.ts
index 7886113..d9a786f 100644
--- a/node/field.ts
+++ b/node/field.ts
@@ -1,3 +1,6 @@
+// DO NOT REMOVE THIS IMPORT it is required for this file to function
+// tslint:disable-next-line:no-unused-variable
+import * as reflectMetadata from 'reflect-metadata';
import * as moment from 'moment';
import {field as sharedField} from '../shared/field';
@@ -28,7 +31,11 @@ export function field(type?: Types): (target: any, key: string) => any {
if (this.instance) {
switch (type) {
case(Types.dateTimeTz):
- this.instance.set(key, newVal.toISOString());
+ if (!moment.isMoment(newVal) || !( newVal).isValid()) {
+ this.instance.set(key, null);
+ } else {
+ this.instance.set(key, newVal.toISOString());
+ }
break;
default:
this.instance.set(key, newVal);
diff --git a/node/getInject.spec.ts b/node/getInject.spec.ts
new file mode 100644
index 0000000..bfc03a7
--- /dev/null
+++ b/node/getInject.spec.ts
@@ -0,0 +1,27 @@
+import * as mockery from 'mockery';
+import * as chai from 'chai';
+
+describe('node/DataContract', function() {
+ let getInject: any;
+
+ before(function(){
+ mockery.enable();
+ mockery.registerMock('../shared/DataObject', {
+ registeredClasses: new Map( [['a', 'a'], ['b', 'b']])
+ });
+ mockery.registerAllowables([
+ './getInject'
+ ]);
+
+ getInject = require('./getInject');
+ });
+
+ it('returns injectables', function() {
+ chai.expect(getInject.getInject()).to.deep.equal(['a', 'b']);
+ });
+
+ after(function() {
+ mockery.deregisterAll();
+ mockery.disable();
+ });
+});
diff --git a/node/getInject.ts b/node/getInject.ts
new file mode 100644
index 0000000..405a960
--- /dev/null
+++ b/node/getInject.ts
@@ -0,0 +1,5 @@
+import { registeredClasses, IDataConnection, IDataContract } from '../shared/DataObject';
+
+export function getInject(): (new (...b: any[]) => IDataConnection)[] {
+ return Array.from(registeredClasses.values());
+}
diff --git a/node/http.spec.ts b/node/http.spec.ts
new file mode 100644
index 0000000..7be3744
--- /dev/null
+++ b/node/http.spec.ts
@@ -0,0 +1,365 @@
+import * as mockery from 'mockery';
+import * as chai from 'chai';
+import * as sinon from 'sinon';
+import * as mockConnect from '../mocks/connect';
+
+describe('node/http', function() {
+ let http: any;
+ let connect: any;
+ let object: any;
+ const responseData: any = {
+ setHeader: sinon.stub(),
+ statusCode: 0
+ };
+
+ before(function() {
+ mockery.enable();
+ mockery.registerAllowables([
+ './http',
+ './DataConnection',
+ './DataConnection.spec.class',
+ './DataContract.spec.class',
+ '../shared/DataObject',
+ './connect'
+ ]);
+
+ connect = require('./connect');
+
+ connect.connect('a', 'b', 'c');
+
+ require('./DataConnection.spec.class');
+ http = require('./http');
+ });
+
+ beforeEach(function () {
+ mockConnect.reset(connect);
+
+ object = {
+ destroy: sinon.stub(),
+ get: sinon.stub(),
+ save: sinon.stub(),
+ set: sinon.stub()
+ };
+
+ object.destroy.returns(Promise.resolve());
+ object.save.returns(Promise.resolve());
+ object.get.returns(43);
+
+ mockConnect.model.findById.withArgs(43).returns(Promise.resolve(object));
+ mockConnect.model.create.returns(Promise.resolve(object));
+ });
+
+ describe('get', function () {
+ const getRequest = {
+ method: 'GET',
+ url: ''
+ };
+
+ beforeEach(function () {
+ getRequest.url = '/orm/';
+ });
+
+ it('errors when given a bad class', function () {
+ getRequest.url += 'badClass/0';
+
+ const response = http.HTTP.handle(getRequest, responseData);
+ return Promise.all([
+ response.then(() => {
+ chai.expect(responseData.statusCode).to.equal(404);
+ }),
+ chai.expect(response).to.eventually.equal('Resource Not Found')
+ ]);
+ });
+
+ it('disallows string ids', function () {
+ getRequest.url += 'badClass/sldkfasdgf';
+
+ const response = http.HTTP.handle(getRequest, responseData);
+ return Promise.all([
+ response.then(() => {
+ chai.expect(responseData.statusCode).to.equal(405);
+ }),
+ chai.expect(response).to.eventually.equal('Method Not Allowed')
+ ]);
+ });
+
+ it('disallows classless access', function () {
+ getRequest.url = '/orm';
+ const response = http.HTTP.handle(getRequest, responseData);
+ return Promise.all([
+ response.then(() => {
+ chai.expect(responseData.statusCode).to.equal(405);
+ }),
+ chai.expect(response).to.eventually.equal('Method Not Allowed')
+ ]);
+ });
+
+ it('returns 404 for missing ids', function () {
+ getRequest.url += 'test.NoProp/1';
+ const response = http.HTTP.handle(getRequest, responseData);
+ return Promise.all([
+ response.then(() => {
+ chai.expect(responseData.statusCode).to.equal(404);
+ }),
+ chai.expect(response).to.eventually.equal('Resource Not Found')
+ ]);
+ });
+
+ it('returns 200 with data for working ids', function () {
+ const parse = (data: string) => JSON.parse(data);
+ getRequest.url += 'test.NoProp/43';
+ const response = http.HTTP.handle(getRequest, responseData);
+ return Promise.all([
+ response.then(() => {
+ chai.expect(responseData.statusCode).to.equal(200);
+ }),
+ chai.expect(response.then(parse)).to.eventually.have.property('id', 43),
+ chai.expect(response.then(parse)).to.eventually.have.property('createdAt'),
+ chai.expect(response.then(parse)).to.eventually.have.property('updatedAt')
+ ]);
+ });
+ });
+
+ describe('put', function () {
+ const putRequest = {
+ method: 'PUT',
+ on: sinon.stub(),
+ url: ''
+ };
+
+ beforeEach(function () {
+ putRequest.url = '/orm/';
+
+ putRequest.on.withArgs('data').callsArgWith(1, '{}');
+ putRequest.on.withArgs('end').callsArg(1);
+ });
+
+ it('errors when given a bad class', function () {
+ putRequest.url += 'badClass/0';
+
+ const response = http.HTTP.handle(putRequest, responseData);
+ return Promise.all([
+ response.then(() => {
+ chai.expect(responseData.statusCode).to.equal(404);
+ }),
+ chai.expect(response).to.eventually.equal('Resource Not Found')
+ ]);
+ });
+
+ it('returns 404 for missing ids', function () {
+ putRequest.url += 'test.NoProp/1';
+ const response = http.HTTP.handle(putRequest, responseData);
+ return Promise.all([
+ response.then(() => {
+ chai.expect(responseData.statusCode).to.equal(404);
+ }),
+ chai.expect(response).to.eventually.equal('Resource Not Found')
+ ]);
+ });
+
+ it('returns 200 for working ids and updates the data (existing)', function () {
+ const parse = (data: string) => JSON.parse(data);
+ putRequest.url += 'test.StringProp/43';
+ putRequest.on.withArgs('data').callsArgWith(1, '{"stringy":"asdasd"}');
+ const response = http.HTTP.handle(putRequest, responseData);
+ return Promise.all([
+ response.then(() => chai.expect(responseData.statusCode).to.equal(200)),
+ chai.expect(response.then(parse)).to.eventually.have.property('id', 43),
+ chai.expect(response.then(parse)).to.eventually.have.property('createdAt'),
+ chai.expect(response.then(parse)).to.eventually.have.property('updatedAt'),
+ response.then(() => chai.expect(object.save.called).to.be.true),
+ response.then(() =>
+ chai.expect(object.set.getCall(0).args).to.deep.equal(['stringy', 'asdasd'])
+ )
+ ]);
+ });
+
+ it('returns 200 for working ids and updates the data (new)', function () {
+ const parse = (data: string) => JSON.parse(data);
+ putRequest.url += 'test.StringProp';
+ putRequest.on.withArgs('data').callsArgWith(1, '{"stringy":"asdasd"}');
+ const response = http.HTTP.handle(putRequest, responseData);
+ return Promise.all([
+ response.then(() => chai.expect(responseData.statusCode).to.equal(200)),
+ chai.expect(response.then(parse)).to.eventually.have.property('id', 43),
+ chai.expect(response.then(parse)).to.eventually.have.property('createdAt'),
+ chai.expect(response.then(parse)).to.eventually.have.property('updatedAt'),
+ response.then(() => chai.expect(mockConnect.model.create.called).to.be.true),
+ response.then(() =>
+ chai.expect(mockConnect.model.create.getCall(0).args).to.deep.equal([{'stringy': 'asdasd'}])
+ )
+ ]);
+ });
+
+ it('errors on bad json', function () {
+ putRequest.url += 'test.StringProp/43';
+ putRequest.on.withArgs('data').callsArgWith(1, '{');
+ const response = http.HTTP.handle(putRequest, responseData);
+ return Promise.all([
+ response.then(() => {
+ chai.expect(responseData.statusCode).to.equal(412);
+ }),
+ chai.expect(response).to.eventually.equal('"Malformed JSON Details: Unexpected end of input"')
+ ]);
+ });
+
+ it('errors on failed save', function () {
+ putRequest.url += 'test.StringProp/43';
+ object.save.returns(Promise.reject(''));
+ const response = http.HTTP.handle(putRequest, responseData);
+ return Promise.all([
+ response.then(() => {
+ chai.expect(responseData.statusCode).to.equal(412);
+ }),
+ chai.expect(response).to.eventually.equal('"Save Failed"')
+ ]);
+ });
+
+ it('explodes on exceptions', function () {
+ putRequest.url += 'test.StringProp/43';
+ delete object.save;
+ const response = http.HTTP.handle(putRequest, responseData);
+ return Promise.all([
+ chai.expect(response).to.eventually.be.rejected
+ ]);
+ });
+ });
+
+ describe('post', function () {
+ const postRequest = {
+ method: 'POST',
+ on: sinon.stub(),
+ url: ''
+ };
+
+ beforeEach(function () {
+ postRequest.url = '/orm/';
+
+ postRequest.on.withArgs('data').callsArgWith(1, '{}');
+ postRequest.on.withArgs('end').callsArg(1);
+ });
+
+ it('searches', function () {
+ postRequest.url += 'test.StringProp';
+ postRequest.on.withArgs('data').callsArgWith(1, '{"stringy":"asdasd"}');
+ const response = http.HTTP.handle(postRequest, responseData);
+ return Promise.all([
+ response.then(() => {
+ chai.expect(responseData.statusCode).to.equal(200);
+ }),
+ chai.expect(response).to.eventually.equal('[]'),
+ response.then(() => chai.expect(mockConnect.model.findAll.called).to.be.true),
+ response.then(() =>
+ chai.expect(mockConnect.model.findAll.getCall(0).args).to.deep.equal([{
+ "include": [
+ {
+ "all": true
+ }
+ ],
+ "where": {
+ "stringy": "asdasd"
+ }
+ }])
+ )
+ ]);
+ });
+
+ it('errors when given a bad class', function () {
+ postRequest.url += 'badClass';
+
+ const response = http.HTTP.handle(postRequest, responseData);
+ return Promise.all([
+ response.then(() => {
+ chai.expect(responseData.statusCode).to.equal(404);
+ }),
+ chai.expect(response).to.eventually.equal('Resource Not Found')
+ ]);
+ });
+
+ it('errors on bad json', function () {
+ postRequest.url += 'test.StringProp';
+ postRequest.on.withArgs('data').callsArgWith(1, '{');
+ const response = http.HTTP.handle(postRequest, responseData);
+ return Promise.all([
+ response.then(() => {
+ chai.expect(responseData.statusCode).to.equal(412);
+ }),
+ chai.expect(response).to.eventually.equal('"Malformed JSON Details: Unexpected end of input"')
+ ]);
+ });
+
+ it('explodes on exceptions', function () {
+ postRequest.url += 'test.StringProp';
+ delete mockConnect.model.findAll;
+ const response = http.HTTP.handle(postRequest, responseData);
+ return Promise.all([
+ chai.expect(response).to.eventually.be.rejected
+ ]);
+ });
+ });
+
+ describe('delete', function () {
+ const deleteRequest = {
+ method: 'DELETE',
+ url: ''
+ };
+
+ beforeEach(function () {
+ deleteRequest.url = '/orm/';
+ });
+
+ it('errors when given a bad class', function () {
+ deleteRequest.url += 'badClass/0';
+
+ const response = http.HTTP.handle(deleteRequest, responseData);
+ return Promise.all([
+ response.then(() => {
+ chai.expect(responseData.statusCode).to.equal(404);
+ }),
+ chai.expect(response).to.eventually.equal('Resource Not Found')
+ ]);
+ });
+
+ it('returns 404 for missing ids', function () {
+ deleteRequest.url += 'test.NoProp/1';
+ const response = http.HTTP.handle(deleteRequest, responseData);
+ return Promise.all([
+ response.then(() => {
+ chai.expect(responseData.statusCode).to.equal(404);
+ }),
+ chai.expect(response).to.eventually.equal('"Resource Not Found"')
+ ]);
+ });
+
+ it('returns 200 for working ids', function () {
+ deleteRequest.url += 'test.NoProp/43';
+ const response = http.HTTP.handle(deleteRequest, responseData).then(
+ (data: string) => JSON.parse(data)
+ );
+ return Promise.all([
+ response.then(() => {
+ chai.expect(responseData.statusCode).to.equal(200);
+ })
+ ]);
+ });
+
+ it('handles failure', function () {
+ deleteRequest.url += 'test.NoProp/43';
+ object.destroy.returns(Promise.reject(''));
+ const response = http.HTTP.handle(deleteRequest, responseData).then(
+ (data: string) => JSON.parse(data)
+ );
+ return Promise.all([
+ response.then(() => {
+ chai.expect(responseData.statusCode).to.equal(412);
+ }),
+ chai.expect(response).to.eventually.equal('Delete Failed')
+ ]);
+ });
+ });
+
+ after(function() {
+ mockery.deregisterAll();
+ mockery.disable();
+ });
+});
diff --git a/node/http.ts b/node/http.ts
index fddb95c..e58a7af 100644
--- a/node/http.ts
+++ b/node/http.ts
@@ -1,6 +1,7 @@
import * as http from 'http';
-import { DataConnection, DataContract } from './DataObject';
+import { DataContract } from './DataContract';
+import { DataConnection } from './DataConnection';
import { registeredClasses } from '../shared/DataObject';
export class HTTP {
@@ -31,13 +32,13 @@ export class HTTP {
contract = HTTP.initialized[idx];
}
- if (requestData.method === 'GET' && id) {
+ if (requestData.method === 'GET' && id !== undefined) {
if (contract) {
return HTTP.GET(id, requestData, responseData, contract);
} else {
return HTTP.respondNotFound(responseData);
}
- } else if (requestData.method === 'POST' && !id) {
+ } else if (requestData.method === 'POST' && id === undefined) {
if (contract) {
return HTTP.POST(requestData, responseData, contract);
} else {
@@ -49,7 +50,7 @@ export class HTTP {
} else {
return HTTP.respondNotFound(responseData);
}
- } else if (requestData.method === 'DELETE' && id) {
+ } else if (requestData.method === 'DELETE' && id !== undefined) {
if (contract) {
return HTTP.DELETE(id, requestData, responseData, contract);
} else {
@@ -107,7 +108,7 @@ export class HTTP {
contract: DataConnection
): Promise {
let dataPromise: Promise;
- if (id) {
+ if (id !== undefined) {
dataPromise = contract.fetch(id);
} else {
dataPromise = new Promise((resolve) => {
diff --git a/node/index.ts b/node/index.ts
index fa12695..8ed8c45 100644
--- a/node/index.ts
+++ b/node/index.ts
@@ -1,4 +1,6 @@
export {field} from './field';
export {connect} from './connect';
-export {DataContract, DataConnection} from './DataObject';
+export {DataContract} from './DataContract';
+export {DataConnection} from './DataConnection';
export {register} from '../shared/DataObject';
+export {HTTP} from './http';
diff --git a/package.json b/package.json
index 68fd30f..0dff812 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,11 @@
"main": "src/index",
"scripts": {
"lint": "tslint -e \"node_modules/**\" -e \"typings/**\" -e \"**/*.d.ts\" \"**/*.ts\"",
+ "mocha": "./run_mocha.sh",
+ "coverage": "istanbul check-coverage",
+ "coveralls": "cat coverage/lcov.info | ./node_modules/.bin/coveralls",
"prepublish": "typings install && tsc",
- "test": "npm run lint"
+ "test": "npm run lint && npm run mocha && npm run coveralls && npm run coverage"
},
"repository": {
"type": "git",
@@ -24,7 +27,17 @@
"@angular/core": "^2.0.0-rc.4",
"@angular/http": "^2.0.0-rc.4",
"@angular/platform-browser": "^2.0.0-rc.4",
+ "chai": "^3.5.0",
+ "chai-as-promised": "^5.3.0",
+ "coveralls": "^2.11.9",
+ "istanbul": "^1.1.0-alpha.1",
+ "livereload": "^0.4.1",
+ "mocha": "^2.5.3",
+ "mockery": "^1.7.0",
+ "nodemon": "^1.9.2",
"rxjs": "^5.0.0-beta.6",
+ "sinon": "^1.17.4",
+ "source-map-support": "^0.4.1",
"tslint": "^3.12.1",
"typescript": "^1.8.10",
"typings": "^1.3.1",
@@ -35,6 +48,7 @@
"typings": "^1.3.1"
},
"dependencies": {
+ "browser-sync": "^2.13.0",
"sequelize": "^3.23.4",
"bunyan": "^1.8.1",
"es6-shim": "^0.35.1",
diff --git a/run_mocha.sh b/run_mocha.sh
new file mode 100755
index 0000000..b21ccd9
--- /dev/null
+++ b/run_mocha.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+./node_modules/.bin/istanbul cover \
+ -i './{,!(node_modules|coverage|webpack|mocks)/**/}/{,!(index|mochaInit|AuthHandler)*}.js' \
+ ./node_modules/.bin/_mocha \
+ -r node_modules/reflect-metadata \
+ -r ./mochaInit \
+ './{,!(node_modules)/**/}/*.spec.js'
\ No newline at end of file
diff --git a/shared/DataObject.ts b/shared/DataObject.ts
index 5de34f2..a8aab7d 100644
--- a/shared/DataObject.ts
+++ b/shared/DataObject.ts
@@ -5,7 +5,7 @@ export interface IDataContract {
createdAt: moment.Moment;
updatedAt: moment.Moment;
- save(): Promise;
+ save(): Promise;
delete(): Promise;
}
diff --git a/shared/field.ts b/shared/field.ts
index 08fb2dd..0c88a95 100644
--- a/shared/field.ts
+++ b/shared/field.ts
@@ -33,13 +33,12 @@ export function field(target: any, key: string, actions: IActions, type?: Types)
case 'Number':
type = Types.float;
break;
+ /* istanbul ignore next */
case 'Object':
- // tslint:disable-next-line:no-console
- console.error('Automatic mapping of Objects is unsupported');
- break;
+ throw new TypeError('Automatic mapping of Objects is unsupported');
+ /* istanbul ignore next */
default:
- // tslint:disable-next-line:no-console
- console.error('Unknown js type found', jsType.name);
+ throw new TypeError('Unknown js type found! ' + jsType.name);
}
}
Reflect.defineMetadata("ORM:type", type, target, key);
diff --git a/typings.json b/typings.json
index 16a55fb..42e2e9d 100644
--- a/typings.json
+++ b/typings.json
@@ -7,5 +7,14 @@
"dependencies": {
"lodash": "registry:npm/lodash#4.0.0+20160416211519",
"sequelize": "registry:npm/sequelize#3.0.0+20160604152601"
+ },
+ "devDependencies": {
+ "chai": "registry:npm/chai#3.5.0+20160415060238",
+ "chai-as-promised": "registry:npm/chai-as-promised#5.1.0+20160310030142",
+ "sinon": "registry:npm/sinon#1.16.0+20160427193336"
+ },
+ "globalDevDependencies": {
+ "mocha": "registry:env/mocha#2.2.5+20160321223601",
+ "mockery": "registry:dt/mockery#1.4.0+20160316155526"
}
}
diff --git a/watch_coverage.sh b/watch_coverage.sh
new file mode 100755
index 0000000..2def40f
--- /dev/null
+++ b/watch_coverage.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+./node_modules/.bin/nodemon -i coverage -x ./run_mocha.sh &
+nodemon=$!
+sleep 5s
+./node_modules/.bin/browser-sync \
+ start \
+ --no-inject-changes \
+ --logLevel info \
+ --no-open \
+ -f 'coverage/lcov-report' \
+ -s --ss 'coverage/lcov-report' | grep -v "File changed" &
+browsersync=$!
+
+wait $nodemon
+wait $browsersync
\ No newline at end of file