mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-01 02:41:39 -05:00
Extracted Bookshelf plugins from Ghost to Framework repo
refs: -cf15f60085
-dd20cc649b
-ccf27f7009
-abf146d61f
-2b54c92a14
-bb029a53f6
-95bd7ee675
-9018b4df22
-df01a6e5f4
-d313726b34
- these plugins were in a state where they were independent enough to be pulled out into their own packages, which is what we did in the referenced commits above - each package is named like `@tryghost/bookshelf-<plugin>` - to avoid requiring multiple packages into Ghost, we've also created a wrapper package called `@tryghost/bookshelf-plugins` which re-exports all these plugins, so the changes in Ghost are very simple -dbebdd43b5
- this commit deletes the plugins + tests, and replaces with our new package with some minor code changes
This commit is contained in:
parent
4e49aeb9a0
commit
495ef867c2
15 changed files with 106 additions and 1290 deletions
|
@ -22,7 +22,7 @@ const security = require('@tryghost/security');
|
|||
const schema = require('../../data/schema');
|
||||
const urlUtils = require('../../../shared/url-utils');
|
||||
const bulkOperations = require('./bulk-operations');
|
||||
const plugins = require('../plugins');
|
||||
const plugins = require('@tryghost/bookshelf-plugins');
|
||||
const tpl = require('@tryghost/tpl');
|
||||
|
||||
const messages = {
|
||||
|
@ -1307,7 +1307,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
|||
nql(filter).querySQL(query);
|
||||
|
||||
if (shouldHavePosts) {
|
||||
require('../plugins/has-posts').addHasPostsWhere(tableNames[modelName], shouldHavePosts)(query);
|
||||
plugins.hasPosts.addHasPostsWhere(tableNames[modelName], shouldHavePosts)(query);
|
||||
}
|
||||
|
||||
if (options.id) {
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
const moment = require('moment-timezone');
|
||||
const _ = require('lodash');
|
||||
const errors = require('@tryghost/errors');
|
||||
|
||||
/**
|
||||
* @param {import('bookshelf')} Bookshelf
|
||||
*/
|
||||
module.exports = function (Bookshelf) {
|
||||
const ParentModel = Bookshelf.Model;
|
||||
|
||||
const Model = Bookshelf.Model.extend({
|
||||
/**
|
||||
* Update collision protection.
|
||||
*
|
||||
* IMPORTANT NOTES:
|
||||
* The `sync` method is called for any query e.g. update, add, delete, fetch
|
||||
*
|
||||
* We had the option to override Bookshelf's `save` method, but hooking into the `sync` method gives us
|
||||
* the ability to access the `changed` object. Bookshelf already knows which attributes has changed.
|
||||
*
|
||||
* Bookshelf's timestamp function can't be overridden, as it's synchronous, there is no way to return an Error.
|
||||
*
|
||||
* If we want to enable the collision plugin for other tables, the queries might need to run in a transaction.
|
||||
* This depends on if we fetch the model before editing. Imagine two concurrent requests come in, both would fetch
|
||||
* the same current database values and both would succeed to update and override each other.
|
||||
*/
|
||||
sync: function timestamp(options) {
|
||||
const parentSync = ParentModel.prototype.sync.apply(this, arguments);
|
||||
const originalUpdateSync = parentSync.update;
|
||||
const self = this;
|
||||
|
||||
// CASE: only enabled for posts table
|
||||
if (this.tableName !== 'posts' ||
|
||||
!self.serverData ||
|
||||
((options.method !== 'update' && options.method !== 'patch') || !options.method)
|
||||
) {
|
||||
return parentSync;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only hook into the update sync
|
||||
*
|
||||
* IMPORTANT NOTES:
|
||||
* Even if the client sends a different `id` property, it get's ignored by bookshelf.
|
||||
* Because you can't change the `id` of an existing post.
|
||||
*
|
||||
* HTML is always auto generated, ignore.
|
||||
*/
|
||||
parentSync.update = async function update() {
|
||||
const response = await originalUpdateSync.apply(this, arguments);
|
||||
const changed = _.omit(self._changed, [
|
||||
'created_at', 'updated_at', 'author_id', 'id',
|
||||
'published_by', 'updated_by', 'html', 'plaintext'
|
||||
]);
|
||||
|
||||
const clientUpdatedAt = moment(self.clientData.updated_at || self.serverData.updated_at || new Date());
|
||||
const serverUpdatedAt = moment(self.serverData.updated_at || clientUpdatedAt);
|
||||
|
||||
if (Object.keys(changed).length) {
|
||||
if (clientUpdatedAt.diff(serverUpdatedAt) !== 0) {
|
||||
// @NOTE: This will rollback the update. We cannot know if relations were updated before doing the update.
|
||||
throw new errors.UpdateCollisionError({
|
||||
message: 'Saving failed! Someone else is editing this post.',
|
||||
code: 'UPDATE_COLLISION',
|
||||
level: 'critical',
|
||||
errorDetails: {
|
||||
clientUpdatedAt: self.clientData.updated_at,
|
||||
serverUpdatedAt: self.serverData.updated_at
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
return parentSync;
|
||||
},
|
||||
|
||||
/**
|
||||
* We have to remember current server data and client data.
|
||||
* The `sync` method has no access to it.
|
||||
* `updated_at` is already set to "Date.now" when the overridden `sync.update` is called.
|
||||
* See https://github.com/tgriesser/bookshelf/blob/79c526870e618748caf94e7476a0bc796ee090a6/src/model.js#L955
|
||||
*/
|
||||
save: function save(data) {
|
||||
this.clientData = _.cloneDeep(data) || {};
|
||||
this.serverData = _.cloneDeep(this.attributes);
|
||||
|
||||
return ParentModel.prototype.save.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
Bookshelf.Model = Model;
|
||||
};
|
|
@ -1,19 +0,0 @@
|
|||
/**
|
||||
* @param {import('bookshelf')} Bookshelf
|
||||
*/
|
||||
const customQueryPlug = function customQueryPlug(Bookshelf) {
|
||||
const Model = Bookshelf.Model.extend({
|
||||
// override this on the model itself
|
||||
customQuery() {},
|
||||
|
||||
applyCustomQuery: function applyCustomQuery(options) {
|
||||
this.query((qb) => {
|
||||
this.customQuery(qb, options);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Bookshelf.Model = Model;
|
||||
};
|
||||
|
||||
module.exports = customQueryPlug;
|
|
@ -1,82 +0,0 @@
|
|||
const _ = require('lodash');
|
||||
const _debug = require('ghost-ignition').debug._base;
|
||||
const debug = _debug('ghost-query');
|
||||
|
||||
/**
|
||||
* Enchances knex query builder with a join to relation configured in
|
||||
*
|
||||
* @param {Bookshelf['Model']} model instance of Bookshelf model
|
||||
* @param {String[]} relationsToLoad relations to be included in joins
|
||||
*/
|
||||
function withEager(model, relationsToLoad) {
|
||||
const tableName = _.result(model.constructor.prototype, 'tableName');
|
||||
|
||||
return function (qb) {
|
||||
if (!model.relationsMeta) {
|
||||
return qb;
|
||||
}
|
||||
|
||||
for (const [key, config] of Object.entries(model.relationsMeta)) {
|
||||
if (relationsToLoad.includes(key)) {
|
||||
const innerQb = qb
|
||||
.leftJoin(config.targetTableName, `${tableName}.id`, `${config.targetTableName}.${config.foreignKey}`);
|
||||
|
||||
debug(`QUERY has posts: ${innerQb.toSQL().sql}`);
|
||||
}
|
||||
}
|
||||
|
||||
return qb;
|
||||
};
|
||||
}
|
||||
|
||||
function load(options) {
|
||||
if (!options) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.eagerLoad) {
|
||||
if (!options.columns && options.withRelated && _.intersection(this.eagerLoad, options.withRelated).length) {
|
||||
this.query(withEager(this, this.eagerLoad));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ## Pagination
|
||||
* Extends `bookshelf.Model` native `fetch` and `fetchAll` methods with
|
||||
* a join to "eager loaded" relation. An exaple of such loading is when
|
||||
* there is a need to order by fields in the related table.
|
||||
*/
|
||||
module.exports = function eagerLoadPlugin(Bookshelf) {
|
||||
const modelPrototype = Bookshelf.Model.prototype;
|
||||
|
||||
Bookshelf.Model = Bookshelf.Model.extend({
|
||||
initialize: function () {
|
||||
return modelPrototype.initialize.apply(this, arguments);
|
||||
},
|
||||
|
||||
fetch: function () {
|
||||
load.apply(this, arguments);
|
||||
|
||||
if (_debug.enabled('ghost-query')) {
|
||||
debug('QUERY', this.query().toQuery());
|
||||
}
|
||||
|
||||
return modelPrototype.fetch.apply(this, arguments);
|
||||
},
|
||||
|
||||
fetchAll: function () {
|
||||
load.apply(this, arguments);
|
||||
|
||||
if (_debug.enabled('ghost-query')) {
|
||||
debug('QUERY', this.query().toQuery());
|
||||
}
|
||||
|
||||
return modelPrototype.fetchAll.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {import('bookshelf')} Bookshelf
|
||||
*/
|
|
@ -1,157 +0,0 @@
|
|||
const debug = require('ghost-ignition').debug('models:plugins:filter');
|
||||
const errors = require('@tryghost/errors');
|
||||
const tpl = require('@tryghost/tpl');
|
||||
|
||||
const messages = {
|
||||
errorParsing: 'Error parsing filter'
|
||||
};
|
||||
|
||||
const RELATIONS = {
|
||||
tags: {
|
||||
tableName: 'tags',
|
||||
type: 'manyToMany',
|
||||
joinTable: 'posts_tags',
|
||||
joinFrom: 'post_id',
|
||||
joinTo: 'tag_id'
|
||||
},
|
||||
authors: {
|
||||
tableName: 'users',
|
||||
tableNameAs: 'authors',
|
||||
type: 'manyToMany',
|
||||
joinTable: 'posts_authors',
|
||||
joinFrom: 'post_id',
|
||||
joinTo: 'author_id'
|
||||
},
|
||||
labels: {
|
||||
tableName: 'labels',
|
||||
type: 'manyToMany',
|
||||
joinTable: 'members_labels',
|
||||
joinFrom: 'member_id',
|
||||
joinTo: 'label_id'
|
||||
},
|
||||
products: {
|
||||
tableName: 'products',
|
||||
type: 'manyToMany',
|
||||
joinTable: 'members_products',
|
||||
joinFrom: 'member_id',
|
||||
joinTo: 'product_id'
|
||||
},
|
||||
posts_meta: {
|
||||
tableName: 'posts_meta',
|
||||
type: 'oneToOne',
|
||||
joinFrom: 'post_id'
|
||||
}
|
||||
};
|
||||
|
||||
const EXPANSIONS = {
|
||||
posts: [{
|
||||
key: 'primary_tag',
|
||||
replacement: 'tags.slug',
|
||||
expansion: 'posts_tags.sort_order:0+tags.visibility:public'
|
||||
}, {
|
||||
key: 'primary_author',
|
||||
replacement: 'authors.slug',
|
||||
expansion: 'posts_authors.sort_order:0+authors.visibility:public'
|
||||
}, {
|
||||
key: 'authors',
|
||||
replacement: 'authors.slug'
|
||||
}, {
|
||||
key: 'author',
|
||||
replacement: 'authors.slug'
|
||||
}, {
|
||||
key: 'tag',
|
||||
replacement: 'tags.slug'
|
||||
}, {
|
||||
key: 'tags',
|
||||
replacement: 'tags.slug'
|
||||
}],
|
||||
members: [{
|
||||
key: 'label',
|
||||
replacement: 'labels.slug'
|
||||
}, {
|
||||
key: 'labels',
|
||||
replacement: 'labels.slug'
|
||||
}, {
|
||||
key: 'product',
|
||||
replacement: 'products.slug'
|
||||
}, {
|
||||
key: 'products',
|
||||
replacement: 'products.slug'
|
||||
}]
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('bookshelf')} Bookshelf
|
||||
*/
|
||||
const filter = function filter(Bookshelf) {
|
||||
const Model = Bookshelf.Model.extend({
|
||||
// Cached copy of the filters setup for this model instance
|
||||
_filters: null,
|
||||
// Override these on the various models
|
||||
enforcedFilters() {},
|
||||
defaultFilters() {},
|
||||
extraFilters() {},
|
||||
filterExpansions() {},
|
||||
/**
|
||||
* Method which makes the necessary query builder calls (through knex) for the filters set on this model
|
||||
* instance.
|
||||
*/
|
||||
applyDefaultAndCustomFilters: function applyDefaultAndCustomFilters(options) {
|
||||
const nql = require('@nexes/nql');
|
||||
|
||||
const expansions = [];
|
||||
|
||||
if (EXPANSIONS[this.tableName]) {
|
||||
expansions.push(...EXPANSIONS[this.tableName]);
|
||||
}
|
||||
|
||||
if (this.filterExpansions()) {
|
||||
expansions.push(...this.filterExpansions());
|
||||
}
|
||||
|
||||
let custom = options.filter;
|
||||
let extra = this.extraFilters(options);
|
||||
let overrides = this.enforcedFilters(options);
|
||||
let defaults = this.defaultFilters(options);
|
||||
let transformer = options.mongoTransformer;
|
||||
|
||||
debug('custom', custom);
|
||||
debug('extra', extra);
|
||||
debug('enforced', overrides);
|
||||
debug('default', defaults);
|
||||
|
||||
if (extra) {
|
||||
if (custom) {
|
||||
custom = `${custom}+${extra}`;
|
||||
} else {
|
||||
custom = extra;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.query((qb) => {
|
||||
nql(custom, {
|
||||
relations: RELATIONS,
|
||||
expansions: expansions,
|
||||
overrides: overrides,
|
||||
defaults: defaults,
|
||||
transformer: transformer
|
||||
}).querySQL(qb);
|
||||
});
|
||||
} catch (err) {
|
||||
throw new errors.BadRequestError({
|
||||
message: tpl(messages.errorParsing),
|
||||
err
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Bookshelf.Model = Model;
|
||||
};
|
||||
|
||||
/**
|
||||
* ## Export Filter plugin
|
||||
* @api public
|
||||
*/
|
||||
module.exports = filter;
|
|
@ -1,63 +0,0 @@
|
|||
const _ = require('lodash');
|
||||
const _debug = require('ghost-ignition').debug._base;
|
||||
const debug = _debug('ghost-query');
|
||||
|
||||
const addHasPostsWhere = (tableName, config) => {
|
||||
const comparisonField = `${tableName}.id`;
|
||||
|
||||
return function (qb) {
|
||||
return qb.whereIn(comparisonField, function () {
|
||||
const innerQb = this
|
||||
.distinct(`${config.joinTable}.${config.joinTo}`)
|
||||
.select()
|
||||
.from(config.joinTable)
|
||||
.whereRaw(`${config.joinTable}.${config.joinTo} = ${comparisonField}`)
|
||||
.join('posts', 'posts.id', `${config.joinTable}.post_id`)
|
||||
.andWhere('posts.status', '=', 'published');
|
||||
|
||||
debug(`QUERY has posts: ${innerQb.toSQL().sql}`);
|
||||
|
||||
return innerQb;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('bookshelf')} Bookshelf
|
||||
*/
|
||||
const hasPosts = function hasPosts(Bookshelf) {
|
||||
const modelPrototype = Bookshelf.Model.prototype;
|
||||
|
||||
Bookshelf.Model = Bookshelf.Model.extend({
|
||||
initialize: function () {
|
||||
return modelPrototype.initialize.apply(this, arguments);
|
||||
},
|
||||
|
||||
fetch: function () {
|
||||
if (this.shouldHavePosts) {
|
||||
this.query(addHasPostsWhere(_.result(this, 'tableName'), this.shouldHavePosts));
|
||||
}
|
||||
|
||||
if (_debug.enabled('ghost-query')) {
|
||||
debug('QUERY', this.query().toQuery());
|
||||
}
|
||||
|
||||
return modelPrototype.fetch.apply(this, arguments);
|
||||
},
|
||||
|
||||
fetchAll: function () {
|
||||
if (this.shouldHavePosts) {
|
||||
this.query(addHasPostsWhere(_.result(this, 'tableName'), this.shouldHavePosts));
|
||||
}
|
||||
|
||||
if (_debug.enabled('ghost-query')) {
|
||||
debug('QUERY', this.query().toQuery());
|
||||
}
|
||||
|
||||
return modelPrototype.fetchAll.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = hasPosts;
|
||||
module.exports.addHasPostsWhere = addHasPostsWhere;
|
|
@ -1,106 +0,0 @@
|
|||
const _debug = require('ghost-ignition').debug._base;
|
||||
const debug = _debug('ghost-query');
|
||||
const _ = require('lodash');
|
||||
|
||||
/**
|
||||
* @param {import('bookshelf')} Bookshelf
|
||||
*/
|
||||
module.exports = function (Bookshelf) {
|
||||
const modelProto = Bookshelf.Model.prototype;
|
||||
const countQueryBuilder = {
|
||||
tags: {
|
||||
posts: function addPostCountToTags(model, options) {
|
||||
model.query('columns', 'tags.*', function (qb) {
|
||||
qb.count('posts.id')
|
||||
.from('posts')
|
||||
.leftOuterJoin('posts_tags', 'posts.id', 'posts_tags.post_id')
|
||||
.whereRaw('posts_tags.tag_id = tags.id')
|
||||
.as('count__posts');
|
||||
|
||||
if (options.context && options.context.public) {
|
||||
// @TODO use the filter behavior for posts
|
||||
qb.andWhere('posts.type', '=', 'post');
|
||||
qb.andWhere('posts.status', '=', 'published');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
users: {
|
||||
posts: function addPostCountToUsers(model, options) {
|
||||
model.query('columns', 'users.*', function (qb) {
|
||||
qb.count('posts.id')
|
||||
.from('posts')
|
||||
.join('posts_authors', 'posts.id', 'posts_authors.post_id')
|
||||
.whereRaw('posts_authors.author_id = users.id')
|
||||
.as('count__posts');
|
||||
|
||||
if (options.context && options.context.public) {
|
||||
// @TODO use the filter behavior for posts
|
||||
qb.andWhere('posts.type', '=', 'post');
|
||||
qb.andWhere('posts.status', '=', 'published');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Model = Bookshelf.Model.extend({
|
||||
addCounts: function (options) {
|
||||
if (!options) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tableName = _.result(this, 'tableName');
|
||||
|
||||
if (options.withRelated && options.withRelated.indexOf('count.posts') > -1) {
|
||||
// remove post_count from withRelated
|
||||
options.withRelated = _.pull([].concat(options.withRelated), 'count.posts');
|
||||
|
||||
// Call the query builder
|
||||
countQueryBuilder[tableName].posts(this, options);
|
||||
}
|
||||
},
|
||||
fetch: function () {
|
||||
this.addCounts.apply(this, arguments);
|
||||
|
||||
// Useful when debugging no. database queries, GQL, etc
|
||||
// To output this, use DEBUG=ghost:*,ghost-query
|
||||
if (_debug.enabled('ghost-query')) {
|
||||
debug('QUERY', this.query().toQuery());
|
||||
}
|
||||
|
||||
// Call parent fetch
|
||||
return modelProto.fetch.apply(this, arguments);
|
||||
},
|
||||
fetchAll: function () {
|
||||
this.addCounts.apply(this, arguments);
|
||||
|
||||
// Useful when debugging no. database queries, GQL, etc
|
||||
// To output this, use DEBUG=ghost:*,ghost-query
|
||||
if (_debug.enabled('ghost-query')) {
|
||||
debug('QUERY', this.query().toQuery());
|
||||
}
|
||||
|
||||
// Call parent fetchAll
|
||||
return modelProto.fetchAll.apply(this, arguments);
|
||||
},
|
||||
|
||||
serialize: function serialize(options) {
|
||||
const attrs = modelProto.serialize.call(this, options);
|
||||
const countRegex = /^(count)(__)(.*)$/;
|
||||
|
||||
_.forOwn(attrs, function (value, key) {
|
||||
const match = key.match(countRegex);
|
||||
if (match) {
|
||||
attrs[match[1]] = attrs[match[1]] || {};
|
||||
attrs[match[1]][match[3]] = value;
|
||||
delete attrs[key];
|
||||
}
|
||||
});
|
||||
|
||||
return attrs;
|
||||
}
|
||||
});
|
||||
|
||||
Bookshelf.Model = Model;
|
||||
};
|
|
@ -1,12 +0,0 @@
|
|||
module.exports = {
|
||||
eagerLoad: require('./eager-load'),
|
||||
filter: require('./filter'),
|
||||
order: require('./order'),
|
||||
customQuery: require('./custom-query'),
|
||||
search: require('./search'),
|
||||
includeCount: require('./include-count'),
|
||||
pagination: require('./pagination'),
|
||||
collision: require('./collision'),
|
||||
transactionEvents: require('./transaction-events'),
|
||||
hasPosts: require('./has-posts')
|
||||
};
|
|
@ -1,80 +0,0 @@
|
|||
const _ = require('lodash');
|
||||
|
||||
/**
|
||||
* @param {import('bookshelf')} Bookshelf
|
||||
*/
|
||||
const orderPlugin = function orderPlugin(Bookshelf) {
|
||||
Bookshelf.Model = Bookshelf.Model.extend({
|
||||
orderAttributes() {},
|
||||
orderRawQuery() {},
|
||||
|
||||
parseOrderOption: function (orderQueryString, withRelated) {
|
||||
const order = {};
|
||||
const orderRaw = [];
|
||||
const eagerLoadArray = [];
|
||||
|
||||
const orderAttributes = this.orderAttributes();
|
||||
if (withRelated && withRelated.indexOf('count.posts') > -1) {
|
||||
orderAttributes.push('count.posts');
|
||||
}
|
||||
|
||||
let rules = [];
|
||||
// CASE: repeat order query parameter keys are present
|
||||
if (_.isArray(orderQueryString)) {
|
||||
orderQueryString.forEach((qs) => {
|
||||
rules.push(...qs.split(','));
|
||||
});
|
||||
} else {
|
||||
rules = orderQueryString.split(',');
|
||||
}
|
||||
|
||||
_.each(rules, (rule) => {
|
||||
let match;
|
||||
let field;
|
||||
let direction;
|
||||
|
||||
match = /^([a-z0-9_.]+)\s+(asc|desc)$/i.exec(rule.trim());
|
||||
|
||||
// invalid order syntax
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
field = match[1].toLowerCase();
|
||||
direction = match[2].toUpperCase();
|
||||
|
||||
const orderRawQuery = this.orderRawQuery(field, direction, withRelated);
|
||||
|
||||
if (orderRawQuery) {
|
||||
orderRaw.push(orderRawQuery.orderByRaw);
|
||||
if (orderRawQuery.eagerLoad) {
|
||||
eagerLoadArray.push(orderRawQuery.eagerLoad);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingOrderAttribute = orderAttributes.find((orderAttribute) => {
|
||||
// NOTE: this logic assumes we use different field names for "parent" and "child" relations.
|
||||
// E.g.: ['parent.title', 'child.title'] and ['child.title', 'parent.title'] - would not
|
||||
// distinguish on which relation to sort neither which order to pick the fields on.
|
||||
// For more context see: https://github.com/TryGhost/Ghost/pull/12226#discussion_r493085098
|
||||
return orderAttribute.endsWith(field);
|
||||
});
|
||||
|
||||
if (!matchingOrderAttribute) {
|
||||
return;
|
||||
}
|
||||
|
||||
order[matchingOrderAttribute] = direction;
|
||||
});
|
||||
|
||||
return {
|
||||
order,
|
||||
orderRaw: orderRaw.join(', '),
|
||||
eagerLoad: _.uniq(eagerLoadArray)
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = orderPlugin;
|
|
@ -1,278 +0,0 @@
|
|||
// # Pagination
|
||||
//
|
||||
// Extends Bookshelf.Model with a `fetchPage` method. Handles everything to do with paginated requests.
|
||||
const _ = require('lodash');
|
||||
|
||||
const errors = require('@tryghost/errors');
|
||||
const tpl = require('@tryghost/tpl');
|
||||
|
||||
const messages = {
|
||||
pageNotFound: 'Page not found',
|
||||
couldNotUnderstandRequest: 'Could not understand request.'
|
||||
};
|
||||
|
||||
let defaults;
|
||||
let paginationUtils;
|
||||
|
||||
/**
|
||||
* ### Default pagination values
|
||||
* These are overridden via `options` passed to each function
|
||||
* @typedef {Object} defaults
|
||||
* @default
|
||||
* @property {Number} `page` \- page in set to display (default: 1)
|
||||
* @property {Number|String} `limit` \- no. results per page (default: 15)
|
||||
*/
|
||||
defaults = {
|
||||
page: 1,
|
||||
limit: 15
|
||||
};
|
||||
|
||||
/**
|
||||
* ## Pagination Utils
|
||||
* @api private
|
||||
*/
|
||||
paginationUtils = {
|
||||
/**
|
||||
* ### Parse Options
|
||||
* Take the given options and ensure they are valid pagination options, else use the defaults
|
||||
* @param {Object} options
|
||||
* @returns {Object} options sanitised for pagination
|
||||
*/
|
||||
parseOptions: function parseOptions(options) {
|
||||
options = _.defaults(options || {}, defaults);
|
||||
|
||||
if (options.limit !== 'all') {
|
||||
options.limit = parseInt(options.limit, 10) || defaults.limit;
|
||||
}
|
||||
|
||||
options.page = parseInt(options.page, 10) || defaults.page;
|
||||
|
||||
return options;
|
||||
},
|
||||
/**
|
||||
* ### Query
|
||||
* Apply the necessary parameters to paginate the query
|
||||
* @param {Bookshelf['Model']} model
|
||||
* @param {Object} options
|
||||
*/
|
||||
addLimitAndOffset: function addLimitAndOffset(model, options) {
|
||||
if (_.isNumber(options.limit)) {
|
||||
model
|
||||
.query('limit', options.limit)
|
||||
.query('offset', options.limit * (options.page - 1));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* ### Format Response
|
||||
* Takes the no. items returned and original options and calculates all of the pagination meta data
|
||||
* @param {Number} totalItems
|
||||
* @param {Object} options
|
||||
* @returns {pagination} pagination metadata
|
||||
*/
|
||||
formatResponse: function formatResponse(totalItems, options) {
|
||||
const calcPages = Math.ceil(totalItems / options.limit) || 0;
|
||||
|
||||
const pagination = {
|
||||
page: options.page || defaults.page,
|
||||
limit: options.limit,
|
||||
pages: calcPages === 0 ? 1 : calcPages,
|
||||
total: totalItems,
|
||||
next: null,
|
||||
prev: null
|
||||
};
|
||||
|
||||
if (pagination.pages > 1) {
|
||||
if (pagination.page === 1) {
|
||||
pagination.next = pagination.page + 1;
|
||||
} else if (pagination.page === pagination.pages) {
|
||||
pagination.prev = pagination.page - 1;
|
||||
} else {
|
||||
pagination.next = pagination.page + 1;
|
||||
pagination.prev = pagination.page - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return pagination;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Bookshelf['Model']} model instance of Bookshelf model
|
||||
* @param {string} propertyName property to be inspected and included in the relation
|
||||
*/
|
||||
handleRelation: function handleRelation(model, propertyName) {
|
||||
const tableName = _.result(model.constructor.prototype, 'tableName');
|
||||
|
||||
const targetTable = propertyName.includes('.') && propertyName.split('.')[0];
|
||||
|
||||
if (targetTable && targetTable !== tableName) {
|
||||
if (!model.eagerLoad) {
|
||||
model.eagerLoad = [];
|
||||
}
|
||||
|
||||
if (!model.eagerLoad.includes(targetTable)) {
|
||||
model.eagerLoad.push(targetTable);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ## Object Definitions
|
||||
|
||||
/**
|
||||
* ### Pagination Object
|
||||
* @typedef {Object} pagination
|
||||
* @property {Number} page \- page in set to display
|
||||
* @property {Number|String} limit \- no. results per page, or 'all'
|
||||
* @property {Number} pages \- total no. pages in the full set
|
||||
* @property {Number} total \- total no. items in the full set
|
||||
* @property {Number|null} next \- next page
|
||||
* @property {Number|null} prev \- previous page
|
||||
*/
|
||||
|
||||
/**
|
||||
* ### Fetch Page Options
|
||||
* @typedef {Object} options
|
||||
* @property {Number} page \- page in set to display
|
||||
* @property {Number|String} limit \- no. results per page, or 'all'
|
||||
* @property {Object} order \- set of order by params and directions
|
||||
*/
|
||||
|
||||
/**
|
||||
* ### Fetch Page Response
|
||||
* @typedef {Object} paginatedResult
|
||||
* @property {Array} collection \- set of results
|
||||
* @property {pagination} pagination \- pagination metadata
|
||||
*/
|
||||
|
||||
/**
|
||||
* ## Pagination
|
||||
* Extends `bookshelf.Model` with `fetchPage`
|
||||
* @param {Bookshelf} bookshelf \- the instance to plug into
|
||||
*/
|
||||
const pagination = function pagination(bookshelf) {
|
||||
// Extend updates the first object passed to it, no need for an assignment
|
||||
_.extend(bookshelf.Model.prototype, {
|
||||
/**
|
||||
* ### Fetch page
|
||||
* A `fetch` extension to get a paginated set of items from a collection
|
||||
*
|
||||
* We trigger two queries:
|
||||
* 1. count query to know how many pages left (important: we don't attach any group/order statements!)
|
||||
* 2. the actualy fetch query with limit and page property
|
||||
*
|
||||
* @param {Object} options
|
||||
* @returns {Promise<paginatedResult>} set of results + pagination metadata
|
||||
*/
|
||||
fetchPage: function fetchPage(options) {
|
||||
// Setup pagination options
|
||||
options = paginationUtils.parseOptions(options);
|
||||
|
||||
// Get the table name and idAttribute for this model
|
||||
const tableName = _.result(this.constructor.prototype, 'tableName');
|
||||
|
||||
const idAttribute = _.result(this.constructor.prototype, 'idAttribute');
|
||||
const self = this;
|
||||
|
||||
// #### Pre count clauses
|
||||
// Add any where or join clauses which need to be included with the aggregate query
|
||||
|
||||
// Clone the base query & set up a promise to get the count of total items in the full set
|
||||
// Necessary due to lack of support for `count distinct` in bookshelf's count()
|
||||
// Skipped if limit='all' as we can use the length of the fetched data set
|
||||
let countPromise = Promise.resolve();
|
||||
if (options.limit !== 'all') {
|
||||
const countQuery = this.query().clone();
|
||||
|
||||
if (options.transacting) {
|
||||
countQuery.transacting(options.transacting);
|
||||
}
|
||||
|
||||
countPromise = countQuery.select(
|
||||
bookshelf.knex.raw('count(distinct ' + tableName + '.' + idAttribute + ') as aggregate')
|
||||
);
|
||||
}
|
||||
|
||||
return countPromise.then(function (countResult) {
|
||||
// #### Post count clauses
|
||||
// Add any where or join clauses which need to NOT be included with the aggregate query
|
||||
|
||||
// Setup the pagination parameters so that we return the correct items from the set
|
||||
paginationUtils.addLimitAndOffset(self, options);
|
||||
|
||||
// Apply ordering options if they are present
|
||||
if (options.order && !_.isEmpty(options.order)) {
|
||||
_.forOwn(options.order, function (direction, property) {
|
||||
if (property === 'count.posts') {
|
||||
self.query('orderBy', 'count__posts', direction);
|
||||
} else {
|
||||
self.query('orderBy', property, direction);
|
||||
|
||||
paginationUtils.handleRelation(self, property);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (options.orderRaw) {
|
||||
self.query((qb) => {
|
||||
qb.orderByRaw(options.orderRaw);
|
||||
});
|
||||
}
|
||||
|
||||
if (!_.isEmpty(options.eagerLoad)) {
|
||||
options.eagerLoad.forEach(property => paginationUtils.handleRelation(self, property));
|
||||
}
|
||||
|
||||
if (options.groups && !_.isEmpty(options.groups)) {
|
||||
_.each(options.groups, function (group) {
|
||||
self.query('groupBy', group);
|
||||
});
|
||||
}
|
||||
|
||||
// Setup the promise to do a fetch on our collection, running the specified query
|
||||
// @TODO: ensure option handling is done using an explicit pick elsewhere
|
||||
|
||||
return self.fetchAll(_.omit(options, ['page', 'limit']))
|
||||
.then(function (fetchResult) {
|
||||
if (options.limit === 'all') {
|
||||
countResult = [{aggregate: fetchResult.length}];
|
||||
}
|
||||
|
||||
return {
|
||||
collection: fetchResult,
|
||||
pagination: paginationUtils.formatResponse(countResult[0] ? countResult[0].aggregate : 0, options)
|
||||
};
|
||||
})
|
||||
.catch(function (err) {
|
||||
// e.g. offset/limit reached max allowed integer value
|
||||
if (err.errno === 20 || err.errno === 1064) {
|
||||
throw new errors.NotFoundError({message: tpl(messages.pageNotFound)});
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
}).catch((err) => {
|
||||
// CASE: SQL syntax is incorrect
|
||||
if (err.errno === 1054 || err.errno === 1) {
|
||||
throw new errors.BadRequestError({
|
||||
message: tpl(messages.couldNotUnderstandRequest),
|
||||
err
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {import('bookshelf')} Bookshelf
|
||||
*/
|
||||
|
||||
/**
|
||||
* ## Export pagination plugin
|
||||
* @api public
|
||||
*/
|
||||
module.exports = pagination;
|
|
@ -1,18 +0,0 @@
|
|||
const searchPlugin = function searchPlugin(Bookshelf) {
|
||||
const Model = Bookshelf.Model.extend({
|
||||
// override this on the model itself
|
||||
searchQuery() {},
|
||||
|
||||
applySearchQuery: function applySearchQuery(options) {
|
||||
if (options.search) {
|
||||
this.query((qb) => {
|
||||
this.searchQuery(qb, options.search);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Bookshelf.Model = Model;
|
||||
};
|
||||
|
||||
module.exports = searchPlugin;
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* This is a feature request in knex for 1.0.
|
||||
* https://github.com/tgriesser/knex/issues/1641
|
||||
*/
|
||||
module.exports = function (bookshelf) {
|
||||
const orig1 = bookshelf.transaction;
|
||||
|
||||
bookshelf.transaction = function (cb) {
|
||||
return orig1.bind(bookshelf)(function (t) {
|
||||
const orig2 = t.commit;
|
||||
const orig3 = t.rollback;
|
||||
|
||||
t.commit = function () {
|
||||
t.emit('committed', true);
|
||||
return orig2.apply(t, arguments);
|
||||
};
|
||||
|
||||
t.rollback = function () {
|
||||
t.emit('committed', false);
|
||||
return orig3.apply(t, arguments);
|
||||
};
|
||||
|
||||
return cb(t);
|
||||
});
|
||||
};
|
||||
};
|
|
@ -44,6 +44,7 @@
|
|||
"@sentry/node": "6.7.0",
|
||||
"@tryghost/adapter-manager": "0.2.13",
|
||||
"@tryghost/admin-api-schema": "2.3.0",
|
||||
"@tryghost/bookshelf-plugins": "0.1.1",
|
||||
"@tryghost/bootstrap-socket": "0.2.9",
|
||||
"@tryghost/constants": "0.1.8",
|
||||
"@tryghost/email-analytics-provider-mailgun": "1.0.0",
|
||||
|
|
|
@ -1,350 +0,0 @@
|
|||
const should = require('should');
|
||||
const sinon = require('sinon');
|
||||
const Promise = require('bluebird');
|
||||
const rewire = require('rewire');
|
||||
const pagination = rewire('../../../../core/server/models/plugins/pagination');
|
||||
|
||||
describe('pagination', function () {
|
||||
let paginationUtils;
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('paginationUtils', function () {
|
||||
before(function () {
|
||||
paginationUtils = pagination.__get__('paginationUtils');
|
||||
});
|
||||
|
||||
describe('formatResponse', function () {
|
||||
let formatResponse;
|
||||
|
||||
before(function () {
|
||||
formatResponse = paginationUtils.formatResponse;
|
||||
});
|
||||
|
||||
it('returns correct pagination object for single page', function () {
|
||||
formatResponse(5, {limit: 10, page: 1}).should.eql({
|
||||
limit: 10,
|
||||
next: null,
|
||||
page: 1,
|
||||
pages: 1,
|
||||
prev: null,
|
||||
total: 5
|
||||
});
|
||||
});
|
||||
|
||||
it('returns correct pagination object for first page of many', function () {
|
||||
formatResponse(44, {limit: 5, page: 1}).should.eql({
|
||||
limit: 5,
|
||||
next: 2,
|
||||
page: 1,
|
||||
pages: 9,
|
||||
prev: null,
|
||||
total: 44
|
||||
});
|
||||
});
|
||||
|
||||
it('returns correct pagination object for middle page of many', function () {
|
||||
formatResponse(44, {limit: 5, page: 9}).should.eql({
|
||||
limit: 5,
|
||||
next: null,
|
||||
page: 9,
|
||||
pages: 9,
|
||||
prev: 8,
|
||||
total: 44
|
||||
});
|
||||
});
|
||||
|
||||
it('returns correct pagination object for last page of many', function () {
|
||||
formatResponse(44, {limit: 5, page: 3}).should.eql({
|
||||
limit: 5,
|
||||
next: 4,
|
||||
page: 3,
|
||||
pages: 9,
|
||||
prev: 2,
|
||||
total: 44
|
||||
});
|
||||
});
|
||||
|
||||
it('returns correct pagination object when page not set', function () {
|
||||
formatResponse(5, {limit: 10}).should.eql({
|
||||
limit: 10,
|
||||
next: null,
|
||||
page: 1,
|
||||
pages: 1,
|
||||
prev: null,
|
||||
total: 5
|
||||
});
|
||||
});
|
||||
|
||||
it('returns correct pagination object for limit all', function () {
|
||||
formatResponse(5, {limit: 'all'}).should.eql({
|
||||
limit: 'all',
|
||||
next: null,
|
||||
page: 1,
|
||||
pages: 1,
|
||||
prev: null,
|
||||
total: 5
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseOptions', function () {
|
||||
let parseOptions;
|
||||
|
||||
before(function () {
|
||||
parseOptions = paginationUtils.parseOptions;
|
||||
});
|
||||
|
||||
it('should use defaults if no options are passed', function () {
|
||||
parseOptions().should.eql({
|
||||
limit: 15,
|
||||
page: 1
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept numbers for limit and page', function () {
|
||||
parseOptions({
|
||||
limit: 10,
|
||||
page: 2
|
||||
}).should.eql({
|
||||
limit: 10,
|
||||
page: 2
|
||||
});
|
||||
});
|
||||
|
||||
it('should use defaults if bad options are passed', function () {
|
||||
parseOptions({
|
||||
limit: 'thelma',
|
||||
page: 'louise'
|
||||
}).should.eql({
|
||||
limit: 15,
|
||||
page: 1
|
||||
});
|
||||
});
|
||||
|
||||
it('should permit all for limit', function () {
|
||||
parseOptions({
|
||||
limit: 'all'
|
||||
}).should.eql({
|
||||
limit: 'all',
|
||||
page: 1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addLimitAndOffset', function () {
|
||||
let addLimitAndOffset;
|
||||
const collection = {};
|
||||
|
||||
before(function () {
|
||||
addLimitAndOffset = paginationUtils.addLimitAndOffset;
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
collection.query = sinon.stub().returns(collection);
|
||||
});
|
||||
|
||||
it('should add query options if limit is set', function () {
|
||||
addLimitAndOffset(collection, {limit: 5, page: 1});
|
||||
|
||||
collection.query.calledTwice.should.be.true();
|
||||
collection.query.firstCall.calledWith('limit', 5).should.be.true();
|
||||
collection.query.secondCall.calledWith('offset', 0).should.be.true();
|
||||
});
|
||||
|
||||
it('should not add query options if limit is not set', function () {
|
||||
addLimitAndOffset(collection, {page: 1});
|
||||
|
||||
collection.query.called.should.be.false();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchPage', function () {
|
||||
let model;
|
||||
let bookshelf;
|
||||
let knex;
|
||||
let mockQuery;
|
||||
|
||||
before(function () {
|
||||
paginationUtils = pagination.__get__('paginationUtils');
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
// Stub paginationUtils
|
||||
paginationUtils.parseOptions = sinon.stub();
|
||||
paginationUtils.addLimitAndOffset = sinon.stub();
|
||||
paginationUtils.formatResponse = sinon.stub().returns({});
|
||||
|
||||
// Mock out bookshelf model
|
||||
mockQuery = {
|
||||
clone: sinon.stub(),
|
||||
select: sinon.stub(),
|
||||
toQuery: sinon.stub()
|
||||
};
|
||||
mockQuery.clone.returns(mockQuery);
|
||||
mockQuery.select.returns(Promise.resolve([{aggregate: 1}]));
|
||||
|
||||
model = function () {
|
||||
};
|
||||
|
||||
model.prototype.fetchAll = sinon.stub().returns(Promise.resolve({}));
|
||||
model.prototype.query = sinon.stub();
|
||||
model.prototype.query.returns(mockQuery);
|
||||
|
||||
knex = {raw: sinon.stub().returns(Promise.resolve())};
|
||||
|
||||
bookshelf = {Model: model, knex: knex};
|
||||
|
||||
pagination(bookshelf);
|
||||
});
|
||||
|
||||
it('extends Model with fetchPage', function () {
|
||||
bookshelf.Model.prototype.should.have.ownProperty('fetchPage');
|
||||
bookshelf.Model.prototype.fetchPage.should.be.a.Function();
|
||||
});
|
||||
|
||||
it('calls all paginationUtils and methods', function (done) {
|
||||
paginationUtils.parseOptions.returns({});
|
||||
|
||||
bookshelf.Model.prototype.fetchPage().then(function () {
|
||||
sinon.assert.callOrder(
|
||||
paginationUtils.parseOptions,
|
||||
model.prototype.query,
|
||||
mockQuery.clone,
|
||||
mockQuery.select,
|
||||
paginationUtils.addLimitAndOffset,
|
||||
model.prototype.fetchAll,
|
||||
paginationUtils.formatResponse
|
||||
);
|
||||
|
||||
paginationUtils.parseOptions.calledOnce.should.be.true();
|
||||
paginationUtils.parseOptions.calledWith(undefined).should.be.true();
|
||||
|
||||
paginationUtils.addLimitAndOffset.calledOnce.should.be.true();
|
||||
paginationUtils.formatResponse.calledOnce.should.be.true();
|
||||
|
||||
model.prototype.query.calledOnce.should.be.true();
|
||||
model.prototype.query.firstCall.calledWith().should.be.true();
|
||||
|
||||
mockQuery.clone.calledOnce.should.be.true();
|
||||
mockQuery.clone.firstCall.calledWith().should.be.true();
|
||||
|
||||
mockQuery.select.calledOnce.should.be.true();
|
||||
mockQuery.select.calledWith().should.be.true();
|
||||
|
||||
model.prototype.fetchAll.calledOnce.should.be.true();
|
||||
model.prototype.fetchAll.calledWith({}).should.be.true();
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('calls all paginationUtils and methods when order set', function (done) {
|
||||
const orderOptions = {order: {id: 'DESC'}};
|
||||
paginationUtils.parseOptions.returns(orderOptions);
|
||||
|
||||
bookshelf.Model.prototype.fetchPage(orderOptions).then(function () {
|
||||
sinon.assert.callOrder(
|
||||
paginationUtils.parseOptions,
|
||||
model.prototype.query,
|
||||
mockQuery.clone,
|
||||
mockQuery.select,
|
||||
paginationUtils.addLimitAndOffset,
|
||||
model.prototype.query,
|
||||
model.prototype.fetchAll,
|
||||
paginationUtils.formatResponse
|
||||
);
|
||||
|
||||
paginationUtils.parseOptions.calledOnce.should.be.true();
|
||||
paginationUtils.parseOptions.calledWith(orderOptions).should.be.true();
|
||||
|
||||
paginationUtils.addLimitAndOffset.calledOnce.should.be.true();
|
||||
paginationUtils.formatResponse.calledOnce.should.be.true();
|
||||
|
||||
model.prototype.query.calledTwice.should.be.true();
|
||||
model.prototype.query.firstCall.calledWith().should.be.true();
|
||||
model.prototype.query.secondCall.calledWith('orderBy', 'id', 'DESC').should.be.true();
|
||||
|
||||
mockQuery.clone.calledOnce.should.be.true();
|
||||
mockQuery.clone.firstCall.calledWith().should.be.true();
|
||||
|
||||
mockQuery.select.calledOnce.should.be.true();
|
||||
mockQuery.select.calledWith().should.be.true();
|
||||
|
||||
model.prototype.fetchAll.calledOnce.should.be.true();
|
||||
model.prototype.fetchAll.calledWith(orderOptions).should.be.true();
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('calls all paginationUtils and methods when group by set', function (done) {
|
||||
const groupOptions = {groups: ['posts.id']};
|
||||
paginationUtils.parseOptions.returns(groupOptions);
|
||||
|
||||
bookshelf.Model.prototype.fetchPage(groupOptions).then(function () {
|
||||
sinon.assert.callOrder(
|
||||
paginationUtils.parseOptions,
|
||||
model.prototype.query,
|
||||
mockQuery.clone,
|
||||
mockQuery.select,
|
||||
paginationUtils.addLimitAndOffset,
|
||||
model.prototype.query,
|
||||
model.prototype.fetchAll,
|
||||
paginationUtils.formatResponse
|
||||
);
|
||||
|
||||
paginationUtils.parseOptions.calledOnce.should.be.true();
|
||||
paginationUtils.parseOptions.calledWith(groupOptions).should.be.true();
|
||||
|
||||
paginationUtils.addLimitAndOffset.calledOnce.should.be.true();
|
||||
paginationUtils.formatResponse.calledOnce.should.be.true();
|
||||
|
||||
model.prototype.query.calledTwice.should.be.true();
|
||||
model.prototype.query.firstCall.calledWith().should.be.true();
|
||||
model.prototype.query.secondCall.calledWith('groupBy', 'posts.id').should.be.true();
|
||||
|
||||
mockQuery.clone.calledOnce.should.be.true();
|
||||
mockQuery.clone.firstCall.calledWith().should.be.true();
|
||||
|
||||
mockQuery.select.calledOnce.should.be.true();
|
||||
mockQuery.select.calledWith().should.be.true();
|
||||
|
||||
model.prototype.fetchAll.calledOnce.should.be.true();
|
||||
model.prototype.fetchAll.calledWith(groupOptions).should.be.true();
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('returns expected response', function (done) {
|
||||
paginationUtils.parseOptions.returns({});
|
||||
bookshelf.Model.prototype.fetchPage().then(function (result) {
|
||||
result.should.have.ownProperty('collection');
|
||||
result.should.have.ownProperty('pagination');
|
||||
result.collection.should.be.an.Object();
|
||||
result.pagination.should.be.an.Object();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns expected response even when aggregate is empty', function (done) {
|
||||
// override aggregate response
|
||||
mockQuery.select.returns(Promise.resolve([]));
|
||||
paginationUtils.parseOptions.returns({});
|
||||
|
||||
bookshelf.Model.prototype.fetchPage().then(function (result) {
|
||||
result.should.have.ownProperty('collection');
|
||||
result.should.have.ownProperty('pagination');
|
||||
result.collection.should.be.an.Object();
|
||||
result.pagination.should.be.an.Object();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
105
yarn.lock
105
yarn.lock
|
@ -324,7 +324,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@nexes/nql-lang/-/nql-lang-0.0.1.tgz#a13c023873f9bc11b9e4e284449c6cfbeccc8011"
|
||||
integrity sha1-oTwCOHP5vBG55OKERJxs++zMgBE=
|
||||
|
||||
"@nexes/nql@0.5.2":
|
||||
"@nexes/nql@0.5.2", "@nexes/nql@^0.5.2":
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@nexes/nql/-/nql-0.5.2.tgz#64d847d563720d9c3a0f9683dde930fee518e064"
|
||||
integrity sha512-qGLwtpYkKoHI++b+zgsEHuISDUv7LC1PI/0bWd6X9bOz2zGj1nJUDFJHjwdOuLmV5q6BR60VDBKw51Mvzqkl2g==
|
||||
|
@ -596,6 +596,92 @@
|
|||
ghost-ignition "^4.6.2"
|
||||
lodash "^4.17.11"
|
||||
|
||||
"@tryghost/bookshelf-collision@^0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-collision/-/bookshelf-collision-0.1.0.tgz#6cfe9735b50122c0a01741effda223b013260ee0"
|
||||
integrity sha512-2Cs/dDg1IrDDNHs/ogz07VVAILJndODOIXRVkwLdZiEfEaiFISzi3sb1UWbDkZGmiYzjOIwZ7/ifB28kqDvYHg==
|
||||
dependencies:
|
||||
"@tryghost/errors" "^0.2.12"
|
||||
lodash "^4.17.21"
|
||||
moment-timezone "^0.5.33"
|
||||
|
||||
"@tryghost/bookshelf-custom-query@^0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-custom-query/-/bookshelf-custom-query-0.1.0.tgz#17a511f83fe51c80007563df28b35776d4092846"
|
||||
integrity sha512-sJCgqVBpQjhZohgUeV+e6EC3yDd7WJgkGP+LSsSvxRq/wxf7RxbkkTwAgd3XPiyi4LcHz/9a2Sr8HBgVDsXLIQ==
|
||||
|
||||
"@tryghost/bookshelf-eager-load@^0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-eager-load/-/bookshelf-eager-load-0.1.1.tgz#3c0b8b28669e0dedbe46c33cb3efd56e49b380af"
|
||||
integrity sha512-o/JzO7Xt7cTdHw9mrwkSaJ2uQKhmzRLvZSt7PyFx/gecD//OyajN7VTU6OfuBQ85gHaYrRAc3YRI047U0ik/eQ==
|
||||
dependencies:
|
||||
lodash "^4.17.21"
|
||||
|
||||
"@tryghost/bookshelf-filter@^0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-filter/-/bookshelf-filter-0.1.1.tgz#4976d498e28b3a48ec3cc1f6a3a69225e3fe0227"
|
||||
integrity sha512-np/J6+Lzqa9NIFhr69pE3E79XBGUinOSzvUtk6pxMukxTJtqAPkwjFOoZrAe21HHkmdzsQ51bvDAYHAPgVKHxA==
|
||||
dependencies:
|
||||
"@nexes/nql" "^0.5.2"
|
||||
"@tryghost/errors" "^0.2.12"
|
||||
"@tryghost/tpl" "^0.1.1"
|
||||
|
||||
"@tryghost/bookshelf-has-posts@^0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-has-posts/-/bookshelf-has-posts-0.1.1.tgz#9ee97057bed61f964b5b7abc79c2073f37dedac5"
|
||||
integrity sha512-zfWKA3CRE/zhV2L7vYR7zQKIn75pYPY4EExDaxpixy77mMoTactXpofsYy1leJ1MwLR0RW850MWRavIN9PTXEA==
|
||||
dependencies:
|
||||
"@tryghost/debug" "^0.1.0"
|
||||
lodash "^4.17.21"
|
||||
|
||||
"@tryghost/bookshelf-include-count@^0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-include-count/-/bookshelf-include-count-0.1.1.tgz#ef1bffc91dfe17934c6aadf298f9d8fc2200d367"
|
||||
integrity sha512-9sIei0t1ymgBha+b7vZpsStkclYXV+EHVO9LTl/WY2V2TYa4RpnbIaa7NELF/1OzQ54Iy67yqqf9J2FhtgiFWA==
|
||||
dependencies:
|
||||
lodash "^4.17.21"
|
||||
|
||||
"@tryghost/bookshelf-order@^0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-order/-/bookshelf-order-0.1.0.tgz#7666e3135ab0322531f610ea5554da227d767087"
|
||||
integrity sha512-mBJYHPw7f+cpZlkfNB5//sE8/yVscHZYhiUKiebSGZVV1CqyzixQ9NPIOsc2oRk4xiG6uHTxhH6WH4MXKTIpOA==
|
||||
dependencies:
|
||||
lodash "^4.17.21"
|
||||
|
||||
"@tryghost/bookshelf-pagination@^0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-pagination/-/bookshelf-pagination-0.1.1.tgz#3177f89f88747ed888db8276d946651f69736729"
|
||||
integrity sha512-aZrbFOnuukB+bBmcw97MxNNrVok8BP1DHgPTY8hUaERQTU009ACHb/Mtb89fMExB/qFbz4UYuToUcGqEvRR7kw==
|
||||
dependencies:
|
||||
"@tryghost/errors" "^0.2.12"
|
||||
"@tryghost/tpl" "^0.1.1"
|
||||
|
||||
"@tryghost/bookshelf-plugins@0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-plugins/-/bookshelf-plugins-0.1.1.tgz#c75db06c15e5680036915bfea4fb89dd485c0678"
|
||||
integrity sha512-b10rPGFWEkjRrO1mB+1XELBHwEOa3er9NWkTcak7thDTVnBnSjJImAdPNVt10gEck6sB8EgS8C7UyZJcGxS/gA==
|
||||
dependencies:
|
||||
"@tryghost/bookshelf-collision" "^0.1.0"
|
||||
"@tryghost/bookshelf-custom-query" "^0.1.0"
|
||||
"@tryghost/bookshelf-eager-load" "^0.1.1"
|
||||
"@tryghost/bookshelf-filter" "^0.1.1"
|
||||
"@tryghost/bookshelf-has-posts" "^0.1.1"
|
||||
"@tryghost/bookshelf-include-count" "^0.1.1"
|
||||
"@tryghost/bookshelf-order" "^0.1.0"
|
||||
"@tryghost/bookshelf-pagination" "^0.1.1"
|
||||
"@tryghost/bookshelf-search" "^0.1.0"
|
||||
"@tryghost/bookshelf-transaction-events" "^0.1.0"
|
||||
|
||||
"@tryghost/bookshelf-search@^0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-search/-/bookshelf-search-0.1.0.tgz#34e22fe6fb49cde7d1755acb4baaf156e0a2e8e7"
|
||||
integrity sha512-O6KZMz9+bnTLX6ZGAiQCI7EQ+JMlIPQX2CezxECUzh+GueTJ+31Ai6o8hhA0QUQpvp2GXDBoGhe54dW1cG5yTw==
|
||||
|
||||
"@tryghost/bookshelf-transaction-events@^0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/bookshelf-transaction-events/-/bookshelf-transaction-events-0.1.0.tgz#cb49e567777aeeb4e911fb9a6421e8450fd6ef15"
|
||||
integrity sha512-WlBTOe+/BtJWTIaH1sOay8d1uGOlHGzprg68E07WzgX6S+dEgUaazZvyFpfHcJxkjb51ysI4qG+6OJf1QUXaWw==
|
||||
|
||||
"@tryghost/bootstrap-socket@0.2.9":
|
||||
version "0.2.9"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/bootstrap-socket/-/bootstrap-socket-0.2.9.tgz#441e9287c4293fc344fc363a4ea8579144393ea6"
|
||||
|
@ -613,6 +699,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@tryghost/constants/-/constants-0.1.8.tgz#b08074cdc6f8a379209750e60c2ab62c8ba895cf"
|
||||
integrity sha512-3/w0k2JlpYjG/3tU3zjvVlrNH+kK9bjZvp7xaCkvmbsLGxQFMtlKU4fFY+rcpDVWOkywIHmKbLGmAqADgZeakA==
|
||||
|
||||
"@tryghost/debug@^0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/debug/-/debug-0.1.0.tgz#f55dc680b07fe49556c436d43e93e726e6ecbfc3"
|
||||
integrity sha512-ZoA6I9cjGMnJ4Nfcm4isOcxQEcm9Rhd1CmiBOXvTBsk442h/H4BIoOOY2Wy55oUiOf4rWxpqi9Duv/rQxJjkpA==
|
||||
dependencies:
|
||||
"@tryghost/root-utils" "^0.2.2"
|
||||
|
||||
"@tryghost/elasticsearch-bunyan@0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/elasticsearch-bunyan/-/elasticsearch-bunyan-0.1.1.tgz#5a36d81dd020825dd563b1357ae6c249580c46f5"
|
||||
|
@ -848,6 +941,14 @@
|
|||
caller "^1.0.1"
|
||||
find-root "^1.1.0"
|
||||
|
||||
"@tryghost/root-utils@^0.2.2":
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/root-utils/-/root-utils-0.2.2.tgz#bdd22faf5bd6628494748363e69de4afc8563a0e"
|
||||
integrity sha512-U2CLiEalfRX1R+7o+YrhzoIy3F7F0GvZKSMN9h5u7tuFWLq6U92QzpSHhpAFtpzgmGhIzOVsVtnVjntaUE0LlA==
|
||||
dependencies:
|
||||
caller "^1.0.1"
|
||||
find-root "^1.1.0"
|
||||
|
||||
"@tryghost/security@0.2.9":
|
||||
version "0.2.9"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/security/-/security-0.2.9.tgz#1bc323b22774c122c54a1b43a2594cdc88feba71"
|
||||
|
@ -6994,7 +7095,7 @@ module-not-found-error@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/module-not-found-error/-/module-not-found-error-1.0.1.tgz#cf8b4ff4f29640674d6cdd02b0e3bc523c2bbdc0"
|
||||
integrity sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=
|
||||
|
||||
moment-timezone@0.5.23, moment-timezone@^0.5.31:
|
||||
moment-timezone@0.5.23, moment-timezone@^0.5.31, moment-timezone@^0.5.33:
|
||||
version "0.5.23"
|
||||
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.23.tgz#7cbb00db2c14c71b19303cb47b0fb0a6d8651463"
|
||||
integrity sha512-WHFH85DkCfiNMDX5D3X7hpNH3/PUhjTGcD0U1SgfBGZxJ3qUmJh5FdvaFjcClxOvB3rzdfj4oRffbI38jEnC1w==
|
||||
|
|
Loading…
Add table
Reference in a new issue