Skip to content

Commit

Permalink
adds onFieldUpdate, onFieldsUpdate to utils/model, and adds onModelUp…
Browse files Browse the repository at this point in the history
…date, and onCollectionUpdate to Collection
  • Loading branch information
JustinBeaudry committed Sep 1, 2018
1 parent f28c642 commit d9e3cdf
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 53 deletions.
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"dependencies": {
"enjoi-browser": "1.x.x",
"joi-browser": "13.x.x",
"lodash": "4.x.x"
"lodash": "4.x.x",
"rxjs": "^6.3.1"
},
"devDependencies": {
"ava": "^1.0.0-beta.7",
Expand Down
64 changes: 54 additions & 10 deletions src/Collection.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import _ from 'lodash';
import {throwError} from 'rxjs';
import {merge} from 'rxjs/operators';
import Serializable from './Serializable';
import Model from './Model';
import {MissingIndexError} from './errors';
import {toJSON, getType} from './utils';
import {MissingFieldError, MissingIndexError, MissingModelError, ModelExistsError, UnexpectedTypeError} from './errors';
import {toJSON, getType, onFieldsUpdate} from './utils';

/**
*
Expand Down Expand Up @@ -34,7 +36,8 @@ class Collection extends Serializable {
* assert(pokemonCollection.get(name).quote, quote) // true
*
* @param {Object|Array} data - data to create a collection with on construction. data can be passed after instantiation
* @param {Model} (model) - the Model to be used when adding data to a collection. A Collection will force all data into this Model if used.
* @param {Model|Function} (model) - The Model to be used when adding data to a collection.
* A Collection will force all data into this Model if used.
* @param {String} (indexBy) [id] - the field that the collection should index on
* @throws {Error}
*/
Expand Down Expand Up @@ -85,7 +88,7 @@ class Collection extends Serializable {
setFromObject.call(this, data);
break;
default:
throw new TypeError(`Expected data to be of type {Object|Array} and got ${typeof data}`);
throw new UnexpectedTypeError();
}
return this;
}
Expand All @@ -108,7 +111,7 @@ class Collection extends Serializable {
let convertedModel = convertToModel.call(this, model);
const key = convertedModel[this.indexBy];
if (this.index[key]) {
throw new Error('Model already exists in Collection index. Call update if you wish to update model');
throw new ModelExistsError();
}
this.index[key] = convertedModel;
return this;
Expand All @@ -129,7 +132,7 @@ class Collection extends Serializable {
remove(modelId) {
let model = this.get(modelId);
if (!model) {
throw new Error('Model does not exist in Collection index. Call add().');
throw new MissingModelError();
}
let convertedModel = convertToModel.call(this, model);
const key = convertedModel[this.indexBy];
Expand All @@ -156,10 +159,13 @@ class Collection extends Serializable {
update(model) {
let convertedModel = convertToModel.call(this, model);
const key = convertedModel[this.indexBy];
if (!this.index[key]) {
throw new Error('Model to update does not exist in Collection index');
if (!key || !this.get(key)) {
throw new MissingIndexError();
}
this.index[key] = convertedModel;
// if we have a metamon Model instance, use `model.set()` so we can validate the schema
this.index[key] = (convertedModel instanceof Model)
? this.get(key).set(model)
: convertedModel;
return this;
}
/**
Expand Down Expand Up @@ -221,11 +227,49 @@ class Collection extends Serializable {
*
* converts index to an array and stringifies
*
* @param {Boolean} asIndex [false] - return the index as JSON
* @returns {String}
*/
toJSON() {
toJSON(asIndex=false) {
if (asIndex) {
return toJSON(this.index);
}
return toJSON(this.toArray());
}
onCollectionUpdate() {
return merge(
this.toArray().map(model => {
if (model instanceof Model) {
return model.onFieldsUpdate().pipe(
map(value => {
return {
[model[this.indexBy]]: value
}
})
)
}
return onFieldsUpdate
.call(model, Object.keys(model))
.pipe(map(value => {
return {
[model[this.indexBy]]: value
}
}))
})
);
}
onModelUpdate(id) {
const model = this.get(id);
if (!model) {
return throwError(new MissingModelError());
}
// if the model is an instance of model, just use the Model method
if (model instanceof Model) {
return model.onFieldsUpdate();
}
// if the model is a plain object just use onFieldsUpdate util
return onFieldsUpdate.call(model, Object.keys(model));
}
}
/**
*
Expand Down
34 changes: 32 additions & 2 deletions src/Model.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import Serializable from './Serializable';
import {pick, omit} from 'lodash';
import Enjoi from 'enjoi-browser/lib/enjoi';
import Serializable from './Serializable';
import {onFieldsUpdate, onFieldUpdate} from './utils';

const MODEL_METHODS = [
'set',
'addView',
'getView',
'onFieldUpdate',
'onFieldsUpdate'
];

const _defaults = Symbol('defaults');
const _schema = Symbol('schema');
Expand Down Expand Up @@ -127,7 +136,7 @@ class Model extends Serializable {
}
});
}
this.set(data);
return this.set(data);
}
/**
*
Expand All @@ -153,6 +162,7 @@ class Model extends Serializable {
Object.assign(this, this[_defaults], data);
}
Object.assign(this, data);
return this;
}
/**
*
Expand All @@ -175,6 +185,7 @@ class Model extends Serializable {
whitelist: isWhitelist,
fields: fields
};
return this;
}
/**
*
Expand All @@ -194,5 +205,24 @@ class Model extends Serializable {
return omit(this, view.fields);
}
}
/**
*
* @param {String} field
* @returns {Observable<any>}
*/
onFieldUpdate(field) {
onFieldUpdate.call(this, field);
}
/**
*
* @param {Array<String>} (fields) - defaults to all fields on model
* @returns {Observable<any>}
*/
onFieldsUpdate(fields) {
if (!fields) {
fields = Object.keys(omit(this.toObject(), MODEL_METHODS));
}
onFieldsUpdate.call(this, fields);
}
}
export default Model;
26 changes: 26 additions & 0 deletions src/errors.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,38 @@
import ErrorFactory from './ErrorFactory';

/**
*
* @class UnexpectedTypeError
* @type {Error|Function}
*/
export const UnexpectedTypeError = ErrorFactory(data => `Expected data to be of type {Object|Array} and got ${typeof data}`);
/**
*
* @class UnsupportedError
* @type {Error|Function}
*/
export const UnsupportedError = ErrorFactory((model, indexBy) => `Model ${model.toJSON()} is missing index ${indexBy}`);
/**
*
* @class MissingIndexError
* @type {Error|Function}
*/
export const MissingIndexError = ErrorFactory(value => `Unsupported value for enum value: ${value}`);
/**
*
* @class MissingModelError
* @type {Error|Function}
*/
export const MissingModelError = ErrorFactory('Model does not exist in collection. call add() to add a model to the index.');
/**
*
* @class ModelExistsError
* @type {Error|Function}
*/
export const ModelExistsError = ErrorFactory('Model already exists in collection index. call update() if you wish to update the model');
/**
*
* @class MissingFieldError
* @type {Error|Function}
*/
export const MissingFieldError = ErrorFactory(field => `Requested field ${field} does not exist on model`);
40 changes: 40 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import {Observable} from 'rxjs';
import {map, merge} from 'rxjs/operators';
import _ from 'lodash';
import {MissingFieldError} from './errors';
/**
*
* returns the current time as a unix timestamp
Expand Down Expand Up @@ -38,3 +41,40 @@ export function getType(values) {
return typeof values;
}
}
/**
*
* @note Must be bound to be used
*
* @param {String} field
* @returns {Observable<any>}
*/
export function onFieldUpdate(field) {
return new Observable(observer => {
if (!this[field]) {
observer.error(new MissingFieldError());
}
const revoke = Proxy.revocable(this[field], {
set: value => {
observer.next(value);
this[field] = value;
}
});
return () => revoke();
});
}

/**
*
* @param {Array<String>} fields
* @returns {MonoTypeOperatorFunction<any> | OperatorFunction<any, any>}
*/
export function onFieldsUpdate(fields) {
return merge(
fields.map(field => {
return this.onFieldUpdate.call(this, field).pipe(map(value => {
return {
[field]: value
}
}));
}));
}
3 changes: 0 additions & 3 deletions tests/collection.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ test.beforeEach(t => {
t.context.field3 = 'field3';
});

test('should throw an error if no model is supplied', t => {
t.throws(() => new Collection());
});
test('should throw an MissingIndexError if an Object TestModel is missing the indexed field', t => {
t.throws(() => new Collection({}, TestModel, 'name'));
});
Expand Down
30 changes: 6 additions & 24 deletions tests/errorfactory.spec.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,7 @@
import test from 'ava';
import sinon from 'sinon';
import faker from 'faker';
import ErrorFactory from '../src/ErrorFactory';

const captureSpy = sinon.spy();

class CaptureStackError extends Error {
constructor() {
super();
}
static captureStackTrace() {
return captureSpy();
}
}

class NoCaptureStackError {}

test('ErrorFactory() should return an Error Constructor', t => {
const text = faker.random.word();
const ErrorConstructor = ErrorFactory();
Expand All @@ -26,7 +12,7 @@ test('ErrorFactory() should return an Error Constructor', t => {
t.is(CustomError.message, text);
});

test('ErrorFactory() should have a baked error message if passed into its constructor', t => {
test('ErrorFactory() should have a baked error message if string message passed into its constructor', t => {
const text = faker.random.word();
const ErrorConstructor = ErrorFactory(text);
const CustomError = new ErrorConstructor();
Expand All @@ -35,13 +21,9 @@ test('ErrorFactory() should have a baked error message if passed into its constr
t.is(CustomError.message, text);
});

test('ErrorFactory() should call captureStackTrace and capture the constructors stack frames', t => {
const ErrorConstructor = ErrorFactory(null, CaptureStackError);
new ErrorConstructor();
t.true(captureSpy.called);
test('ErrorFactory() should have a baked error message if function message passed into its constructor', t => {
const text = faker.random.word();
const ErrorConstructor = ErrorFactory(value => `${value}`);
const CustomError = new ErrorConstructor(text);``
t.is(CustomError.message, text);
});

test('ErrorFactory() should not call captureStackTrace if not present on constructor', t => {
const ErrorConstructor = ErrorFactory(null, NoCaptureStackError);
t.not(typeof ErrorConstructor.captureStackTrace, 'function');
});
Loading

0 comments on commit d9e3cdf

Please sign in to comment.