@bigtest/mirage
Version:
A client-side server to develop, test and prototype your app.
340 lines (289 loc) • 8.17 kB
JavaScript
import _assign from 'lodash/assign';
import _map from 'lodash/map';
import _isEqual from 'lodash/isEqual';
import _sortBy from 'lodash/sortBy';
function duplicate(data) {
if (Array.isArray(data)) {
return data.map(duplicate);
} else {
return _assign({}, data);
}
}
/**
* A collection of db records i.e. a database table.
* @class DbCollection
* @constructor
* @public
*/
class DbCollection {
constructor(name, initialData, IdentityManager) {
this.name = name;
this._records = [];
this.identityManager = new IdentityManager();
if (initialData) {
this.insert(initialData);
}
}
/**
* Returns a copy of the data, to prevent inadvertent data manipulation.
* @method all
* @public
*/
all() {
return duplicate(this._records);
}
/**
* Inserts `data` into the collection. `data` can be a single object
* or an array of objects. Returns the inserted record.
* @method insert
* @param data
* @public
*/
insert(data) {
if (!Array.isArray(data)) {
return this._insertRecord(data);
} else {
// Need to sort in order to ensure IDs inserted in the correct order
let sorted = _sortBy(data, 'id');
return _map(sorted, (x) => this._insertRecord(x));
}
}
/**
* Returns a single record from the `collection` if `ids` is a single
* id, or an array of records if `ids` is an array of ids. Note
* each id can be an int or a string, but integer ids as strings
* (e.g. the string “1”) will be treated as integers.
* @method find
* @param ids
* @public
*/
find(ids) {
if (Array.isArray(ids)) {
let records = this._findRecords(ids)
.filter(Boolean)
.map(duplicate); // Return a copy
return records;
} else {
let record = this._findRecord(ids);
if (!record) {
return null;
}
// Return a copy
return duplicate(record);
}
}
/**
* Returns the first model from `collection` that matches the
* key-value pairs in the `query` object. Note that a string
* comparison is used. `query` is a POJO.
* @method find
* @param query
* @public
*/
findBy(query) {
let record = this._findRecordBy(query);
if (!record) {
return null;
}
// Return a copy
return duplicate(record);
}
/**
* Returns an array of models from `collection` that match the
* key-value pairs in the `query` object. Note that a string
* comparison is used. `query` is a POJO.
* @method where
* @param query
* @public
*/
where(query) {
return this._findRecordsWhere(query).map(duplicate);
}
/**
* Finds the first record matching the provided query in
* `collection`, or creates a new record using a merge of the
* `query` and optional `attributesForCreate`.
* @method firstOrCreate
* @param query
* @param attributesForCreate
* @public
*/
firstOrCreate(query, attributesForCreate = {}) {
let queryResult = this.where(query);
let [record] = queryResult;
if (record) {
return record;
} else {
let mergedAttributes = _assign(attributesForCreate, query);
let createdRecord = this.insert(mergedAttributes);
return createdRecord;
}
}
/**
* Updates one or more records in collection.
* If attrs is the only arg present, updates all records
* in the collection according to the key-value pairs in attrs.
* If target is present, restricts updates to those that
* match target. If target is a number or string, finds a
* single record whose id is target to update. If target is
* a POJO, queries collection for records that match the
* key-value pairs in target, and updates their attrs.
* Returns the updated record or records.
* @method update
* @param target
* @param attrs
* @public
*/
update(target, attrs) {
let records;
if (typeof attrs === 'undefined') {
attrs = target;
let changedRecords = [];
this._records.forEach((record) => {
let oldRecord = _assign({}, record);
this._updateRecord(record, attrs);
if (!_isEqual(oldRecord, record)) {
changedRecords.push(record);
}
});
return changedRecords;
} else if (typeof target === 'number' || typeof target === 'string') {
let id = target;
let record = this._findRecord(id);
this._updateRecord(record, attrs);
return record;
} else if (Array.isArray(target)) {
let ids = target;
records = this._findRecords(ids);
records.forEach((record) => {
this._updateRecord(record, attrs);
});
return records;
} else if (typeof target === 'object') {
let query = target;
records = this._findRecordsWhere(query);
records.forEach((record) => {
this._updateRecord(record, attrs);
});
return records;
}
}
/**
* Removes one or more records in `collection`.
* If `target` is undefined, removes all records.
* If `target` is a number or string, removes a
* single record using `target` as id. If `target` is
* a POJO, queries `collection` for records that
* match the key-value pairs in `target`, and
* removes them from the collection.
* @method remove
* @param target
* @public
*/
remove(target) {
let records;
if (typeof target === 'undefined') {
this._records = [];
this.identityManager.reset();
} else if (typeof target === 'number' || typeof target === 'string') {
let record = this._findRecord(target);
let index = this._records.indexOf(record);
this._records.splice(index, 1);
} else if (Array.isArray(target)) {
records = this._findRecords(target);
records.forEach((record) => {
let index = this._records.indexOf(record);
this._records.splice(index, 1);
});
} else if (typeof target === 'object') {
records = this._findRecordsWhere(target);
records.forEach((record) => {
let index = this._records.indexOf(record);
this._records.splice(index, 1);
});
}
}
/*
Private methods.
These return the actual db objects, whereas the public
API query methods return copies.
*/
/**
* @method _findRecord
* @param id
* @private
*/
_findRecord(id) {
id = id.toString();
let [record] = this._records.filter((obj) => obj.id === id);
return record;
}
/**
* @method _findRecordBy
* @param query
* @private
*/
_findRecordBy(query) {
return this._findRecordsWhere(query)[0];
}
/**
* @method _findRecords
* @param ids
* @private
*/
_findRecords(ids) {
return ids.map(this._findRecord, this);
}
/**
* @method _findRecordsWhere
* @param query
* @private
*/
_findRecordsWhere(query) {
let records = this._records;
function defaultQueryFunction(record) {
let keys = Object.keys(query);
return keys.every(function(key) {
return String(record[key]) === String(query[key]);
});
}
let queryFunction = typeof query === 'object' ? defaultQueryFunction : query;
return records.filter(queryFunction);
}
/**
* @method _insertRecord
* @param data
* @private
*/
_insertRecord(data) {
let attrs = duplicate(data);
if (attrs && (attrs.id === undefined || attrs.id === null)) {
attrs.id = this.identityManager.fetch(attrs);
} else {
attrs.id = attrs.id.toString();
this.identityManager.set(attrs.id);
}
this._records.push(attrs);
return duplicate(attrs);
}
/**
* @method _updateRecord
* @param record
* @param attrs
* @private
*/
_updateRecord(record, attrs) {
let targetId = (attrs && attrs.hasOwnProperty('id')) ? attrs.id.toString() : null;
let currentId = record.id;
if (targetId && currentId !== targetId) {
throw new Error('Updating the ID of a record is not permitted');
}
for (let attr in attrs) {
if (attr === 'id') {
continue;
}
record[attr] = attrs[attr];
}
}
}
export default DbCollection;