0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Added schema+migration for email_{batches,recipients} tables (#12192)

no issue

We want to store a list of recipients for each bulk email so that we have a consistent set of data that background processing/sending jobs can work from without worrying about moving large data sets around or member data changing mid-send.

- `email_batches` table acts as a join table with status for email<->email_recipient
  - stores a provider-specific ID that we get back when submitting a batch for sending to the bulk email provider
  - `status` allows for batch-specific status updates and picking up where we left off when submitting batches if needed
  - explicitly tying a list of email recipients to a batch allows for partial retries
- `email_recipients` table acts as a join table for email<->member
  - `member_id` does not have a foreign key constraint because members can be deleted but does have an index so that we can efficiently query which emails a member has received
  - stores static copies of the member info present at the time of sending an email for consistency in background jobs and auditing/historical data
This commit is contained in:
Kevin Ansfield 2020-09-14 12:21:58 +01:00 committed by GitHub
parent cbdc91ce48
commit 76c1b60a4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 60 additions and 2 deletions

View file

@ -0,0 +1,34 @@
const logging = require('../../../../../shared/logging');
const commands = require('../../../schema').commands;
module.exports = {
async up({connection}) {
// table creation order is important because of foreign key constraints,
// email_recipients references email_batches so email_batches has to exist when creating
return Promise.each(['email_batches', 'email_recipients'], async (table) => {
const tableExists = await connection.schema.hasTable(table);
if (tableExists) {
return logging.warn(`Skipping add table "${table}" - already exists`);
}
logging.info(`Adding table: ${table}`);
return commands.createTable(table, connection);
});
},
async down({connection}) {
// table deletion order is important because of foreign key constraints,
// email_recipients references email_batches so it has to be deleted first to not break constraints
return Promise.each(['email_recipients', 'email_batches'], async (table) => {
const tableExists = await connection.schema.hasTable(table);
if (!tableExists) {
return logging.warn(`Skipping drop table "${table}" - does not exist`);
}
logging.info(`Dropping table: ${table}`);
return commands.deleteTable(table, connection);
});
}
};

View file

@ -459,5 +459,29 @@ module.exports = {
created_by: {type: 'string', maxlength: 24, nullable: false}, created_by: {type: 'string', maxlength: 24, nullable: false},
updated_at: {type: 'dateTime', nullable: true}, updated_at: {type: 'dateTime', nullable: true},
updated_by: {type: 'string', maxlength: 24, nullable: true} updated_by: {type: 'string', maxlength: 24, nullable: true}
},
email_batches: {
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
email_id: {type: 'string', maxlength: 24, nullable: false, references: 'emails.id'},
provider_id: {type: 'string', maxlength: 255, nullable: true},
status: {
type: 'string',
maxlength: 50,
nullable: false,
defaultTo: 'pending',
validations: {isIn: [['pending', 'submitting', 'submitted', 'failed']]}
},
created_at: {type: 'dateTime', nullable: false},
updated_at: {type: 'dateTime', nullable: false}
},
email_recipients: {
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
email_id: {type: 'string', maxlength: 24, nullable: false, references: 'emails.id'},
member_id: {type: 'string', maxlength: 24, nullable: false, index: true},
batch_id: {type: 'string', maxlength: 24, nullable: false, references: 'email_batches.id'},
processed_at: {type: 'dateTime', nullable: true},
member_uuid: {type: 'string', maxlength: 36, nullable: false},
member_email: {type: 'string', maxlength: 191, nullable: false},
member_name: {type: 'string', maxlength: 191, nullable: true}
} }
}; };

View file

@ -55,7 +55,7 @@ describe('DB API', function () {
const jsonResponse = res.body; const jsonResponse = res.body;
should.exist(jsonResponse.db); should.exist(jsonResponse.db);
jsonResponse.db.should.have.length(1); jsonResponse.db.should.have.length(1);
Object.keys(jsonResponse.db[0].data).length.should.eql(30); Object.keys(jsonResponse.db[0].data).length.should.eql(32);
}); });
}); });

View file

@ -32,7 +32,7 @@ const defaultSettings = require('../../../../core/server/data/schema/default-set
*/ */
describe('DB version integrity', function () { describe('DB version integrity', function () {
// Only these variables should need updating // Only these variables should need updating
const currentSchemaHash = '42a966364eb4b5851e807133374821da'; const currentSchemaHash = 'c2b2de0157edddb68791dde49391d4e5';
const currentFixturesHash = '29148c40dfaf4f828c5fca95666f6545'; const currentFixturesHash = '29148c40dfaf4f828c5fca95666f6545';
const currentSettingsHash = 'c8daa2c9632bb75f9d60655de09ae3bd'; const currentSettingsHash = 'c8daa2c9632bb75f9d60655de09ae3bd';
const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01'; const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';