0
Fork 0
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:
Fabien 'egg' O'Carroll 2021-04-08 18:01:49 +01:00 committed by GitHub
parent 94766c05bf
commit 15b7485a94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 147 additions and 1 deletions

View file

@ -28,6 +28,7 @@ const models = [
'api-key',
'mobiledoc-revision',
'member',
'product',
'member-subscribe-event',
'member-paid-subscription-event',
'member-login-event',

View file

@ -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')

View file

@ -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'
}]
};

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

View file

@ -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');
});
});
});