diff --git a/ghost/core/core/server/data/exporter/table-lists.js b/ghost/core/core/server/data/exporter/table-lists.js index e0b037227f7..1a001c0743d 100644 --- a/ghost/core/core/server/data/exporter/table-lists.js +++ b/ghost/core/core/server/data/exporter/table-lists.js @@ -59,6 +59,7 @@ const BACKUP_TABLES = [ 'outbox', 'gifts', 'welcome_email_automations', + 'welcome_email_automation_runs', 'welcome_email_automated_emails' ]; diff --git a/ghost/core/core/server/data/migrations/versions/6.28/2026-04-08-13-21-16-add-welcome-email-automation-runs-table.js b/ghost/core/core/server/data/migrations/versions/6.28/2026-04-08-13-21-16-add-welcome-email-automation-runs-table.js new file mode 100644 index 00000000000..e33b429999f --- /dev/null +++ b/ghost/core/core/server/data/migrations/versions/6.28/2026-04-08-13-21-16-add-welcome-email-automation-runs-table.js @@ -0,0 +1,24 @@ +const {addTable} = require('../../utils'); + +// The default names are too long for MySQL. +const WELCOME_EMAIL_AUTOMATION_RUNS_AUTOMATION_FK = 'wear_automation_id_foreign'; +const WELCOME_EMAIL_AUTOMATION_RUNS_MEMBER_FK = 'wear_member_id_foreign'; +const WELCOME_EMAIL_AUTOMATION_RUNS_NEXT_EMAIL_FK = 'wear_next_email_id_foreign'; + +const welcomeEmailAutomationRunsSpec = { + id: {type: 'string', maxlength: 24, nullable: false, primary: true}, + welcome_email_automation_id: {type: 'string', maxlength: 24, nullable: false, references: 'welcome_email_automations.id', constraintName: WELCOME_EMAIL_AUTOMATION_RUNS_AUTOMATION_FK, cascadeDelete: true}, + member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', constraintName: WELCOME_EMAIL_AUTOMATION_RUNS_MEMBER_FK, cascadeDelete: true}, + next_welcome_email_automated_email_id: {type: 'string', maxlength: 24, nullable: true, references: 'welcome_email_automated_emails.id', constraintName: WELCOME_EMAIL_AUTOMATION_RUNS_NEXT_EMAIL_FK, cascadeDelete: false}, + ready_at: {type: 'dateTime', nullable: true}, + step_started_at: {type: 'dateTime', nullable: true}, + step_attempts: {type: 'integer', unsigned: true, nullable: false, defaultTo: 0}, + exit_reason: {type: 'string', maxlength: 50, nullable: true, validations: {isIn: [['member not found', 'email send failed', 'member unsubscribed', 'member changed status', 'finished']]}}, + created_at: {type: 'dateTime', nullable: false}, + updated_at: {type: 'dateTime', nullable: true}, + '@@INDEXES@@': [ + ['ready_at'] + ] +}; + +module.exports = addTable('welcome_email_automation_runs', welcomeEmailAutomationRunsSpec); diff --git a/ghost/core/core/server/data/schema/schema.js b/ghost/core/core/server/data/schema/schema.js index d82eaa118a2..c0a0b6a6280 100644 --- a/ghost/core/core/server/data/schema/schema.js +++ b/ghost/core/core/server/data/schema/schema.js @@ -1192,6 +1192,21 @@ module.exports = { created_at: {type: 'dateTime', nullable: false}, updated_at: {type: 'dateTime', nullable: true} }, + welcome_email_automation_runs: { + id: {type: 'string', maxlength: 24, nullable: false, primary: true}, + welcome_email_automation_id: {type: 'string', maxlength: 24, nullable: false, references: 'welcome_email_automations.id', constraintName: 'wear_automation_id_foreign', cascadeDelete: true}, + member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', constraintName: 'wear_member_id_foreign', cascadeDelete: true}, + next_welcome_email_automated_email_id: {type: 'string', maxlength: 24, nullable: true, references: 'welcome_email_automated_emails.id', constraintName: 'wear_next_email_id_foreign', cascadeDelete: false}, + ready_at: {type: 'dateTime', nullable: true}, + step_started_at: {type: 'dateTime', nullable: true}, + step_attempts: {type: 'integer', unsigned: true, nullable: false, defaultTo: 0}, + exit_reason: {type: 'string', maxlength: 50, nullable: true, validations: {isIn: [['member not found', 'email send failed', 'member unsubscribed', 'member changed status', 'finished']]}}, + created_at: {type: 'dateTime', nullable: false}, + updated_at: {type: 'dateTime', nullable: true}, + '@@INDEXES@@': [ + ['ready_at'] + ] + }, automated_email_recipients: { id: {type: 'string', maxlength: 24, nullable: false, primary: true}, automated_email_id: {type: 'string', maxlength: 24, nullable: false, references: 'welcome_email_automated_emails.id'}, diff --git a/ghost/core/core/server/models/welcome-email-automation-run.js b/ghost/core/core/server/models/welcome-email-automation-run.js new file mode 100644 index 00000000000..d91cd62c5e6 --- /dev/null +++ b/ghost/core/core/server/models/welcome-email-automation-run.js @@ -0,0 +1,27 @@ +const ghostBookshelf = require('./base'); + +const WelcomeEmailAutomationRun = ghostBookshelf.Model.extend({ + tableName: 'welcome_email_automation_runs', + + defaults() { + return { + stepAttempts: 0 + }; + }, + + welcomeEmailAutomation() { + return this.belongsTo('WelcomeEmailAutomation', 'welcome_email_automation_id', 'id'); + }, + + member() { + return this.belongsTo('Member', 'member_id', 'id'); + }, + + nextWelcomeEmailAutomatedEmail() { + return this.belongsTo('WelcomeEmailAutomatedEmail', 'next_welcome_email_automated_email_id', 'id'); + } +}); + +module.exports = { + WelcomeEmailAutomationRun: ghostBookshelf.model('WelcomeEmailAutomationRun', WelcomeEmailAutomationRun) +}; diff --git a/ghost/core/test/integration/exporter/exporter.test.js b/ghost/core/test/integration/exporter/exporter.test.js index 392ef5cd6ff..15451c5c3ae 100644 --- a/ghost/core/test/integration/exporter/exporter.test.js +++ b/ghost/core/test/integration/exporter/exporter.test.js @@ -102,6 +102,7 @@ describe('Exporter', function () { 'users', 'webhooks', 'welcome_email_automated_emails', + 'welcome_email_automation_runs', 'welcome_email_automations' ]; diff --git a/ghost/core/test/unit/server/data/schema/integrity.test.js b/ghost/core/test/unit/server/data/schema/integrity.test.js index 6660288a49c..432849267ec 100644 --- a/ghost/core/test/unit/server/data/schema/integrity.test.js +++ b/ghost/core/test/unit/server/data/schema/integrity.test.js @@ -35,7 +35,7 @@ const validateRouteSettings = require('../../../../../core/server/services/route */ describe('DB version integrity', function () { // Only these variables should need updating - const currentSchemaHash = 'f209300a0c528851f89f2e6302410c85'; + const currentSchemaHash = 'dc07b0777e196b9a952519830f1d3521'; const currentFixturesHash = '2f86ab1e3820e86465f9ad738dd0ee93'; const currentSettingsHash = 'a102b80d2ab0cd92325ed007c94d7da6'; const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01'; diff --git a/ghost/core/test/unit/server/models/welcome-email-automation-run.test.js b/ghost/core/test/unit/server/models/welcome-email-automation-run.test.js new file mode 100644 index 00000000000..f368f134b81 --- /dev/null +++ b/ghost/core/test/unit/server/models/welcome-email-automation-run.test.js @@ -0,0 +1,46 @@ +const assert = require('node:assert/strict'); +const models = require('../../../../core/server/models'); + +describe('Unit: models/welcome-email-automation-run', function () { + before(function () { + models.init(); + }); + + describe('tableName', function () { + it('uses the correct table name', function () { + const model = new models.WelcomeEmailAutomationRun(); + assert.equal(model.tableName, 'welcome_email_automation_runs'); + }); + }); + + describe('defaults', function () { + it('sets stepAttempts to 0', function () { + const model = new models.WelcomeEmailAutomationRun(); + const defaults = model.defaults(); + assert.equal(defaults.stepAttempts, 0); + }); + + it('returns only stepAttempts as a default', function () { + const model = new models.WelcomeEmailAutomationRun(); + const defaults = model.defaults(); + assert.deepEqual(Object.keys(defaults), ['stepAttempts']); + }); + }); + + describe('relationships', function () { + it('has a welcomeEmailAutomation relationship', function () { + const model = new models.WelcomeEmailAutomationRun(); + assert.equal(typeof model.welcomeEmailAutomation, 'function'); + }); + + it('has a member relationship', function () { + const model = new models.WelcomeEmailAutomationRun(); + assert.equal(typeof model.member, 'function'); + }); + + it('has a nextWelcomeEmailAutomatedEmail relationship', function () { + const model = new models.WelcomeEmailAutomationRun(); + assert.equal(typeof model.nextWelcomeEmailAutomatedEmail, 'function'); + }); + }); +});