mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Added Product model and Member model relation (#12859)
refs https://github.com/TryGhost/Team/issues/586 - Member model now has `products` relation, sorted using `sort_order`, following convention from `labels` - Product model has handling to set `slug` from name, following convention of Label model - Updated filter plugin to handle filtering Member models by their `product` relations e.g. `product:[slug, slug]`
This commit is contained in:
parent
94766c05bf
commit
15b7485a94
5 changed files with 147 additions and 1 deletions
|
@ -28,6 +28,7 @@ const models = [
|
|||
'api-key',
|
||||
'mobiledoc-revision',
|
||||
'member',
|
||||
'product',
|
||||
'member-subscribe-event',
|
||||
'member-paid-subscription-event',
|
||||
'member-login-event',
|
||||
|
|
|
@ -17,7 +17,7 @@ const Member = ghostBookshelf.Model.extend({
|
|||
};
|
||||
},
|
||||
|
||||
relationships: ['labels', 'stripeCustomers', 'email_recipients'],
|
||||
relationships: ['products', 'labels', 'stripeCustomers', 'email_recipients'],
|
||||
|
||||
// do not delete email_recipients records when a member is destroyed. Recipient
|
||||
// records are used for analytics and historical records
|
||||
|
@ -28,11 +28,23 @@ const Member = ghostBookshelf.Model.extend({
|
|||
},
|
||||
|
||||
relationshipBelongsTo: {
|
||||
products: 'products',
|
||||
labels: 'labels',
|
||||
stripeCustomers: 'members_stripe_customers',
|
||||
email_recipients: 'email_recipients'
|
||||
},
|
||||
|
||||
products() {
|
||||
return this.belongsToMany('Product', 'members_products', 'member_id', 'product_id')
|
||||
.withPivot('sort_order')
|
||||
.query('orderBy', 'sort_order', 'ASC')
|
||||
.query((qb) => {
|
||||
// avoids bookshelf adding a `DISTINCT` to the query
|
||||
// we know the result set will already be unique and DISTINCT hurts query performance
|
||||
qb.columns('products.*');
|
||||
});
|
||||
},
|
||||
|
||||
labels: function labels() {
|
||||
return this.belongsToMany('Label', 'members_labels', 'member_id', 'label_id')
|
||||
.withPivot('sort_order')
|
||||
|
|
|
@ -25,6 +25,13 @@ const RELATIONS = {
|
|||
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',
|
||||
|
@ -60,6 +67,12 @@ const EXPANSIONS = {
|
|||
}, {
|
||||
key: 'labels',
|
||||
replacement: 'labels.slug'
|
||||
}, {
|
||||
key: 'product',
|
||||
replacement: 'products.slug'
|
||||
}, {
|
||||
key: 'products',
|
||||
replacement: 'products.slug'
|
||||
}]
|
||||
};
|
||||
|
||||
|
|
40
core/server/models/product.js
Normal file
40
core/server/models/product.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
const ghostBookshelf = require('./base');
|
||||
|
||||
const Product = ghostBookshelf.Model.extend({
|
||||
tableName: 'products',
|
||||
|
||||
async onSaving(model, _attr, options) {
|
||||
ghostBookshelf.Model.prototype.onSaving.apply(this, arguments);
|
||||
|
||||
if (model.get('name')) {
|
||||
model.set('name', model.get('name').trim());
|
||||
}
|
||||
|
||||
if (model.hasChanged('slug') || !model.get('slug')) {
|
||||
const slug = model.get('slug') || model.get('name');
|
||||
|
||||
if (!slug) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanSlug = await ghostBookshelf.Model.generateSlug(Product, slug, {
|
||||
transacting: options.transacting
|
||||
});
|
||||
|
||||
return model.set({slug: cleanSlug});
|
||||
}
|
||||
},
|
||||
|
||||
members: function members() {
|
||||
return this.belongsToMany('Member', 'members_products', 'product_id', 'member_id');
|
||||
}
|
||||
});
|
||||
|
||||
const Products = ghostBookshelf.Collection.extend({
|
||||
model: Product
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
Product: ghostBookshelf.model('Product', Product),
|
||||
Products: ghostBookshelf.collection('Products', Products)
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
const should = require('should');
|
||||
const BaseModel = require('../../../core/server/models/base');
|
||||
const {Label} = require('../../../core/server/models/label');
|
||||
const {Product} = require('../../../core/server/models/product');
|
||||
const {Member} = require('../../../core/server/models/member');
|
||||
const {MemberStripeCustomer} = require('../../../core/server/models/member-stripe-customer');
|
||||
const {StripeCustomerSubscription} = require('../../../core/server/models/stripe-customer-subscription');
|
||||
|
@ -218,5 +219,84 @@ describe('Member Model', function run() {
|
|||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('products', function () {
|
||||
it('Products can be created & added to members by the product array', async function () {
|
||||
const context = testUtils.context.admin;
|
||||
const product = await Product.add({
|
||||
name: 'Product-Add-Test'
|
||||
});
|
||||
const member = await Member.add({
|
||||
email: 'testing-products@test.member',
|
||||
products: [{
|
||||
id: product.id
|
||||
}, {
|
||||
name: 'Product-Create-Test'
|
||||
}]
|
||||
}, {
|
||||
...context,
|
||||
withRelated: ['products']
|
||||
});
|
||||
|
||||
const createdProduct = await Product.findOne({
|
||||
name: 'Product-Create-Test'
|
||||
}, context);
|
||||
|
||||
should.exist(createdProduct, 'Product should have been created');
|
||||
|
||||
const products = member.related('products').toJSON();
|
||||
|
||||
should.exist(
|
||||
products.find(model => model.name === 'Product-Create-Test')
|
||||
);
|
||||
|
||||
should.exist(
|
||||
products.find(model => model.name === 'Product-Add-Test')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filtering on products', function () {
|
||||
it('Should allow filtering on products', async function () {
|
||||
const context = testUtils.context.admin;
|
||||
|
||||
await Member.add({
|
||||
email: 'filter-test@test.member',
|
||||
products: [{
|
||||
name: 'VIP',
|
||||
slug: 'vip'
|
||||
}]
|
||||
}, context);
|
||||
|
||||
const member = await Member.findOne({
|
||||
email: 'filter-test@test.member'
|
||||
}, context);
|
||||
|
||||
should.exist(member, 'Member should have been created');
|
||||
|
||||
const product = await Product.findOne({
|
||||
slug: 'vip'
|
||||
}, context);
|
||||
|
||||
should.exist(product, 'Product should have been created');
|
||||
|
||||
const memberProduct = await BaseModel.knex('members_products').where({
|
||||
product_id: product.get('id'),
|
||||
member_id: member.get('id')
|
||||
}).select().first();
|
||||
|
||||
should.exist(memberProduct, 'Product should have been attached to member');
|
||||
|
||||
const vipProductMembers = await Member.findPage({filter: 'products:vip'});
|
||||
const foundMemberInVIP = vipProductMembers.data.find(model => model.id === member.id);
|
||||
|
||||
should.exist(foundMemberInVIP, 'Member should have been included in products filter');
|
||||
|
||||
const podcastProductMembers = await Member.findPage({filter: 'products:podcast'});
|
||||
const foundMemberInPodcast = podcastProductMembers.data.find(model => model.id === member.id);
|
||||
|
||||
should.not.exist(foundMemberInPodcast, 'Member should not have been included in products filter');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue