From d59909941cdea59b7d2cf23c7e8d67d8ee3f0592 Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Mon, 31 Oct 2022 12:04:24 +0700 Subject: [PATCH 1/3] Backfilled missing columns in products table refs https://github.com/TryGhost/Toolbox/issues/464 - due to a bug with the content importer, importing a JSON file where the `products` do not contain price info will store null values in the table instead of the defaults - this ends up causing further issues because we're not populating the table for paid products - this commit is a copy of the 5.19 migration `2022-09-02-20-52-backfill-new-product-columns.js`, but adds a check for a null `t.currency`, which combined with the `t.type === paid`, should identify the rows we want to update --- ...0-31-12-03-backfill-new-product-columns.js | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 ghost/core/core/server/data/migrations/versions/5.22/2022-10-31-12-03-backfill-new-product-columns.js diff --git a/ghost/core/core/server/data/migrations/versions/5.22/2022-10-31-12-03-backfill-new-product-columns.js b/ghost/core/core/server/data/migrations/versions/5.22/2022-10-31-12-03-backfill-new-product-columns.js new file mode 100644 index 0000000000..a498d3150c --- /dev/null +++ b/ghost/core/core/server/data/migrations/versions/5.22/2022-10-31-12-03-backfill-new-product-columns.js @@ -0,0 +1,35 @@ +const logging = require('@tryghost/logging'); + +const {createTransactionalMigration} = require('../../utils'); + +module.exports = createTransactionalMigration( + async function up(knex) { + const rows = await knex('products as t') // eslint-disable-line no-restricted-syntax + .select( + 't.id as id', + 'mp.amount as monthly_price', + 'yp.amount as yearly_price', + knex.raw('coalesce(yp.currency, mp.currency) as currency') + ) + .leftJoin('stripe_prices AS mp', 't.monthly_price_id', 'mp.id') + .leftJoin('stripe_prices AS yp', 't.yearly_price_id', 'yp.id') + .where({ + 't.type': 'paid', + 't.currency': null + }); + + if (!rows.length) { + logging.info('Did not find any active paid Tiers'); + return; + } else { + logging.info(`Updating ${rows.length} Tiers with price and currency information`); + } + + for (const row of rows) { // eslint-disable-line no-restricted-syntax + await knex('products').update(row).where('id', row.id); + } + }, + async function down() { + // no-op: we don't want to reintroduce the missing data + } +); From f878e847077c0135cc9324a00b0d8035c8dda4df Mon Sep 17 00:00:00 2001 From: "Fabien \"egg\" O'Carroll" Date: Mon, 31 Oct 2022 16:49:10 +0700 Subject: [PATCH 2/3] Fixed Tiers importer not correctly mapping price data refs https://github.com/TryGhost/Toolbox/issues/464 Bceause the import does not use the API, any backwards compat code we put in the API does not get run for imports, this means we need to update the importer to map the stripe_prices data onto the products table so that we have valid data in the database. --- .../data/importer/importers/data/products.js | 47 ++++++++++++++ .../importer/importers/data/products.test.js | 64 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 ghost/core/test/unit/server/data/importer/importers/data/products.test.js diff --git a/ghost/core/core/server/data/importer/importers/data/products.js b/ghost/core/core/server/data/importer/importers/data/products.js index 2294aaf4ca..b0466f180c 100644 --- a/ghost/core/core/server/data/importer/importers/data/products.js +++ b/ghost/core/core/server/data/importer/importers/data/products.js @@ -29,6 +29,48 @@ class ProductsImporter extends BaseImporter { }; } + populatePriceData() { + const invalidRows = []; + _.each(this.dataToImport, (row) => { + if (row.slug === 'free') { + return; + } + if (row.currency && row.monthly_price && row.yearly_price) { + return; + } + if (!row.monthly_price || !row.currency) { + const monthlyStripePrice = _.find( + this.requiredFromFile.stripe_prices, + {id: row.monthly_price_id} + ) || _.find( + this.requiredExistingData.stripe_prices, + {id: row.monthly_price_id} + ); + if (!monthlyStripePrice) { + invalidRows.push(row.id); + return; + } + row.monthly_price = row.monthly_price || monthlyStripePrice.amount; + row.currency = monthlyStripePrice.currency; + } + if (!row.yearly_price) { + const yearlyStripePrice = _.find( + this.requiredFromFile.stripe_prices, + {id: row.yearly_price_id} + ) || _.find( + this.requiredExistingData.stripe_prices, + {id: row.yearly_price_id} + ); + if (!yearlyStripePrice) { + invalidRows.push(row.id); + return; + } + row.yearly_price = row.yearly_price || yearlyStripePrice.amount; + } + }); + this.dataToImport = this.dataToImport.filter(item => !invalidRows.includes(item.id)); + } + validateStripePrice() { // the stripe price either needs to exist in the current db, // or be imported as part of the same import @@ -84,6 +126,11 @@ class ProductsImporter extends BaseImporter { this.dataToImport = this.dataToImport.filter(item => !duplicateProducts.includes(item.id)); } + beforeImport() { + this.populatePriceData(); + return super.beforeImport(); + } + replaceIdentifiers() { // this has to be in replaceIdentifiers because it's after required* fields are set this.preventDuplicates(); diff --git a/ghost/core/test/unit/server/data/importer/importers/data/products.test.js b/ghost/core/test/unit/server/data/importer/importers/data/products.test.js new file mode 100644 index 0000000000..028c412bc4 --- /dev/null +++ b/ghost/core/test/unit/server/data/importer/importers/data/products.test.js @@ -0,0 +1,64 @@ +const assert = require('assert'); +const ProductsImporter = require('../../../../../../../core/server/data/importer/importers/data/products'); + +const fakeProducts = [{ + id: 'product_1', + name: 'New One', + slug: 'new-one', + active: 1, + welcome_page_url: null, + visibility: 'public', + trial_days: 0, + description: null, + type: 'paid', + created_at: '2022-10-20T11:11:32.000Z', + updated_at: '2022-10-21T04:47:42.000Z', + monthly_price_id: 'price_1', + yearly_price_id: 'price_2' +}]; + +const fakePrices = [{ + id: 'price_1', + stripe_price_id: 'price_YYYYYYYYYYYYYYYYYYYYYYYY', + stripe_product_id: 'prod_YYYYYYYYYYYYYY', + active: 1, + nickname: 'Monthly', + currency: 'usd', + amount: 500, + type: 'recurring', + interval: 'month', + description: null, + created_at: '2022-10-21T04:57:17.000Z', + updated_at: '2022-10-21T04:57:17.000Z' +}, +{ + id: 'price_2', + stripe_price_id: 'price_XXXXXXXXXXXXXXXXXXXXXXXX', + stripe_product_id: 'prod_XXXXXXXXXXXXXX', + active: 1, + nickname: 'Yearly', + currency: 'usd', + amount: 5000, + type: 'recurring', + interval: 'year', + description: null, + created_at: '2022-10-27T02:51:28.000Z', + updated_at: '2022-10-27T02:51:28.000Z' +}]; + +describe('ProductsImporter', function () { + describe('#beforeImport', function () { + it('Removes the sender_email column', function () { + const importer = new ProductsImporter({products: fakeProducts, stripe_prices: fakePrices}); + + importer.beforeImport(); + assert(importer.dataToImport.length === 1); + + const product = importer.dataToImport[0]; + + assert(product.currency === 'usd'); + assert(product.monthly_price === 500); + assert(product.yearly_price === 5000); + }); + }); +}); From 44722efe389ff17aa2c65cafd68622eb3f45f59e Mon Sep 17 00:00:00 2001 From: Ghost CI <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 10:41:31 +0000 Subject: [PATCH 3/3] v5.22.0 --- ghost/admin/package.json | 2 +- ghost/core/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ghost/admin/package.json b/ghost/admin/package.json index 778cf8f399..608a002705 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -1,6 +1,6 @@ { "name": "ghost-admin", - "version": "5.21.0", + "version": "5.22.0", "description": "Ember.js admin client for Ghost", "author": "Ghost Foundation", "homepage": "http://ghost.org", diff --git a/ghost/core/package.json b/ghost/core/package.json index fbfaa71c5f..31f90c128c 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -1,6 +1,6 @@ { "name": "ghost", - "version": "5.21.0", + "version": "5.22.0", "description": "The professional publishing platform", "author": "Ghost Foundation", "homepage": "https://ghost.org",