From 25182b7b822a31fae49adcf1a8ca1fa28b1c5af5 Mon Sep 17 00:00:00 2001 From: Fabien 'egg' O'Carroll Date: Thu, 8 Apr 2021 14:15:30 +0100 Subject: [PATCH] Added `products` and `members_products` tables (#12844) refs https://github.com/TryGhost/Team/issues/586 - Add the products table, so that we can store Products in Ghost - Add the members_products table, so that we can associate Members w/ Products - Use sort_order on the members_products table to follow the same convention in members_labels - Populate the products table with a single product, using the name from the stripe_product_name setting - Populate the members_products table with relations based on the status column of the members table Populating the tables allows us to transition from the current system, which does not care about products, into the new system, where Products are used to group members. The intention is that all existing paid members have the same product --- core/server/data/exporter/index.js | 2 + .../versions/4.3/01-add-products-table.js | 9 ++++ .../4.3/02-add-members-products-table.js | 8 +++ .../versions/4.3/03-add-default-product.js | 39 +++++++++++++++ .../4.3/04-attach-members-to-product.js | 49 +++++++++++++++++++ core/server/data/schema/schema.js | 13 +++++ test/regression/exporter/exporter_spec.js | 2 + test/unit/data/schema/integrity_spec.js | 2 +- 8 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 core/server/data/migrations/versions/4.3/01-add-products-table.js create mode 100644 core/server/data/migrations/versions/4.3/02-add-members-products-table.js create mode 100644 core/server/data/migrations/versions/4.3/03-add-default-product.js create mode 100644 core/server/data/migrations/versions/4.3/04-attach-members-to-product.js diff --git a/core/server/data/exporter/index.js b/core/server/data/exporter/index.js index c3d5145745..8954c650c4 100644 --- a/core/server/data/exporter/index.js +++ b/core/server/data/exporter/index.js @@ -20,6 +20,7 @@ const BACKUP_TABLES = [ 'labels', 'members', 'members_labels', + 'members_products', 'members_stripe_customers', 'members_stripe_customers_subscriptions', 'migrations', @@ -27,6 +28,7 @@ const BACKUP_TABLES = [ 'permissions', 'permissions_roles', 'permissions_users', + 'products', 'webhooks', 'snippets', 'tokens', diff --git a/core/server/data/migrations/versions/4.3/01-add-products-table.js b/core/server/data/migrations/versions/4.3/01-add-products-table.js new file mode 100644 index 0000000000..396628e65c --- /dev/null +++ b/core/server/data/migrations/versions/4.3/01-add-products-table.js @@ -0,0 +1,9 @@ +const {addTable} = require('../../utils'); + +module.exports = addTable('products', { + id: {type: 'string', maxlength: 24, nullable: false, primary: true}, + name: {type: 'string', maxlength: 191, nullable: false, unique: true}, + slug: {type: 'string', maxlength: 191, nullable: false, unique: true}, + created_at: {type: 'dateTime', nullable: false}, + updated_at: {type: 'dateTime', nullable: true} +}); diff --git a/core/server/data/migrations/versions/4.3/02-add-members-products-table.js b/core/server/data/migrations/versions/4.3/02-add-members-products-table.js new file mode 100644 index 0000000000..25e2fcbbb2 --- /dev/null +++ b/core/server/data/migrations/versions/4.3/02-add-members-products-table.js @@ -0,0 +1,8 @@ +const {addTable} = require('../../utils'); + +module.exports = addTable('members_products', { + id: {type: 'string', maxlength: 24, nullable: false, primary: true}, + member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true}, + product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id', cascadeDelete: true}, + sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0} +}); diff --git a/core/server/data/migrations/versions/4.3/03-add-default-product.js b/core/server/data/migrations/versions/4.3/03-add-default-product.js new file mode 100644 index 0000000000..64c1ce5663 --- /dev/null +++ b/core/server/data/migrations/versions/4.3/03-add-default-product.js @@ -0,0 +1,39 @@ +const {createTransactionalMigration} = require('../../utils'); +const ObjectID = require('bson-objectid'); +const {slugify} = require('@tryghost/string'); +const logging = require('../../../../../shared/logging'); + +module.exports = createTransactionalMigration( + async function up(knex) { + const [result] = await knex + .count('id', {as: 'total'}) + .from('products'); + + if (result.total !== 0) { + logging.warn(`Not adding default product, a product already exists`); + return; + } + + const productNameSetting = await knex + .select('value') + .from('settings') + .where('key', 'stripe_product_name') + .first(); + + const nameSettingHasValue = !!(productNameSetting && productNameSetting.value); + const name = nameSettingHasValue ? productNameSetting.value : 'Ghost Subscription'; + + logging.info(`Adding product "${name}"`); + await knex('products') + .insert({ + id: ObjectID.generate(), + name: name, + slug: slugify(name), + created_at: knex.raw(`CURRENT_TIMESTAMP`) + }); + }, + async function down(knex) { + logging.info('Removing all products'); + await knex('products').del(); + } +); diff --git a/core/server/data/migrations/versions/4.3/04-attach-members-to-product.js b/core/server/data/migrations/versions/4.3/04-attach-members-to-product.js new file mode 100644 index 0000000000..2349202f74 --- /dev/null +++ b/core/server/data/migrations/versions/4.3/04-attach-members-to-product.js @@ -0,0 +1,49 @@ +const {createTransactionalMigration} = require('../../utils'); +const ObjectID = require('bson-objectid'); +const {chunk} = require('lodash'); +const logging = require('../../../../../shared/logging'); + +module.exports = createTransactionalMigration( + async function up(knex) { + const membersWithProduct = await knex + .select('id') + .from('members') + .whereIn('status', ['comped', 'paid']); + + if (membersWithProduct.length === 0) { + logging.info(`No members found with product`); + return; + } + + const product = await knex + .select('id', 'name') + .from('products') + .first(); + + if (!product) { + logging.warn(`No product found to attach members to`); + return; + } + + logging.info(`Attaching product ${product.name} to ${membersWithProduct.length} members`); + const memberProductRelations = membersWithProduct.map((member) => { + return { + id: ObjectID.generate(), + member_id: member.id, + product_id: product.id + }; + }); + + // SQLite max variables is 999, we have 3 per insert (id, member_id, product_id) so most inserts in a query is 999/3 = 333 + const chunkSize = 333; + const memberProductRelationsChunks = chunk(memberProductRelations, chunkSize); + + for (const relations of memberProductRelationsChunks) { + await knex.insert(relations).into('members_products'); + } + }, + async function down(knex) { + logging.info('Removing all members_products relations'); + await knex('members_products').del(); + } +); diff --git a/core/server/data/schema/schema.js b/core/server/data/schema/schema.js index 514db83d1c..cf8bc0ab5f 100644 --- a/core/server/data/schema/schema.js +++ b/core/server/data/schema/schema.js @@ -359,6 +359,19 @@ module.exports = { updated_at: {type: 'dateTime', nullable: true}, updated_by: {type: 'string', maxlength: 24, nullable: true} }, + products: { + id: {type: 'string', maxlength: 24, nullable: false, primary: true}, + name: {type: 'string', maxlength: 191, nullable: false, unique: true}, + slug: {type: 'string', maxlength: 191, nullable: false, unique: true}, + created_at: {type: 'dateTime', nullable: false}, + updated_at: {type: 'dateTime', nullable: true} + }, + members_products: { + id: {type: 'string', maxlength: 24, nullable: false, primary: true}, + member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true}, + product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id', cascadeDelete: true}, + sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0} + }, members_payment_events: { id: {type: 'string', maxlength: 24, nullable: false, primary: true}, member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true}, diff --git a/test/regression/exporter/exporter_spec.js b/test/regression/exporter/exporter_spec.js index d386a76efb..4153cc679a 100644 --- a/test/regression/exporter/exporter_spec.js +++ b/test/regression/exporter/exporter_spec.js @@ -37,6 +37,7 @@ describe('Exporter', function () { 'members_login_events', 'members_paid_subscription_events', 'members_payment_events', + 'members_products', 'members_status_events', 'members_stripe_customers', 'members_stripe_customers_subscriptions', @@ -51,6 +52,7 @@ describe('Exporter', function () { 'posts_authors', 'posts_meta', 'posts_tags', + 'products', 'roles', 'roles_users', 'sessions', diff --git a/test/unit/data/schema/integrity_spec.js b/test/unit/data/schema/integrity_spec.js index 14276b9198..3fada51347 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 = '559cdbb49a7eeb5758caf0c6e3bf790d'; + const currentSchemaHash = '9d62f0a673a4f02af8a980495793ac4f'; const currentFixturesHash = '779f29a247161414025637e10e99a278'; const currentSettingsHash = '7ac732b994a5bb1565f88c8a84872964'; const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';