From a79ed1170d7b1946dc2e134b52ca1427c6ef9c3e Mon Sep 17 00:00:00 2001 From: Fabien 'egg' O'Carroll Date: Wed, 27 Jan 2021 14:06:21 +0000 Subject: [PATCH] Added status property to members (#12570) refs #12160 This flag will allow us easier filtering of members via the API * Added status column to members table This flag will be used to determine if a member is free or paid, rather than relying on joins with the customers and subscriptions tables. * Added migration to populate members.status As we add the column with a default value of "free" we only need to care about the paid members here. We also preemptively handle migrations for SQLite where there are > 998 paid members. --- .../utils/serializers/output/members.js | 4 +- .../4.0/02-add-status-column-to-members.js | 11 +++++ .../03-populate-status-column-for-members.js | 45 +++++++++++++++++++ core/server/data/schema/schema.js | 5 +++ core/server/models/member.js | 1 + test/api-acceptance/admin/utils_v3.js | 1 + test/regression/api/v3/admin/utils.js | 1 + test/unit/data/schema/integrity_spec.js | 2 +- 8 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 core/server/data/migrations/versions/4.0/02-add-status-column-to-members.js create mode 100644 core/server/data/migrations/versions/4.0/03-populate-status-column-for-members.js diff --git a/core/server/api/canary/utils/serializers/output/members.js b/core/server/api/canary/utils/serializers/output/members.js index 845508b836..4b1db072a2 100644 --- a/core/server/api/canary/utils/serializers/output/members.js +++ b/core/server/api/canary/utils/serializers/output/members.js @@ -104,7 +104,8 @@ function serializeMember(member, options) { email_count: json.email_count, email_opened_count: json.email_opened_count, email_open_rate: json.email_open_rate, - email_recipients: json.email_recipients + email_recipients: json.email_recipients, + status: json.status }; } @@ -152,6 +153,7 @@ function createSerializer(debugString, serialize) { * @prop {number} email_opened_count * @prop {number} email_open_rate * @prop {null|SerializedEmailRecipient[]} email_recipients + * @prop {'free'|'paid'} status */ /** diff --git a/core/server/data/migrations/versions/4.0/02-add-status-column-to-members.js b/core/server/data/migrations/versions/4.0/02-add-status-column-to-members.js new file mode 100644 index 0000000000..c7c41440fa --- /dev/null +++ b/core/server/data/migrations/versions/4.0/02-add-status-column-to-members.js @@ -0,0 +1,11 @@ +const {createAddColumnMigration} = require('../../utils'); + +module.exports = createAddColumnMigration('members', 'status', { + type: 'string', + maxlength: 50, + nullable: false, + defaultTo: 'free', + validations: { + isIn: [['free', 'paid']] + } +}); diff --git a/core/server/data/migrations/versions/4.0/03-populate-status-column-for-members.js b/core/server/data/migrations/versions/4.0/03-populate-status-column-for-members.js new file mode 100644 index 0000000000..8ede32a9e7 --- /dev/null +++ b/core/server/data/migrations/versions/4.0/03-populate-status-column-for-members.js @@ -0,0 +1,45 @@ +const {chunk} = require('lodash'); +const {createTransactionalMigration} = require('../../utils'); +const logging = require('../../../../../shared/logging'); + +module.exports = createTransactionalMigration( + async function up(knex) { + logging.info('Updating members.status based on members_stripe_customers_subscriptions.status'); + const paidMemberIds = (await knex('members') + .select('members.id') + .innerJoin( + 'members_stripe_customers', + 'members.id', + 'members_stripe_customers.member_id' + ).innerJoin( + 'members_stripe_customers_subscriptions', + function () { + this.on( + 'members_stripe_customers.customer_id', + 'members_stripe_customers_subscriptions.customer_id' + ).onIn( + 'members_stripe_customers_subscriptions.status', + ['active', 'trialing', 'past_due', 'unpaid'] + ); + } + )).map(({id}) => id); + + // Umm? Well... The current version of SQLite3 bundled with Ghost supports + // a maximum of 999 variables, we use one variable for the SET value + // and so we're left with 998 for our WHERE IN clause values + const chunkSize = 998; + const paidMemberIdChunks = chunk(paidMemberIds, chunkSize); + + for (const paidMemberIdsChunk of paidMemberIdChunks) { + await knex('members') + .update('status', 'paid') + .whereIn('id', paidMemberIdsChunk); + } + }, + async function down(knex) { + logging.info('Updating all members status to "free"'); + return knex('members').update({ + status: 'free' + }); + } +); diff --git a/core/server/data/schema/schema.js b/core/server/data/schema/schema.js index e093b4de02..faf7b2526c 100644 --- a/core/server/data/schema/schema.js +++ b/core/server/data/schema/schema.js @@ -373,6 +373,11 @@ module.exports = { id: {type: 'string', maxlength: 24, nullable: false, primary: true}, uuid: {type: 'string', maxlength: 36, nullable: true, unique: true, validations: {isUUID: true}}, email: {type: 'string', maxlength: 191, nullable: false, unique: true, validations: {isEmail: true}}, + status: { + type: 'string', maxlength: 50, nullable: false, defaultTo: 'free', validations: { + isIn: [['free', 'paid']] + } + }, name: {type: 'string', maxlength: 191, nullable: true}, note: {type: 'string', maxlength: 2000, nullable: true}, geolocation: {type: 'string', maxlength: 2000, nullable: true}, diff --git a/core/server/models/member.js b/core/server/models/member.js index 7d1eb1b4d4..6b24aef521 100644 --- a/core/server/models/member.js +++ b/core/server/models/member.js @@ -10,6 +10,7 @@ const Member = ghostBookshelf.Model.extend({ defaults() { return { + status: 'free', subscribed: true, uuid: uuid.v4(), email_count: 0, diff --git a/test/api-acceptance/admin/utils_v3.js b/test/api-acceptance/admin/utils_v3.js index cfd53fa7c6..0258338450 100644 --- a/test/api-acceptance/admin/utils_v3.js +++ b/test/api-acceptance/admin/utils_v3.js @@ -95,6 +95,7 @@ const expectedProperties = { .concat('avatar_image') .concat('comped') .concat('labels') + .without('status') , member_signin_url: ['member_id', 'url'], role: _(schema.roles) diff --git a/test/regression/api/v3/admin/utils.js b/test/regression/api/v3/admin/utils.js index af4fdfc5fb..932fdfd43b 100644 --- a/test/regression/api/v3/admin/utils.js +++ b/test/regression/api/v3/admin/utils.js @@ -61,6 +61,7 @@ const expectedProperties = { .concat('avatar_image') .concat('comped') .concat('labels') + .without('status') , member_signin_url: ['member_id', 'url'], role: _(schema.roles) diff --git a/test/unit/data/schema/integrity_spec.js b/test/unit/data/schema/integrity_spec.js index 02c7b3ee0b..2866139ee5 100644 --- a/test/unit/data/schema/integrity_spec.js +++ b/test/unit/data/schema/integrity_spec.js @@ -32,7 +32,7 @@ const defaultSettings = require('../../../../core/server/data/schema/default-set */ describe('DB version integrity', function () { // Only these variables should need updating - const currentSchemaHash = 'c61ecbc8c11f62d1d350c66ee1ac3151'; + const currentSchemaHash = '47e9b182da4ea9c056878354cc291191'; const currentFixturesHash = '370d0da0ab7c45050b2ff30bce8896ba'; const currentSettingsHash = '162f9294cc427eb32bc0577006c385ce'; const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';