mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 03:44:29 +03:00
Added ?paid
query parameter to Admin API members browse endpoint (#11892)
no issue - NQL does not support the relationship setup that members->stripe customer<->stripe subscriptions uses so it wasn't possible to use the `filter` param to query against having an active subscription - adds `customQuery` bookshelf plugin that allows customisation of SQL query used in `findPage` method by individual models - use `customQuery` in Member model to set up joins and conditionals to select free/paid members when `options.paid` is present - allow `?paid` param through API and permitted options for member model
This commit is contained in:
parent
f1291058ec
commit
f4d9a41d3b
@ -131,7 +131,8 @@ const members = {
|
||||
'order',
|
||||
'debug',
|
||||
'page',
|
||||
'search'
|
||||
'search',
|
||||
'paid'
|
||||
],
|
||||
permissions: true,
|
||||
validation: {},
|
||||
|
@ -36,6 +36,9 @@ ghostBookshelf.plugin('registry');
|
||||
// Add committed/rollback events.
|
||||
ghostBookshelf.plugin(plugins.transactionEvents);
|
||||
|
||||
// Load the Ghost custom-query plugin, which applying a custom query to findPage requests
|
||||
ghostBookshelf.plugin(plugins.customQuery);
|
||||
|
||||
// Load the Ghost filter plugin, which handles applying a 'filter' to findPage requests
|
||||
ghostBookshelf.plugin(plugins.filter);
|
||||
|
||||
@ -886,6 +889,9 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
||||
// Set this to true or pass ?debug=true as an API option to get output
|
||||
itemCollection.debug = options.debug && config.get('env') !== 'production';
|
||||
|
||||
// Apply model-specific query behaviour
|
||||
itemCollection.applyCustomQuery(options);
|
||||
|
||||
// Add Filter behaviour
|
||||
itemCollection.applyDefaultAndCustomFilters(options);
|
||||
|
||||
|
@ -159,6 +159,40 @@ const Member = ghostBookshelf.Model.extend({
|
||||
queryBuilder.orWhere('email', 'like', `%${query}%`);
|
||||
},
|
||||
|
||||
// TODO: hacky way to filter by members with an active subscription,
|
||||
// replace with a proper way to do this via filter param.
|
||||
// NOTE: assumes members will have a single subscription
|
||||
customQuery: function customQuery(queryBuilder, options) {
|
||||
if (options.paid === true) {
|
||||
queryBuilder.innerJoin(
|
||||
'members_stripe_customers',
|
||||
'members.id',
|
||||
'members_stripe_customers.member_id'
|
||||
);
|
||||
queryBuilder.innerJoin(
|
||||
'members_stripe_customers_subscriptions',
|
||||
function () {
|
||||
this.on(
|
||||
'members_stripe_customers.customer_id',
|
||||
'members_stripe_customers_subscriptions.customer_id'
|
||||
).andOn(
|
||||
'members_stripe_customers_subscriptions.status',
|
||||
ghostBookshelf.knex.raw('?', ['active'])
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (options.paid === false) {
|
||||
queryBuilder.leftJoin(
|
||||
'members_stripe_customers',
|
||||
'members.id',
|
||||
'members_stripe_customers.member_id'
|
||||
);
|
||||
queryBuilder.whereNull('members_stripe_customers.member_id');
|
||||
}
|
||||
},
|
||||
|
||||
toJSON(unfilteredOptions) {
|
||||
const options = Member.filterOptions(unfilteredOptions, 'toJSON');
|
||||
const attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
|
||||
@ -184,7 +218,8 @@ const Member = ghostBookshelf.Model.extend({
|
||||
let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
|
||||
|
||||
if (['findPage', 'findAll'].includes(methodName)) {
|
||||
options = options.concat(['search']);
|
||||
// TODO: remove 'paid' once it's possible to use in a filter
|
||||
options = options.concat(['search', 'paid']);
|
||||
}
|
||||
|
||||
return options;
|
||||
|
16
core/server/models/plugins/custom-query.js
Normal file
16
core/server/models/plugins/custom-query.js
Normal file
@ -0,0 +1,16 @@
|
||||
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,5 +1,6 @@
|
||||
module.exports = {
|
||||
filter: require('./filter'),
|
||||
customQuery: require('./custom-query'),
|
||||
search: require('./search'),
|
||||
includeCount: require('./include-count'),
|
||||
pagination: require('./pagination'),
|
||||
|
@ -42,7 +42,7 @@ describe('Members API', function () {
|
||||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.members);
|
||||
jsonResponse.members.should.have.length(2);
|
||||
jsonResponse.members.should.have.length(3);
|
||||
localUtils.API.checkResponse(jsonResponse.members[0], 'member', 'stripe');
|
||||
|
||||
testUtils.API.isISO8601(jsonResponse.members[0].created_at).should.be.true();
|
||||
@ -51,7 +51,7 @@ describe('Members API', function () {
|
||||
jsonResponse.meta.pagination.should.have.property('page', 1);
|
||||
jsonResponse.meta.pagination.should.have.property('limit', 15);
|
||||
jsonResponse.meta.pagination.should.have.property('pages', 1);
|
||||
jsonResponse.meta.pagination.should.have.property('total', 2);
|
||||
jsonResponse.meta.pagination.should.have.property('total', 3);
|
||||
jsonResponse.meta.pagination.should.have.property('next', null);
|
||||
jsonResponse.meta.pagination.should.have.property('prev', null);
|
||||
});
|
||||
@ -96,6 +96,26 @@ describe('Members API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('Can browse with paid', function () {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery('members/?paid=true'))
|
||||
.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('paid@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}/`))
|
||||
@ -338,8 +358,8 @@ describe('Members API', function () {
|
||||
should.exist(jsonResponse.total_on_date);
|
||||
should.exist(jsonResponse.new_today);
|
||||
|
||||
// 2 from fixtures, 2 from above posts, 2 from above import
|
||||
jsonResponse.total.should.equal(6);
|
||||
// 3 from fixtures, 2 from above posts, 2 from above import
|
||||
jsonResponse.total.should.equal(7);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -273,8 +273,8 @@ describe('Members API', function () {
|
||||
should.exist(jsonResponse.total_on_date);
|
||||
should.exist(jsonResponse.new_today);
|
||||
|
||||
// 2 from fixtures and 5 imported in previous tests
|
||||
jsonResponse.total.should.equal(6);
|
||||
// 3 from fixtures and 5 imported in previous tests
|
||||
jsonResponse.total.should.equal(7);
|
||||
});
|
||||
});
|
||||
|
||||
@ -297,8 +297,8 @@ describe('Members API', function () {
|
||||
should.exist(jsonResponse.total_on_date);
|
||||
should.exist(jsonResponse.new_today);
|
||||
|
||||
// 2 from fixtures and 5 imported in previous tests
|
||||
jsonResponse.total.should.equal(6);
|
||||
// 3 from fixtures and 5 imported in previous tests
|
||||
jsonResponse.total.should.equal(7);
|
||||
});
|
||||
});
|
||||
|
||||
@ -321,8 +321,8 @@ describe('Members API', function () {
|
||||
should.exist(jsonResponse.total_on_date);
|
||||
should.exist(jsonResponse.new_today);
|
||||
|
||||
// 2 from fixtures and 5 imported in previous tests
|
||||
jsonResponse.total.should.equal(6);
|
||||
// 3 from fixtures and 5 imported in previous tests
|
||||
jsonResponse.total.should.equal(7);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -316,6 +316,11 @@ DataGenerator.Content = {
|
||||
{
|
||||
id: ObjectId.generate(),
|
||||
email: 'member2@test.com'
|
||||
},
|
||||
{
|
||||
id: ObjectId.generate(),
|
||||
email: 'paid@test.com',
|
||||
name: 'Egon Spengler'
|
||||
}
|
||||
],
|
||||
|
||||
@ -332,6 +337,34 @@ DataGenerator.Content = {
|
||||
}
|
||||
],
|
||||
|
||||
members_stripe_customers: [
|
||||
{
|
||||
id: ObjectId.generate(),
|
||||
member_id: null, // relation added later
|
||||
customer_id: 'cus_HR3tBmNhx4QsZY',
|
||||
name: 'Egon Spengler',
|
||||
email: 'paid@test.com'
|
||||
}
|
||||
],
|
||||
|
||||
members_stripe_customers_subscriptions: [
|
||||
{
|
||||
id: ObjectId.generate(),
|
||||
customer_id: 'cus_HR3tBmNhx4QsZY',
|
||||
subscription_id: 'sub_HR3tLNgGAHsa7b',
|
||||
plan_id: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730bb8',
|
||||
status: 'active',
|
||||
cancel_at_period_end: false,
|
||||
current_period_end: '2020-07-09 19:01:20',
|
||||
start_date: '2020-06-09 19:01:20',
|
||||
default_payment_card_last4: '4242',
|
||||
plan_nickname: 'Monthly',
|
||||
plan_interval: 'month',
|
||||
plan_amount: '1000',
|
||||
plan_currency: 'usd'
|
||||
}
|
||||
],
|
||||
|
||||
webhooks: [
|
||||
{
|
||||
id: ObjectId.generate(),
|
||||
@ -411,6 +444,7 @@ DataGenerator.Content.api_keys[0].integration_id = DataGenerator.Content.integra
|
||||
DataGenerator.Content.api_keys[1].integration_id = DataGenerator.Content.integrations[0].id;
|
||||
DataGenerator.Content.emails[0].post_id = DataGenerator.Content.posts[0].id;
|
||||
DataGenerator.Content.emails[1].post_id = DataGenerator.Content.posts[1].id;
|
||||
DataGenerator.Content.members_stripe_customers[0].member_id = DataGenerator.Content.members[2].id;
|
||||
|
||||
DataGenerator.forKnex = (function () {
|
||||
function createBasic(overrides) {
|
||||
@ -875,7 +909,8 @@ DataGenerator.forKnex = (function () {
|
||||
|
||||
const members = [
|
||||
createMember(DataGenerator.Content.members[0]),
|
||||
createMember(DataGenerator.Content.members[1])
|
||||
createMember(DataGenerator.Content.members[1]),
|
||||
createMember(DataGenerator.Content.members[2])
|
||||
];
|
||||
|
||||
const labels = [
|
||||
@ -883,12 +918,18 @@ DataGenerator.forKnex = (function () {
|
||||
];
|
||||
|
||||
const members_labels = [
|
||||
{
|
||||
id: ObjectId.generate(),
|
||||
member_id: DataGenerator.Content.members[0].id,
|
||||
label_id: DataGenerator.Content.labels[0].id,
|
||||
sort_order: 0
|
||||
}
|
||||
createMembersLabels(
|
||||
DataGenerator.Content.members[0].id,
|
||||
DataGenerator.Content.labels[0].id
|
||||
)
|
||||
];
|
||||
|
||||
const members_stripe_customers = [
|
||||
createBasic(DataGenerator.Content.members_stripe_customers[0])
|
||||
];
|
||||
|
||||
const stripe_customer_subscriptions = [
|
||||
createBasic(DataGenerator.Content.members_stripe_customers_subscriptions[0])
|
||||
];
|
||||
|
||||
return {
|
||||
@ -909,6 +950,8 @@ DataGenerator.forKnex = (function () {
|
||||
createMember,
|
||||
createLabel,
|
||||
createMembersLabels,
|
||||
createMembersStripeCustomer: createBasic,
|
||||
createStripeCustomerSubscription: createBasic,
|
||||
createInvite,
|
||||
createWebhook,
|
||||
createIntegration,
|
||||
@ -927,7 +970,9 @@ DataGenerator.forKnex = (function () {
|
||||
emails,
|
||||
labels,
|
||||
members,
|
||||
members_labels
|
||||
members_labels,
|
||||
members_stripe_customers,
|
||||
stripe_customer_subscriptions
|
||||
};
|
||||
}());
|
||||
|
||||
|
@ -483,8 +483,17 @@ fixtures = {
|
||||
});
|
||||
|
||||
member.labels = memberLabelRelations;
|
||||
|
||||
return models.Member.add(member, module.exports.context.internal);
|
||||
});
|
||||
}).then(function () {
|
||||
return Promise.each(_.cloneDeep(DataGenerator.forKnex.members_stripe_customers), function (customer) {
|
||||
return models.MemberStripeCustomer.add(customer, module.exports.context.internal);
|
||||
});
|
||||
}).then(function () {
|
||||
return Promise.each(_.cloneDeep(DataGenerator.forKnex.stripe_customer_subscriptions), function (subscription) {
|
||||
return models.StripeCustomerSubscription.add(subscription, module.exports.context.internal);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user