Skip to content

Commit

Permalink
feat(Mapper): Add hooks for validation, get (see comments), add bette…
Browse files Browse the repository at this point in the history
…r testing for hooks
  • Loading branch information
bdfoster committed Nov 17, 2017
1 parent b982f70 commit 0135789
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 21 deletions.
13 changes: 8 additions & 5 deletions src/Container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {EventEmitter} from 'nomatic-events';
import {Adapter} from './adapters/index';
import AlreadyExistsError from './errors/AlreadyExistsError';
import ValidationError from './errors/ValidationError';
import Mapper, {MapperHookFunction, MapperOptions} from './Mapper';
import Mapper, {MapperHookFunction, MapperOptions, MapperValidateHookFunction} from './Mapper';
import Query from './Query';
import Record, {RecordData, RecordVirtualProperties} from './Record';

Expand All @@ -13,10 +13,13 @@ export interface ContainerMapperOptions {
required?: string[];
additionalProperties?: boolean | object;
virtuals?: RecordVirtualProperties;
afterGet?: MapperHookFunction | MapperHookFunction[];
afterInsert?: MapperHookFunction | MapperHookFunction[];
afterUpdate?: MapperHookFunction | MapperHookFunction[];
afterValidate?: MapperValidateHookFunction | MapperValidateHookFunction[];
beforeInsert?: MapperHookFunction | MapperHookFunction[];
beforeUpdate?: MapperHookFunction | MapperHookFunction[];
beforeValidate?: MapperValidateHookFunction | MapperValidateHookFunction[];
}

export interface ContainerMappers {
Expand All @@ -26,10 +29,14 @@ export interface ContainerMappers {

export interface ContainerOptions {
adapter: Adapter;
afterGet?: MapperHookFunction | MapperHookFunction[];
afterInsert?: MapperHookFunction | MapperHookFunction[];
afterUpdate?: MapperHookFunction | MapperHookFunction[];
afterValidate?: MapperValidateHookFunction | MapperValidateHookFunction[];
beforeInsert?: MapperHookFunction | MapperHookFunction[];
beforeUpdate?: MapperHookFunction | MapperHookFunction[];
beforeValidate?: MapperValidateHookFunction | MapperValidateHookFunction[];

mappers: {
[key: string]: ContainerMapperOptions;
};
Expand Down Expand Up @@ -130,10 +137,6 @@ export class Container extends EventEmitter {

const mapperOptions: MapperOptions = {
adapter: this.adapter,
afterInsert: options.afterInsert,
afterUpdate: options.afterUpdate,
beforeInsert: options.beforeInsert,
beforeUpdate: options.beforeUpdate,
name: name,
validate: validateRunner,
virtuals: options.virtuals
Expand Down
57 changes: 49 additions & 8 deletions src/Mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,34 @@ import Query from './Query';
import {Record, RecordData, RecordOptions, RecordValidateFunction, RecordVirtualProperties} from './Record';

export type MapperHookFunction = (record: Record) => void;
export type MapperBeforeGetHookFunction = (id: string) => void;
export type MapperValidateHookFunction = (record: Record, operation: 'insert' | 'replace' | 'update') => void;

export interface MapperOptions {
adapter: Adapter;
afterGet?: MapperHookFunction | MapperHookFunction[];
afterInsert?: MapperHookFunction | MapperHookFunction[];
afterUpdate?: MapperHookFunction | MapperHookFunction[];
afterValidate?: MapperValidateHookFunction | MapperValidateHookFunction[];
beforeGet?: MapperBeforeGetHookFunction | MapperBeforeGetHookFunction[];
beforeInsert?: MapperHookFunction | MapperHookFunction[];
beforeUpdate?: MapperHookFunction | MapperHookFunction[];
beforeValidate?: MapperValidateHookFunction | MapperValidateHookFunction[];
name: string;
validate?: RecordValidateFunction;
virtuals?: RecordVirtualProperties;
}

export class Mapper extends EventEmitter {
static hooksList: string[] = [
'afterGet',
'afterInsert',
'afterUpdate',
'afterValidate',
'beforeGet',
'beforeInsert',
'beforeUpdate'
'beforeUpdate',
'beforeValidate'
];

private _validate: RecordValidateFunction;
Expand All @@ -33,6 +43,21 @@ export class Mapper extends EventEmitter {
public name: string;
public readonly collection: string;

/**
* Hooks ordering:
* Operation(s) Order
* --------------------------------------------------------------------------------------------------
* find, findAll afterGet
* get, getAll beforeGet, afterGet
* insert,insertAll beforeValidate, afterValidate, beforeInsert, afterInsert, afterGet
* replace, update beforeValidate, afterValidate, beforeUpdate, afterUpdate, afterGet
* --------------------------------------------------------------------------------------------------
*
* Hooks are called on each record, so getAll, findAll, and insertAll operations will call each hook for each
* record.
*
* @param {MapperOptions} options
*/
constructor(options: MapperOptions) {
super();
this.adapter = options.adapter;
Expand All @@ -54,6 +79,14 @@ export class Mapper extends EventEmitter {
}
}

private validate(record: Record, operation: string) {
this.emit('beforeValidate', record, operation);
return record.validate().then(() => {
this.emit('afterValidate', record, operation);
return;
});
}

public createRecord(data: RecordData = {}): Record {
const options: RecordOptions = {
validate: this._validate,
Expand All @@ -65,8 +98,11 @@ export class Mapper extends EventEmitter {
}

public async get(id: string): Promise<Record> {
this.emit('beforeGet', id);
const response = await this.adapter.get(this.collection, id);
return this.createRecord(response);
const record = this.createRecord(response);
this.emit('afterGet', record);
return record;
}

public async getAll(ids: string[]) {
Expand Down Expand Up @@ -98,7 +134,9 @@ export class Mapper extends EventEmitter {
const response = await this.adapter.findAll(this.collection, q);

for (const i in response) {
results.push(this.createRecord(response[i]));
const record = this.createRecord(response[i]);
this.emit('afterGet', record);
results.push(record);
}

return results;
Expand All @@ -117,7 +155,7 @@ export class Mapper extends EventEmitter {

if (validate) {
try {
await record.validate();
await this.validate(record, 'update');
} catch (error) {
throw error;
}
Expand All @@ -128,12 +166,13 @@ export class Mapper extends EventEmitter {
const result = await this.adapter.update(this.collection, record.id, record.serialize('save'));
record.commit(result);
this.emit('afterUpdate', record);
this.emit('afterGet', record);
return record;
}

public async save(record: Record, validate: boolean = true): Promise<Record> {
public async save(record: Record, validate: boolean = true, force: boolean = false): Promise<Record> {
if (record.id && record.rev) {
if (record.changes().length === 0) {
if (record.changes().length === 0 && !force) {
return this.get(record.id).then((response) => {
if (response.rev === record.rev) {
return record;
Expand Down Expand Up @@ -175,7 +214,7 @@ export class Mapper extends EventEmitter {

if (validate) {
try {
await record.validate();
await this.validate(record, 'replace');
} catch (error) {
throw error;
}
Expand All @@ -186,6 +225,7 @@ export class Mapper extends EventEmitter {
return await this.adapter.replace(this.collection, id, record.serialize('save'), rev).then((data) => {
record.commit(data);
this.emit('afterUpdate', record);
this.emit('afterGet', record);
return record;
});
}
Expand All @@ -201,7 +241,7 @@ export class Mapper extends EventEmitter {

if (validate) {
try {
await record.validate();
await this.validate(record, 'insert');
} catch (error) {
throw error;
}
Expand All @@ -212,6 +252,7 @@ export class Mapper extends EventEmitter {
return await this.adapter.insert(this.collection, record.serialize('save')).then((result) => {
record.init(result);
this.emit('afterInsert', record);
this.emit('afterGet', record);
return record;
});
}
Expand Down
17 changes: 11 additions & 6 deletions src/Record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,6 @@ export class Record extends EventEmitter {
if (typeof key !== 'string') key = key.toString();
if (this._virtuals.has(<string>key)) return this._virtuals.get(<string>key).get.apply(this.proxy());



return get(this._data, key);
}

Expand All @@ -303,8 +301,9 @@ export class Record extends EventEmitter {
*
* @param key
* @param value
* @param silent
*/
public set(key, value) {
public set(key, value, silent: boolean = false) {
if (typeof key !== 'string') key = key.toString();
let isVirtual = false;

Expand Down Expand Up @@ -373,18 +372,21 @@ export class Record extends EventEmitter {

set(this._data, key, value);

this._changes.push(change);
if (!silent) {
this._changes.push(change);
}
}

/**
* Essentially deletes a property at `key` under `_data`. Uses of this method will have a corresponding entry in
* `_changes` so long as a value exists. Virtual properties cannot be unset.
*
* @param key
* @param silent
* @returns {boolean} If true, unset was successful. If false, there's nothing set at `key` under `_data` or it is
* a virtual property.
*/
public unset(key) {
public unset(key, silent: boolean = false) {
if (!isNullOrUndefined(get(this._virtuals, key))) return false;
const operation: RecordOperation = 'remove';
let old = this.get(key);
Expand All @@ -403,7 +405,10 @@ export class Record extends EventEmitter {
};

if (unset(this._data, key)) {
this._changes.push(change);
if (!silent) {
this._changes.push(change);
}

return true;
}

Expand Down
11 changes: 10 additions & 1 deletion test/Container.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {inspect} from 'util';

describe('Container', () => {
const config = require('./fixtures/config/' + process.env.NODE_ENV + '.json')['arangodb'];
const mock = require('./fixtures/mock.json');
let adapter;
let instance;
let gets = 0;

before((done) => {
adapter = new ArangoDBAdapter(config);
Expand All @@ -26,6 +26,9 @@ describe('Container', () => {

instance = new Container({
adapter: adapter,
afterGet(record) {
gets++;
},
beforeInsert(record) {
record.createdAt = new Date();
},
Expand Down Expand Up @@ -196,8 +199,10 @@ describe('Container', () => {

describe('#findAll', () => {
it('should return all results', (done) => {
const startGets = gets;
instance.findAll('people', {}).then((results) => {
expect(results.length).to.equal(people.length);
expect(gets).to.equal(startGets + results.length);
}).then(done, done);
});

Expand Down Expand Up @@ -234,19 +239,23 @@ describe('Container', () => {

describe('#get()', () => {
it('should return a saved Record instance', (done) => {
const startGets = gets;
instance.get('people', people[0]['id']).then((result) => {
expect(result.id).to.equal(people[0]['id']);
expect(result.name).to.equal(`${people[0].firstName} ${people[0].lastName}`);
expect(gets).to.equal(startGets + 1);
}).then(done, done);
});
});

describe('#getAll()', () => {
it('should return an array of saved Record instances', (done) => {
const startGets = gets;
instance.getAll('people', [people[0]['id'], people[1]['id']]).then((results) => {
expect(results.length).to.equal(2);
expect(results[0].id).to.equal(people[0]['id']);
expect(results[1].id).to.equal(people[1]['id']);
expect(gets).to.equal(startGets + 2);
}).then(done, done);
});
});
Expand Down
Loading

0 comments on commit 0135789

Please sign in to comment.