0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Used models internally and for exported API (#195)

no-issue

Using models internally and in the exported API means that we avoid expensive
`toJSON` calls, which affects performance when looping through large lists of
members. It also allows us to take advantage of the new relations used in the
models.

The addition of "ByID" methods for linking stripe customers and setting
complimentary subscriptions allows bulk imports to avoid the overhead of creating
a model for each members, instead passing an id string. n.b. currently the impl
_does_ still create models, but it makes it easier to optimise and refactor in the 
future.
This commit is contained in:
Fabien 'egg' O'Carroll 2020-08-12 12:57:28 +01:00 committed by GitHub
parent 85800c57f7
commit 117309b4e8
4 changed files with 108 additions and 233 deletions

View file

@ -100,6 +100,11 @@ module.exports = function MembersApi({
getSubject
});
const users = Users({
stripe,
Member
});
async function sendEmailWithMagicLink({email, requestedType, payload, options = {forceEmailType: false}}) {
if (options.forceEmailType) {
return magicLinkService.sendMagicLink({email, payload, subject: email, type: requestedType});
@ -117,11 +122,6 @@ module.exports = function MembersApi({
return magicLinkService.getMagicLink({email, subject: email, type: 'signin'});
}
const users = Users({
stripe,
Member
});
async function getMemberDataFromMagicLinkToken(token) {
const email = await magicLinkService.getUserFromToken(token);
const {labels = [], name = '', oldEmail} = await magicLinkService.getPayloadFromToken(token);
@ -148,7 +148,11 @@ module.exports = function MembersApi({
}
async function getMemberIdentityData(email) {
return users.get({email});
const model = await users.get({email}, {withRelated: ['stripeSubscriptions', 'stripeSubscriptions.customer']});
if (!model) {
return null;
}
return model.toJSON();
}
async function getMemberIdentityToken(email) {
@ -261,10 +265,10 @@ module.exports = function MembersApi({
return res.end('Unauthorized');
}
const member = email ? await users.get({email}) : null;
const member = email ? await users.get({email}, {withRelated: ['stripeSubscriptions']}) : null;
// Do not allow members already with a subscription to initiate a new checkout session
if (member && member.stripe.subscriptions.length > 0) {
if (member && member.related('stripeSubscriptions').length > 0) {
res.writeHead(403);
return res.end('No permission');
}
@ -374,8 +378,8 @@ module.exports = function MembersApi({
const payerName = _.get(customer, 'subscriptions.data[0].default_payment_method.billing_details.name');
if (payerName && !member.name) {
await users.update({name: payerName}, {id: member.id});
if (payerName && !member.get('name')) {
await users.update({name: payerName}, {id: member.get('id')});
}
const emailType = 'signup';
@ -411,7 +415,7 @@ module.exports = function MembersApi({
const claims = await decodeToken(identity);
const email = claims.sub;
member = email ? await users.get({email}) : null;
member = email ? await users.get({email}, {withRelated: ['stripeSubscriptions']}) : null;
if (!member) {
throw new common.errors.BadRequestError({
@ -429,7 +433,9 @@ module.exports = function MembersApi({
message: 'Updating subscription failed! Could not find plan'
});
}
const subscription = member.stripe.subscriptions.find(sub => sub.id === subscriptionId);
const subscription = member.related('stripeSubscriptions').models.find(
subscription => subscription.get('subscription_id') === subscriptionId
);
if (!subscription) {
res.writeHead(403);
return res.end('No permission');
@ -442,7 +448,7 @@ module.exports = function MembersApi({
});
}
const subscriptionUpdate = {
id: subscription.id
id: subscription.get('subscription_id')
};
if (cancelAtPeriodEnd !== undefined) {
subscriptionUpdate.cancel_at_period_end = !!(cancelAtPeriodEnd);

View file

@ -46,16 +46,16 @@ module.exports = function ({
return;
}
const customers = (await StripeCustomer.findAll({
filter: `member_id:${member.id}`
})).toJSON();
if (!member.relations.stripeCustomers) {
await member.load(['stripeCustomers']);
}
const subscriptions = await customers.reduce(async (subscriptionsPromise, customer) => {
const customerSubscriptions = await StripeCustomerSubscription.findAll({
filter: `customer_id:${customer.customer_id}`
});
return (await subscriptionsPromise).concat(customerSubscriptions.toJSON());
}, []);
if (!member.relations.stripeSubscriptions) {
await member.load(['stripeSubscriptions', 'stripeSubscriptions.customer']);
}
const customers = member.related('stripeCustomers').toJSON();
const subscriptions = member.related('stripeSubscriptions').toJSON();
return {
customers: customers,

View file

@ -5,15 +5,6 @@ const api = require('./api');
const STRIPE_API_VERSION = '2019-09-09';
const CURRENCY_SYMBOLS = {
usd: '$',
aud: '$',
cad: '$',
gbp: '£',
eur: '€',
inr: '₹'
};
module.exports = class StripePaymentProcessor {
constructor(config, storage, logging) {
this.logging = logging;
@ -244,7 +235,7 @@ module.exports = class StripePaymentProcessor {
payment_method_types: ['card'],
success_url: options.successUrl || this._billingSuccessUrl,
cancel_url: options.cancelUrl || this._billingCancelUrl,
customer_email: member.email,
customer_email: member.get('email'),
setup_intent_data: {
metadata: {
customer_id: customer.id
@ -296,35 +287,7 @@ module.exports = class StripePaymentProcessor {
async getSubscriptions(member) {
const metadata = await this.storage.get(member);
const customers = metadata.customers.reduce((customers, customer) => {
return Object.assign(customers, {
[customer.customer_id]: {
id: customer.customer_id,
name: customer.name,
email: customer.email
}
});
}, {});
return metadata.subscriptions.map((subscription) => {
return {
id: subscription.subscription_id,
customer: customers[subscription.customer_id],
plan: {
id: subscription.plan_id,
nickname: subscription.plan_nickname,
interval: subscription.plan_interval,
amount: subscription.plan_amount,
currency: String.prototype.toUpperCase.call(subscription.plan_currency),
currency_symbol: CURRENCY_SYMBOLS[subscription.plan_currency]
},
status: subscription.status,
start_date: subscription.start_date,
default_payment_card_last4: subscription.default_payment_card_last4,
cancel_at_period_end: subscription.cancel_at_period_end,
current_period_end: subscription.current_period_end
};
});
return metadata.subscriptions;
}
async setComplimentarySubscription(member) {
@ -438,11 +401,11 @@ module.exports = class StripePaymentProcessor {
}
async _updateCustomer(member, customer) {
debug(`Attaching customer to member ${member.email} ${customer.id}`);
debug(`Attaching customer to member ${member.get('email')} ${customer.id}`);
await this.storage.set({
customer: {
customer_id: customer.id,
member_id: member.id,
member_id: member.get('id'),
name: customer.name,
email: customer.email
}
@ -500,9 +463,9 @@ module.exports = class StripePaymentProcessor {
}
}
debug(`Creating customer for member ${member.email}`);
debug(`Creating customer for member ${member.get('email')}`);
const customer = await create(this._stripe, 'customers', {
email: member.email
email: member.get('email')
});
await this._updateCustomer(member, customer);

View file

@ -1,184 +1,89 @@
const _ = require('lodash');
const debug = require('ghost-ignition').debug('users');
const common = require('./common');
module.exports = function ({
stripe,
Member
}) {
async function createMember({email, name, note, labels, geolocation}) {
const model = await Member.add({
async function get(data, options) {
debug(`get id:${data.id} email:${data.email}`);
return Member.findOne(data, options);
}
async function destroy(data, options) {
debug(`destroy id:${data.id} email:${data.email}`);
const member = await Member.findOne(data, options);
if (!member) {
return;
}
if (stripe && options.cancelStripeSubscriptions) {
await stripe.cancelStripeSubscriptions(member);
}
return Member.destroy({
id: data.id
}, options);
}
async function update(data, options) {
debug(`update id:${options.id}`);
return Member.edit(_.pick(data, [
'email',
'name',
'note',
'labels',
'geolocation'
]), options);
}
async function list(options = {}) {
return Member.findPage(options);
}
async function create({email, name, note, labels, geolocation}, options) {
debug(`create email:${email}`);
/** Member.add model method expects label object array*/
if (labels) {
labels.forEach((label, index) => {
if (_.isString(label)) {
labels[index] = {name: label};
}
});
}
return Member.add({
email,
name,
note,
labels,
geolocation
});
const member = model.toJSON();
return member;
}, options);
}
async function getMember(data, options = {}) {
if (!data.email && !data.id && !data.uuid) {
return null;
}
const model = await Member.findOne(data, options);
if (!model) {
return null;
}
const member = model.toJSON(options);
return member;
}
async function updateMember(data, options = {}) {
const attrs = _.pick(data, ['email', 'name', 'note', 'subscribed', 'geolocation']);
const model = await Member.edit(attrs, options);
const member = model.toJSON(options);
return member;
}
function deleteMember(options) {
options = options || {};
return Member.destroy(options);
}
function listMembers(options) {
return Member.findPage(options).then((models) => {
return {
members: models.data.map(model => model.toJSON(options)),
meta: models.meta
};
});
}
async function getStripeSubscriptions(member) {
if (!stripe) {
return [];
}
return await stripe.getActiveSubscriptions(member);
}
async function cancelStripeSubscriptions(member) {
if (stripe) {
await stripe.cancelAllSubscriptions(member);
}
}
async function setComplimentarySubscription(member) {
if (stripe) {
await stripe.setComplimentarySubscription(member);
}
}
async function cancelComplimentarySubscription(member) {
if (stripe) {
await stripe.cancelComplimentarySubscription(member);
}
}
async function linkStripeCustomer(id, member) {
if (stripe) {
await stripe.linkStripeCustomer(id, member);
}
}
async function getStripeCustomer(id) {
if (stripe) {
return await stripe.getStripeCustomer(id);
}
}
async function get(data, options) {
debug(`get id:${data.id} email:${data.email}`);
const member = await getMember(data, options);
if (!member) {
return member;
}
try {
const subscriptions = await getStripeSubscriptions(member);
return Object.assign(member, {
stripe: {
subscriptions
}
});
} catch (err) {
common.logging.error(err);
return null;
}
}
async function destroy(data, options) {
debug(`destroy id:${data.id} email:${data.email}`);
const member = await getMember(data, options);
if (!member) {
return;
}
await cancelStripeSubscriptions(member);
return deleteMember(data);
}
async function update(data, options) {
debug(`update id:${options.id}`);
const member = await updateMember(data, options);
if (!member) {
return member;
}
try {
const subscriptions = await getStripeSubscriptions(member);
return Object.assign(member, {
stripe: {
subscriptions
}
});
} catch (err) {
common.logging.error(err);
return null;
}
}
async function list(options) {
const {meta, members} = await listMembers(options);
const membersWithSubscriptions = await Promise.all(members.map(async function (member) {
const subscriptions = await getStripeSubscriptions(member);
return Object.assign(member, {
stripe: {
subscriptions
}
});
}));
return {
meta,
members: membersWithSubscriptions
function safeStripe(methodName) {
return async function (...args) {
if (stripe) {
return await stripe[methodName](...args);
}
};
}
async function create(data) {
debug(`create email:${data.email}`);
/** Member.add model method expects label object array*/
if (data.labels) {
data.labels.forEach((label, index) => {
if (_.isString(label)) {
data.labels[index] = {name: label};
}
});
async function linkStripeCustomerById(customerId, memberId) {
if (!stripe) {
return;
}
const member = await get({id: memberId});
return stripe.linkStripeCustomer(customerId, member);
}
const member = await createMember(data);
return member;
async function setComplimentarySubscriptionById(memberId) {
if (!stripe) {
return;
}
const member = await get({id: memberId});
return stripe.setComplimentarySubscription(member);
}
return {
@ -187,11 +92,12 @@ module.exports = function ({
list,
get,
destroy,
getStripeSubscriptions,
setComplimentarySubscription,
cancelComplimentarySubscription,
cancelStripeSubscriptions,
getStripeCustomer,
linkStripeCustomer
setComplimentarySubscription: safeStripe('setComplimentarySubscription'),
setComplimentarySubscriptionById,
cancelComplimentarySubscription: safeStripe('cancelComplimentarySubscription'),
cancelStripeSubscriptions: safeStripe('cancelComplimentarySubscription'),
getStripeCustomer: safeStripe('getStripeCustomer'),
linkStripeCustomer: safeStripe('linkStripeCustomer'),
linkStripeCustomerById
};
};