Ghost/test/unit/models/plugins/pagination_spec.js
naz bbcc83dadb
Added support for ordering Post API resources by fields coming form posts_meta table (#12226)
refs #11729

- When ordering is done by fields from a relation (like post's `meta_title` that comes form `posts_meta` table), Bookshelf does not include those relations in the original query which caused errors. To support this usecase added a mechanism to detect fields from a relation and load those relations into query. 
- Extended ordering to include table name in ordered field name.  The information about the table name is needed to avoid using `tableName` within pagination plugin and gives path to having other than original table ordering fields (e.g. order by posts_meta table fields)
- Added test case to check ordering on posts_meta fields
- Added support for "eager loading" relations. Allows to extend query builder object with joins to related tables,
which could be used in ordering (possibly in filtering later). Bookshelf does not support ordering/filtering by proprieties coming from relations, that's why this kind of plugin and query expansion is needed
- Added note about lack of support for child relations with same property names.
2020-09-24 13:32:40 +12:00

351 lines
12 KiB
JavaScript

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();
});
});
});
});