0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Refactored Bookshelf CRUD functions into plugin

no issue

- we're going to pull this out into the framework monorepo but
  refactoring it here first makes it a lot easier to extract without
  losing the history
This commit is contained in:
Daniel Lockyer 2021-06-15 17:54:13 +01:00
parent 35e51e364b
commit 97c0c93959
5 changed files with 492 additions and 477 deletions

View file

@ -0,0 +1,219 @@
const _ = require('lodash');
const errors = require('@tryghost/errors');
/**
* @param {Bookshelf} Bookshelf
*/
module.exports = function (Bookshelf) {
Bookshelf.Model = Bookshelf.Model.extend({}, {
/**
* ### Find All
* Fetches all the data for a particular model
* @param {Object} unfilteredOptions (optional)
* @return {Promise<Bookshelf['Collection']>} Collection of all Models
*/
findAll: async function findAll(unfilteredOptions) {
const options = this.filterOptions(unfilteredOptions, 'findAll');
const itemCollection = this.getFilteredCollection(options);
// @TODO: we can't use order raw when running migrations (see https://github.com/tgriesser/knex/issues/2763)
if (this.orderDefaultRaw && !options.migrating) {
itemCollection.query((qb) => {
qb.orderByRaw(this.orderDefaultRaw(options));
});
}
const result = await itemCollection.fetchAll(options);
if (options.withRelated) {
_.each(result.models, function each(item) {
item.withRelated = options.withRelated;
});
}
return result;
},
/**
* ### Find Page
* Find results by page - returns an object containing the
* information about the request (page, limit), along with the
* info needed for pagination (pages, total).
*
* **response:**
*
* {
* data: [
* {...}, ...
* ],
* meta: {
* pagination: {
* page: __,
* limit: __,
* pages: __,
* total: __
* }
* }
* }
*
* @param {Object} unfilteredOptions
*/
findPage: async function findPage(unfilteredOptions) {
const options = this.filterOptions(unfilteredOptions, 'findPage');
const itemCollection = this.getFilteredCollection(options);
const requestedColumns = options.columns;
// Set this to true or pass ?debug=true as an API option to get output
itemCollection.debug = unfilteredOptions.debug && process.env.NODE_ENV !== 'production';
// Ensure only valid fields/columns are added to query
// and append default columns to fetch
if (options.columns) {
options.columns = _.intersection(options.columns, this.prototype.permittedAttributes());
options.columns = _.union(options.columns, this.prototype.defaultColumnsToFetch());
}
if (options.order) {
const {order, orderRaw, eagerLoad} = itemCollection.parseOrderOption(options.order, options.withRelated);
options.orderRaw = orderRaw;
options.order = order;
options.eagerLoad = eagerLoad;
} else if (options.autoOrder) {
options.orderRaw = options.autoOrder;
} else if (this.orderDefaultRaw) {
options.orderRaw = this.orderDefaultRaw(options);
} else if (this.orderDefaultOptions) {
options.order = this.orderDefaultOptions();
}
const response = await itemCollection.fetchPage(options);
// Attributes are being filtered here, so they are not leaked into calling layer
// where models are serialized to json and do not do more filtering.
// Re-add and pick any computed properties that were stripped before fetchPage call.
const data = response.collection.models.map((model) => {
if (requestedColumns) {
model.attributes = _.pick(model.attributes, requestedColumns);
model._previousAttributes = _.pick(model._previousAttributes, requestedColumns);
}
return model;
});
return {
data: data,
meta: {pagination: response.pagination}
};
},
/**
* ### Find One
* Naive find one where data determines what to match on
* @param {Object} data
* @param {Object} unfilteredOptions (optional)
* @return {Promise<Bookshelf['Model']>} Single Model
*/
findOne: function findOne(data, unfilteredOptions) {
const options = this.filterOptions(unfilteredOptions, 'findOne');
data = this.filterData(data);
const model = this.forge(data);
// @NOTE: The API layer decides if this option is allowed
if (options.filter) {
model.applyDefaultAndCustomFilters(options);
}
// Ensure only valid fields/columns are added to query
if (options.columns) {
options.columns = _.intersection(options.columns, this.prototype.permittedAttributes());
}
return model.fetch(options);
},
/**
* ### Edit
* Naive edit
*
* We always forward the `method` option to Bookshelf, see http://bookshelfjs.org/#Model-instance-save.
* Based on the `method` option Bookshelf and Ghost can determine if a query is an insert or an update.
*
* @param {Object} data
* @param {Object} unfilteredOptions (optional)
* @return {Promise<Bookshelf['Model']>} Edited Model
*/
edit: async function edit(data, unfilteredOptions) {
const options = this.filterOptions(unfilteredOptions, 'edit');
const id = options.id;
const model = this.forge({id: id});
data = this.filterData(data);
// @NOTE: The API layer decides if this option is allowed
if (options.filter) {
model.applyDefaultAndCustomFilters(options);
}
// We allow you to disable timestamps when run migration, so that the posts `updated_at` value is the same
if (options.importing) {
model.hasTimestamps = false;
}
const object = await model.fetch(options);
if (object) {
options.method = 'update';
return object.save(data, options);
}
throw new errors.NotFoundError();
},
/**
* ### Add
* Naive add
* @param {Object} data
* @param {Object} unfilteredOptions (optional)
* @return {Promise<Bookshelf['Model']>} Newly Added Model
*/
add: function add(data, unfilteredOptions) {
const options = this.filterOptions(unfilteredOptions, 'add');
let model;
data = this.filterData(data);
model = this.forge(data);
// We allow you to disable timestamps when importing posts so that the new posts `updated_at` value is the same
// as the import json blob. More details refer to https://github.com/TryGhost/Ghost/issues/1696
if (options.importing) {
model.hasTimestamps = false;
}
// Bookshelf determines whether an operation is an update or an insert based on the id
// Ghost auto-generates Object id's, so we need to tell Bookshelf here that we are inserting data
options.method = 'insert';
return model.save(null, options);
},
/**
* ### Destroy
* Naive destroy
* @param {Object} unfilteredOptions (optional)
* @return {Promise<Bookshelf['Model']>} Empty Model
*/
destroy: async function destroy(unfilteredOptions) {
const options = this.filterOptions(unfilteredOptions, 'destroy');
if (!options.destroyBy) {
options.destroyBy = {
id: options.id
};
}
// Fetch the object before destroying it, so that the changed data is available to events
const obj = await this.forge(options.destroyBy).fetch(options);
return obj.destroy(options);
}
});
};
/**
* @typedef {import('bookshelf')} Bookshelf
*/

View file

@ -13,7 +13,6 @@ const moment = require('moment');
const Promise = require('bluebird'); const Promise = require('bluebird');
const ObjectId = require('bson-objectid'); const ObjectId = require('bson-objectid');
const debug = require('@tryghost/debug')('models:base'); const debug = require('@tryghost/debug')('models:base');
const config = require('../../../shared/config');
const db = require('../../data/db'); const db = require('../../data/db');
const events = require('../../lib/common/events'); const events = require('../../lib/common/events');
const logging = require('@tryghost/logging'); const logging = require('@tryghost/logging');
@ -69,6 +68,8 @@ ghostBookshelf.plugin(plugins.collision);
// Load hasPosts plugin for authors models // Load hasPosts plugin for authors models
ghostBookshelf.plugin(plugins.hasPosts); ghostBookshelf.plugin(plugins.hasPosts);
ghostBookshelf.plugin(require('./crud'));
// Manages nested updates (relationships) // Manages nested updates (relationships)
ghostBookshelf.plugin('bookshelf-relations', { ghostBookshelf.plugin('bookshelf-relations', {
allowedOptions: ['context', 'importing', 'migrating'], allowedOptions: ['context', 'importing', 'migrating'],
@ -908,228 +909,12 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
return filteredCollectionQuery; return filteredCollectionQuery;
}, },
/**
* ### Find All
* Fetches all the data for a particular model
* @param {Object} unfilteredOptions (optional)
* @return {Promise<ghostBookshelf.Collection>} Collection of all Models
*/
findAll: function findAll(unfilteredOptions) {
const options = this.filterOptions(unfilteredOptions, 'findAll');
const itemCollection = this.getFilteredCollection(options);
// @TODO: we can't use order raw when running migrations (see https://github.com/tgriesser/knex/issues/2763)
if (this.orderDefaultRaw && !options.migrating) {
itemCollection.query((qb) => {
qb.orderByRaw(this.orderDefaultRaw(options));
});
}
return itemCollection.fetchAll(options).then(function then(result) {
if (options.withRelated) {
_.each(result.models, function each(item) {
item.withRelated = options.withRelated;
});
}
return result;
});
},
/**
* ### Find Page
* Find results by page - returns an object containing the
* information about the request (page, limit), along with the
* info needed for pagination (pages, total).
*
* **response:**
*
* {
* data: [
* {...}, ...
* ],
* meta: {
* pagination: {
* page: __,
* limit: __,
* pages: __,
* total: __
* }
* }
* }
*
* @param {Object} unfilteredOptions
*/
findPage: function findPage(unfilteredOptions) {
const options = this.filterOptions(unfilteredOptions, 'findPage');
const itemCollection = this.getFilteredCollection(options);
const requestedColumns = options.columns;
// Set this to true or pass ?debug=true as an API option to get output
itemCollection.debug = unfilteredOptions.debug && config.get('env') !== 'production';
// Ensure only valid fields/columns are added to query
// and append default columns to fetch
if (options.columns) {
options.columns = _.intersection(options.columns, this.prototype.permittedAttributes());
options.columns = _.union(options.columns, this.prototype.defaultColumnsToFetch());
}
if (options.order) {
const {order, orderRaw, eagerLoad} = itemCollection.parseOrderOption(options.order, options.withRelated);
options.orderRaw = orderRaw;
options.order = order;
options.eagerLoad = eagerLoad;
} else if (options.autoOrder) {
options.orderRaw = options.autoOrder;
} else if (this.orderDefaultRaw) {
options.orderRaw = this.orderDefaultRaw(options);
} else if (this.orderDefaultOptions) {
options.order = this.orderDefaultOptions();
}
return itemCollection.fetchPage(options).then(function formatResponse(response) {
// Attributes are being filtered here, so they are not leaked into calling layer
// where models are serialized to json and do not do more filtering.
// Re-add and pick any computed properties that were stripped before fetchPage call.
const data = response.collection.models.map((model) => {
if (requestedColumns) {
model.attributes = _.pick(model.attributes, requestedColumns);
model._previousAttributes = _.pick(model._previousAttributes, requestedColumns);
}
return model;
});
return {
data: data,
meta: {pagination: response.pagination}
};
}).catch((err) => {
throw err;
});
},
/**
* ### Find One
* Naive find one where data determines what to match on
* @param {Object} data
* @param {Object} unfilteredOptions (optional)
* @return {Promise<ghostBookshelf.Model>} Single Model
*/
findOne: function findOne(data, unfilteredOptions) {
const options = this.filterOptions(unfilteredOptions, 'findOne');
data = this.filterData(data);
const model = this.forge(data);
// @NOTE: The API layer decides if this option is allowed
if (options.filter) {
model.applyDefaultAndCustomFilters(options);
}
// Ensure only valid fields/columns are added to query
if (options.columns) {
options.columns = _.intersection(options.columns, this.prototype.permittedAttributes());
}
return model.fetch(options);
},
/**
* ### Edit
* Naive edit
*
* We always forward the `method` option to Bookshelf, see http://bookshelfjs.org/#Model-instance-save.
* Based on the `method` option Bookshelf and Ghost can determine if a query is an insert or an update.
*
* @param {Object} data
* @param {Object} unfilteredOptions (optional)
* @return {Promise<ghostBookshelf.Model>} Edited Model
*/
edit: function edit(data, unfilteredOptions) {
const options = this.filterOptions(unfilteredOptions, 'edit');
const id = options.id;
const model = this.forge({id: id});
data = this.filterData(data);
// @NOTE: The API layer decides if this option is allowed
if (options.filter) {
model.applyDefaultAndCustomFilters(options);
}
// We allow you to disable timestamps when run migration, so that the posts `updated_at` value is the same
if (options.importing) {
model.hasTimestamps = false;
}
return model
.fetch(options)
.then((object) => {
if (object) {
options.method = 'update';
return object.save(data, options);
}
throw new errors.NotFoundError();
});
},
/**
* ### Add
* Naive add
* @param {Object} data
* @param {Object} unfilteredOptions (optional)
* @return {Promise<ghostBookshelf.Model>} Newly Added Model
*/
add: function add(data, unfilteredOptions) {
const options = this.filterOptions(unfilteredOptions, 'add');
let model;
data = this.filterData(data);
model = this.forge(data);
// We allow you to disable timestamps when importing posts so that the new posts `updated_at` value is the same
// as the import json blob. More details refer to https://github.com/TryGhost/Ghost/issues/1696
if (options.importing) {
model.hasTimestamps = false;
}
// Bookshelf determines whether an operation is an update or an insert based on the id
// Ghost auto-generates Object id's, so we need to tell Bookshelf here that we are inserting data
options.method = 'insert';
return model.save(null, options);
},
bulkAdd: function bulkAdd(data, tableName) { bulkAdd: function bulkAdd(data, tableName) {
tableName = tableName || this.prototype.tableName; tableName = tableName || this.prototype.tableName;
return bulkOperations.insert(tableName, data); return bulkOperations.insert(tableName, data);
}, },
/**
* ### Destroy
* Naive destroy
* @param {Object} unfilteredOptions (optional)
* @return {Promise<ghostBookshelf.Model>} Empty Model
*/
destroy: function destroy(unfilteredOptions) {
const options = this.filterOptions(unfilteredOptions, 'destroy');
if (!options.destroyBy) {
options.destroyBy = {
id: options.id
};
}
// Fetch the object before destroying it, so that the changed data is available to events
return this.forge(options.destroyBy)
.fetch(options)
.then(function then(obj) {
return obj.destroy(options);
});
},
bulkDestroy: function bulkDestroy(data, tableName) { bulkDestroy: function bulkDestroy(data, tableName) {
tableName = tableName || this.prototype.tableName; tableName = tableName || this.prototype.tableName;

View file

@ -4,7 +4,7 @@ const models = require('../../models');
const events = require('../../lib/common/events'); const events = require('../../lib/common/events');
const logging = require('@tryghost/logging'); const logging = require('@tryghost/logging');
const errors = require('@tryghost/errors'); const errors = require('@tryghost/errors');
const {sequence} = require('@tryghost/promise'); const Promise = require('bluebird');
/** /**
* WHEN timezone changes, we will: * WHEN timezone changes, we will:
@ -34,21 +34,20 @@ events.on('settings.timezone.edited', function (settingModel, options) {
* We lock the target row on fetch by using the `forUpdate` option. * We lock the target row on fetch by using the `forUpdate` option.
* Read more in models/post.js - `onFetching` * Read more in models/post.js - `onFetching`
*/ */
return models.Base.transaction(function (transacting) { return models.Base.transaction(async function (transacting) {
options.transacting = transacting; options.transacting = transacting;
options.forUpdate = true; options.forUpdate = true;
return models.Post.findAll(_.merge({filter: 'status:scheduled'}, options)) try {
.then(function (results) { const results = await models.Post.findAll(_.merge({filter: 'status:scheduled'}, options));
if (!results.length) { if (!results.length) {
return; return;
} }
return sequence(results.map(function (post) { await Promise.mapSeries(results, async (post) => {
return function reschedulePostIfPossible() { const newPublishedAtMoment = moment(post.get('published_at')).add(timezoneOffsetDiff, 'minutes');
const newPublishedAtMoment = moment(post.get('published_at')).add(timezoneOffsetDiff, 'minutes');
/** /**
* CASE: * CASE:
* - your configured TZ is GMT+01:00 * - your configured TZ is GMT+01:00
* - now is 10AM +01:00 (9AM UTC) * - now is 10AM +01:00 (9AM UTC)
@ -59,28 +58,26 @@ events.on('settings.timezone.edited', function (settingModel, options) {
* - so we update published_at to 7PM - 480minutes === 11AM UTC * - so we update published_at to 7PM - 480minutes === 11AM UTC
* - 11AM UTC === 7PM +08:00 * - 11AM UTC === 7PM +08:00
*/ */
if (newPublishedAtMoment.isBefore(moment().add(5, 'minutes'))) { if (newPublishedAtMoment.isBefore(moment().add(5, 'minutes'))) {
post.set('status', 'draft'); post.set('status', 'draft');
} else { } else {
post.set('published_at', newPublishedAtMoment.toDate()); post.set('published_at', newPublishedAtMoment.toDate());
} }
return models.Post.edit(post.toJSON(), _.merge({id: post.id}, options)).reflect(); try {
}; await models.Post.edit(post.toJSON(), _.merge({id: post.id}, options));
})).each(function (result) { } catch (err) {
if (!result.isFulfilled()) { logging.error(new errors.GhostError({
logging.error(new errors.GhostError({ err
err: result.reason() }));
})); }
}
});
})
.catch(function (err) {
logging.error(new errors.GhostError({
err: err,
level: 'critical'
}));
}); });
} catch (err) {
logging.error(new errors.GhostError({
err: err,
level: 'critical'
}));
}
}); });
}); });

View file

@ -0,0 +1,243 @@
const errors = require('@tryghost/errors');
const should = require('should');
const sinon = require('sinon');
const models = require('../../../../core/server/models');
describe('Models: crud', function () {
before(function () {
models.init();
});
afterEach(function () {
sinon.restore();
});
describe('destroy', function () {
it('forges model using destroyBy, fetches it, and calls destroy, passing filtered options', function () {
const unfilteredOptions = {
destroyBy: {
prop: 'whatever'
}
};
const model = models.Base.Model.forge({});
const filterOptionsSpy = sinon.spy(models.Base.Model, 'filterOptions');
const forgeStub = sinon.stub(models.Base.Model, 'forge')
.returns(model);
const fetchStub = sinon.stub(model, 'fetch')
.resolves(model);
const destroyStub = sinon.stub(model, 'destroy');
return models.Base.Model.destroy(unfilteredOptions).then(() => {
should.equal(filterOptionsSpy.args[0][0], unfilteredOptions);
should.equal(filterOptionsSpy.args[0][1], 'destroy');
should.deepEqual(forgeStub.args[0][0], {
prop: 'whatever'
});
const filteredOptions = filterOptionsSpy.returnValues[0];
should.equal(fetchStub.args[0][0], filteredOptions);
should.equal(destroyStub.args[0][0], filteredOptions);
});
});
it('uses options.id to forge model, if no destroyBy is provided', function () {
const unfilteredOptions = {
id: 23
};
const model = models.Base.Model.forge({});
const filterOptionsSpy = sinon.spy(models.Base.Model, 'filterOptions');
const forgeStub = sinon.stub(models.Base.Model, 'forge')
.returns(model);
const fetchStub = sinon.stub(model, 'fetch')
.resolves(model);
const destroyStub = sinon.stub(model, 'destroy');
return models.Base.Model.destroy(unfilteredOptions).then(() => {
should.equal(filterOptionsSpy.args[0][0], unfilteredOptions);
should.equal(filterOptionsSpy.args[0][1], 'destroy');
should.deepEqual(forgeStub.args[0][0], {
id: 23
});
const filteredOptions = filterOptionsSpy.returnValues[0];
should.equal(fetchStub.args[0][0], filteredOptions);
should.equal(destroyStub.args[0][0], filteredOptions);
});
});
});
describe('findOne', function () {
it('forges model using filtered data, fetches it passing filtered options and resolves with the fetched model', function () {
const data = {
id: 670
};
const unfilteredOptions = {
donny: 'donson'
};
const model = models.Base.Model.forge({});
const fetchedModel = models.Base.Model.forge({});
const filterOptionsSpy = sinon.spy(models.Base.Model, 'filterOptions');
const filterDataSpy = sinon.spy(models.Base.Model, 'filterData');
const forgeStub = sinon.stub(models.Base.Model, 'forge')
.returns(model);
const fetchStub = sinon.stub(model, 'fetch')
.resolves(fetchedModel);
const findOneReturnValue = models.Base.Model.findOne(data, unfilteredOptions);
should.equal(findOneReturnValue, fetchStub.returnValues[0]);
return findOneReturnValue.then((result) => {
should.equal(result, fetchedModel);
should.equal(filterOptionsSpy.args[0][0], unfilteredOptions);
should.equal(filterOptionsSpy.args[0][1], 'findOne');
should.equal(filterDataSpy.args[0][0], data);
const filteredData = filterDataSpy.returnValues[0];
should.deepEqual(forgeStub.args[0][0], filteredData);
const filteredOptions = filterOptionsSpy.returnValues[0];
should.equal(fetchStub.args[0][0], filteredOptions);
});
});
});
describe('edit', function () {
it('resolves with the savedModel after forges model w/ id, fetches w/ filtered options, saves w/ filtered data and options and method=update', function () {
const data = {
life: 'suffering'
};
const unfilteredOptions = {
id: 'something real special'
};
const model = models.Base.Model.forge({});
const savedModel = models.Base.Model.forge({});
const filterOptionsSpy = sinon.spy(models.Base.Model, 'filterOptions');
const filterDataSpy = sinon.spy(models.Base.Model, 'filterData');
const forgeStub = sinon.stub(models.Base.Model, 'forge')
.returns(model);
const fetchStub = sinon.stub(model, 'fetch')
.resolves(model);
const saveStub = sinon.stub(model, 'save')
.resolves(savedModel);
return models.Base.Model.edit(data, unfilteredOptions).then((result) => {
should.equal(result, savedModel);
should.equal(filterOptionsSpy.args[0][0], unfilteredOptions);
should.equal(filterOptionsSpy.args[0][1], 'edit');
should.equal(filterDataSpy.args[0][0], data);
const filteredOptions = filterOptionsSpy.returnValues[0];
should.deepEqual(forgeStub.args[0][0], {id: filteredOptions.id});
should.equal(fetchStub.args[0][0], filteredOptions);
const filteredData = filterDataSpy.returnValues[0];
should.equal(saveStub.args[0][0], filteredData);
should.equal(saveStub.args[0][1].method, 'update');
should.deepEqual(saveStub.args[0][1], filteredOptions);
});
});
it('sets model.hasTimestamps to false if options.importing is truthy', function () {
const data = {
base: 'cannon'
};
const unfilteredOptions = {
importing: true
};
const model = models.Base.Model.forge({});
const forgeStub = sinon.stub(models.Base.Model, 'forge')
.returns(model);
const fetchStub = sinon.stub(model, 'fetch')
.resolves();
return models.Base.Model.findOne(data, unfilteredOptions).then(() => {
should.equal(model.hasTimestamps, true);
});
});
it('throws an error if model cannot be found on edit', function () {
const data = {
db: 'cooper'
};
const unfilteredOptions = {
id: 'something real special'
};
const model = models.Base.Model.forge({});
const filterOptionsSpy = sinon.spy(models.Base.Model, 'filterOptions');
const filterDataSpy = sinon.spy(models.Base.Model, 'filterData');
const forgeStub = sinon.stub(models.Base.Model, 'forge')
.returns(model);
const fetchStub = sinon.stub(model, 'fetch')
.resolves();
const saveSpy = sinon.stub(model, 'save');
return models.Base.Model.edit(data, unfilteredOptions).then(() => {
throw new Error('That should not happen');
}).catch((err) => {
(err instanceof errors.NotFoundError).should.be.true();
});
});
});
describe('add', function () {
it('forges model w/ filtered data, saves w/ null and options and method=insert', function () {
const data = {
rum: 'ham'
};
const unfilteredOptions = {};
const model = models.Base.Model.forge({});
const savedModel = models.Base.Model.forge({});
const filterOptionsSpy = sinon.spy(models.Base.Model, 'filterOptions');
const filterDataSpy = sinon.spy(models.Base.Model, 'filterData');
const forgeStub = sinon.stub(models.Base.Model, 'forge')
.returns(model);
const saveStub = sinon.stub(model, 'save')
.resolves(savedModel);
return models.Base.Model.add(data, unfilteredOptions).then((result) => {
should.equal(result, savedModel);
should.equal(filterOptionsSpy.args[0][0], unfilteredOptions);
should.equal(filterOptionsSpy.args[0][1], 'add');
should.equal(filterDataSpy.args[0][0], data);
const filteredData = filterDataSpy.returnValues[0];
should.deepEqual(forgeStub.args[0][0], filteredData);
const filteredOptions = filterOptionsSpy.returnValues[0];
should.equal(saveStub.args[0][0], null);
should.equal(saveStub.args[0][1].method, 'insert');
should.deepEqual(saveStub.args[0][1], filteredOptions);
});
});
it('sets model.hasTimestamps to false if options.importing is truthy', function () {
const data = {
newham: 'generals'
};
const unfilteredOptions = {
importing: true
};
const model = models.Base.Model.forge({});
const forgeStub = sinon.stub(models.Base.Model, 'forge')
.returns(model);
const saveStub = sinon.stub(model, 'save')
.resolves();
return models.Base.Model.add(data, unfilteredOptions).then(() => {
should.equal(model.hasTimestamps, false);
});
});
});
});

View file

@ -183,233 +183,4 @@ describe('Models: base', function () {
base.get('b').should.eql(''); base.get('b').should.eql('');
}); });
}); });
describe('destroy', function () {
it('forges model using destroyBy, fetches it, and calls destroy, passing filtered options', function () {
const unfilteredOptions = {
destroyBy: {
prop: 'whatever'
}
};
const model = models.Base.Model.forge({});
const filterOptionsSpy = sinon.spy(models.Base.Model, 'filterOptions');
const forgeStub = sinon.stub(models.Base.Model, 'forge')
.returns(model);
const fetchStub = sinon.stub(model, 'fetch')
.resolves(model);
const destroyStub = sinon.stub(model, 'destroy');
return models.Base.Model.destroy(unfilteredOptions).then(() => {
should.equal(filterOptionsSpy.args[0][0], unfilteredOptions);
should.equal(filterOptionsSpy.args[0][1], 'destroy');
should.deepEqual(forgeStub.args[0][0], {
prop: 'whatever'
});
const filteredOptions = filterOptionsSpy.returnValues[0];
should.equal(fetchStub.args[0][0], filteredOptions);
should.equal(destroyStub.args[0][0], filteredOptions);
});
});
it('uses options.id to forge model, if no destroyBy is provided', function () {
const unfilteredOptions = {
id: 23
};
const model = models.Base.Model.forge({});
const filterOptionsSpy = sinon.spy(models.Base.Model, 'filterOptions');
const forgeStub = sinon.stub(models.Base.Model, 'forge')
.returns(model);
const fetchStub = sinon.stub(model, 'fetch')
.resolves(model);
const destroyStub = sinon.stub(model, 'destroy');
return models.Base.Model.destroy(unfilteredOptions).then(() => {
should.equal(filterOptionsSpy.args[0][0], unfilteredOptions);
should.equal(filterOptionsSpy.args[0][1], 'destroy');
should.deepEqual(forgeStub.args[0][0], {
id: 23
});
const filteredOptions = filterOptionsSpy.returnValues[0];
should.equal(fetchStub.args[0][0], filteredOptions);
should.equal(destroyStub.args[0][0], filteredOptions);
});
});
});
describe('findOne', function () {
it('forges model using filtered data, fetches it passing filtered options and resolves with the fetched model', function () {
const data = {
id: 670
};
const unfilteredOptions = {
donny: 'donson'
};
const model = models.Base.Model.forge({});
const fetchedModel = models.Base.Model.forge({});
const filterOptionsSpy = sinon.spy(models.Base.Model, 'filterOptions');
const filterDataSpy = sinon.spy(models.Base.Model, 'filterData');
const forgeStub = sinon.stub(models.Base.Model, 'forge')
.returns(model);
const fetchStub = sinon.stub(model, 'fetch')
.resolves(fetchedModel);
const findOneReturnValue = models.Base.Model.findOne(data, unfilteredOptions);
should.equal(findOneReturnValue, fetchStub.returnValues[0]);
return findOneReturnValue.then((result) => {
should.equal(result, fetchedModel);
should.equal(filterOptionsSpy.args[0][0], unfilteredOptions);
should.equal(filterOptionsSpy.args[0][1], 'findOne');
should.equal(filterDataSpy.args[0][0], data);
const filteredData = filterDataSpy.returnValues[0];
should.deepEqual(forgeStub.args[0][0], filteredData);
const filteredOptions = filterOptionsSpy.returnValues[0];
should.equal(fetchStub.args[0][0], filteredOptions);
});
});
});
describe('edit', function () {
it('resolves with the savedModel after forges model w/ id, fetches w/ filtered options, saves w/ filtered data and options and method=update', function () {
const data = {
life: 'suffering'
};
const unfilteredOptions = {
id: 'something real special'
};
const model = models.Base.Model.forge({});
const savedModel = models.Base.Model.forge({});
const filterOptionsSpy = sinon.spy(models.Base.Model, 'filterOptions');
const filterDataSpy = sinon.spy(models.Base.Model, 'filterData');
const forgeStub = sinon.stub(models.Base.Model, 'forge')
.returns(model);
const fetchStub = sinon.stub(model, 'fetch')
.resolves(model);
const saveStub = sinon.stub(model, 'save')
.resolves(savedModel);
return models.Base.Model.edit(data, unfilteredOptions).then((result) => {
should.equal(result, savedModel);
should.equal(filterOptionsSpy.args[0][0], unfilteredOptions);
should.equal(filterOptionsSpy.args[0][1], 'edit');
should.equal(filterDataSpy.args[0][0], data);
const filteredOptions = filterOptionsSpy.returnValues[0];
should.deepEqual(forgeStub.args[0][0], {id: filteredOptions.id});
should.equal(fetchStub.args[0][0], filteredOptions);
const filteredData = filterDataSpy.returnValues[0];
should.equal(saveStub.args[0][0], filteredData);
should.equal(saveStub.args[0][1].method, 'update');
should.deepEqual(saveStub.args[0][1], filteredOptions);
});
});
it('sets model.hasTimestamps to false if options.importing is truthy', function () {
const data = {
base: 'cannon'
};
const unfilteredOptions = {
importing: true
};
const model = models.Base.Model.forge({});
const forgeStub = sinon.stub(models.Base.Model, 'forge')
.returns(model);
const fetchStub = sinon.stub(model, 'fetch')
.resolves();
return models.Base.Model.findOne(data, unfilteredOptions).then(() => {
should.equal(model.hasTimestamps, true);
});
});
it('throws an error if model cannot be found on edit', function () {
const data = {
db: 'cooper'
};
const unfilteredOptions = {
id: 'something real special'
};
const model = models.Base.Model.forge({});
const filterOptionsSpy = sinon.spy(models.Base.Model, 'filterOptions');
const filterDataSpy = sinon.spy(models.Base.Model, 'filterData');
const forgeStub = sinon.stub(models.Base.Model, 'forge')
.returns(model);
const fetchStub = sinon.stub(model, 'fetch')
.resolves();
const saveSpy = sinon.stub(model, 'save');
return models.Base.Model.edit(data, unfilteredOptions).then(() => {
throw new Error('That should not happen');
}).catch((err) => {
(err instanceof errors.NotFoundError).should.be.true();
});
});
});
describe('add', function () {
it('forges model w/ filtered data, saves w/ null and options and method=insert', function () {
const data = {
rum: 'ham'
};
const unfilteredOptions = {};
const model = models.Base.Model.forge({});
const savedModel = models.Base.Model.forge({});
const filterOptionsSpy = sinon.spy(models.Base.Model, 'filterOptions');
const filterDataSpy = sinon.spy(models.Base.Model, 'filterData');
const forgeStub = sinon.stub(models.Base.Model, 'forge')
.returns(model);
const saveStub = sinon.stub(model, 'save')
.resolves(savedModel);
return models.Base.Model.add(data, unfilteredOptions).then((result) => {
should.equal(result, savedModel);
should.equal(filterOptionsSpy.args[0][0], unfilteredOptions);
should.equal(filterOptionsSpy.args[0][1], 'add');
should.equal(filterDataSpy.args[0][0], data);
const filteredData = filterDataSpy.returnValues[0];
should.deepEqual(forgeStub.args[0][0], filteredData);
const filteredOptions = filterOptionsSpy.returnValues[0];
should.equal(saveStub.args[0][0], null);
should.equal(saveStub.args[0][1].method, 'insert');
should.deepEqual(saveStub.args[0][1], filteredOptions);
});
});
it('sets model.hasTimestamps to false if options.importing is truthy', function () {
const data = {
newham: 'generals'
};
const unfilteredOptions = {
importing: true
};
const model = models.Base.Model.forge({});
const forgeStub = sinon.stub(models.Base.Model, 'forge')
.returns(model);
const saveStub = sinon.stub(model, 'save')
.resolves();
return models.Base.Model.add(data, unfilteredOptions).then(() => {
should.equal(model.hasTimestamps, false);
});
});
});
}); });