Added ?search= param to Admin API members endpoint (#11854)

no issue

- adds `search` bookshelf plugin that calls out to an optional `searchQuery()` method on individual models to apply model-specific SQL conditions to queries
- updated the base model's `findPage()` method to use the search plugin within `findPage` calls
- added a `searchQuery` method to the `member` model that performs a basic `LIKE %query%` for both `name` and `email` columns
- allowed the `?search=` parameter to pass through in the `options` object for member browse requests
This commit is contained in:
Kevin Ansfield 2020-05-28 10:14:02 +01:00 committed by GitHub
parent e7dc5f0bb3
commit 35f8042d7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 108 additions and 1 deletions

View File

@ -104,7 +104,8 @@ const members = {
'filter',
'order',
'debug',
'page'
'page',
'search'
],
permissions: true,
validation: {},

View File

@ -38,6 +38,9 @@ ghostBookshelf.plugin(plugins.transactionEvents);
// Load the Ghost filter plugin, which handles applying a 'filter' to findPage requests
ghostBookshelf.plugin(plugins.filter);
// Load the Ghost search plugin, which handles applying a search query to findPage requests
ghostBookshelf.plugin(plugins.search);
// Load the Ghost include count plugin, which allows for the inclusion of cross-table counts
ghostBookshelf.plugin(plugins.includeCount);
@ -885,6 +888,9 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
// Add Filter behaviour
itemCollection.applyDefaultAndCustomFilters(options);
// Apply model-specific search behaviour
itemCollection.applySearchQuery(options);
// Ensure only valid fields/columns are added to query
// and append default columns to fetch
if (options.columns) {

View File

@ -154,6 +154,11 @@ const Member = ghostBookshelf.Model.extend({
return options;
},
searchQuery: function searchQuery(queryBuilder, query) {
queryBuilder.where('name', 'like', `%${query}%`);
queryBuilder.orWhere('email', 'like', `%${query}%`);
},
toJSON(unfilteredOptions) {
const options = Member.filterOptions(unfilteredOptions, 'toJSON');
const attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
@ -169,6 +174,21 @@ const Member = ghostBookshelf.Model.extend({
return attrs;
}
}, {
/**
* Returns an array of keys permitted in a method's `options` hash, depending on the current method.
* @param {String} methodName The name of the method to check valid options for.
* @return {Array} Keys allowed in the `options` hash of the model's method.
*/
permittedOptions: function permittedOptions(methodName) {
let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
if (['findPage', 'findAll'].includes(methodName)) {
options = options.concat(['search']);
}
return options;
}
});
const Members = ghostBookshelf.Collection.extend({

View File

@ -1,5 +1,6 @@
module.exports = {
filter: require('./filter'),
search: require('./search'),
includeCount: require('./include-count'),
pagination: require('./pagination'),
collision: require('./collision'),

View File

@ -0,0 +1,18 @@
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;

View File

@ -76,6 +76,26 @@ describe('Members API', function () {
});
});
it('Can browse with search', function () {
return request
.get(localUtils.API.getApiQuery('members/?search=member1'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.members);
jsonResponse.members.should.have.length(1);
jsonResponse.members[0].email.should.equal('member1@test.com');
localUtils.API.checkResponse(jsonResponse, 'members');
localUtils.API.checkResponse(jsonResponse.members[0], 'member', 'stripe');
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
});
});
it('Can read', function () {
return request
.get(localUtils.API.getApiQuery(`members/${testUtils.DataGenerator.Content.members[0].id}/`))

View File

@ -30,6 +30,46 @@ describe('Members API', function () {
});
});
it('Can search by case-insensitive name', function () {
return request
.get(localUtils.API.getApiQuery('members/?search=egg'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.members);
jsonResponse.members.should.have.length(1);
jsonResponse.members[0].email.should.equal('member1@test.com');
localUtils.API.checkResponse(jsonResponse, 'members');
localUtils.API.checkResponse(jsonResponse.members[0], 'member', 'stripe');
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
});
});
it('Can search by case-insensitive email', function () {
return request
.get(localUtils.API.getApiQuery('members/?search=MEMBER2'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.members);
jsonResponse.members.should.have.length(1);
jsonResponse.members[0].email.should.equal('member2@test.com');
localUtils.API.checkResponse(jsonResponse, 'members');
localUtils.API.checkResponse(jsonResponse.members[0], 'member', 'stripe');
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
});
});
it('Add should fail when passing incorrect email_type query parameter', function () {
const member = {
name: 'test',

View File

@ -17,6 +17,7 @@ const expectedProperties = {
slug: ['slug'],
invites: ['invites', 'meta'],
themes: ['themes'],
members: ['members', 'meta'],
post: _(schema.posts)
.keys()