From 3246a8d2c97e9ddb8791661e86ab4510ddbd91c8 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Tue, 7 May 2024 16:12:51 +0800 Subject: [PATCH] Added `latest_event_timestamp` to `email` table (#20118) ref ENG-832 - Added migrations for `latest_event_timestamp` column in emails table. - updated schema - updated emails model --- ...-13-add-latest-event-timestamp-to-email.js | 7 +++ ghost/core/core/server/data/schema/schema.js | 3 +- ghost/core/core/server/models/email.js | 3 +- .../__snapshots__/activity-feed.test.js.snap | 8 +-- .../admin/__snapshots__/emails.test.js.snap | 13 ++-- .../admin/__snapshots__/members.test.js.snap | 62 +++++++++++++++++++ .../unit/server/data/schema/integrity.test.js | 2 +- 7 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 ghost/core/core/server/data/migrations/versions/5.83/2024-05-02-06-01-13-add-latest-event-timestamp-to-email.js diff --git a/ghost/core/core/server/data/migrations/versions/5.83/2024-05-02-06-01-13-add-latest-event-timestamp-to-email.js b/ghost/core/core/server/data/migrations/versions/5.83/2024-05-02-06-01-13-add-latest-event-timestamp-to-email.js new file mode 100644 index 0000000000..7db5ffb64b --- /dev/null +++ b/ghost/core/core/server/data/migrations/versions/5.83/2024-05-02-06-01-13-add-latest-event-timestamp-to-email.js @@ -0,0 +1,7 @@ +// For information on writing migrations, see https://www.notion.so/ghost/Database-migrations-eb5b78c435d741d2b34a582d57c24253 + +const {createAddColumnMigration} = require('../../utils'); + +module.exports = createAddColumnMigration('emails', 'latest_event_timestamp', { + type: 'dateTime', nullable: true +}); diff --git a/ghost/core/core/server/data/schema/schema.js b/ghost/core/core/server/data/schema/schema.js index 21e0ed51f6..aba5a711cc 100644 --- a/ghost/core/core/server/data/schema/schema.js +++ b/ghost/core/core/server/data/schema/schema.js @@ -839,7 +839,8 @@ module.exports = { created_at: {type: 'dateTime', nullable: false}, created_by: {type: 'string', maxlength: 24, nullable: false}, updated_at: {type: 'dateTime', nullable: true}, - updated_by: {type: 'string', maxlength: 24, nullable: true} + updated_by: {type: 'string', maxlength: 24, nullable: true}, + latest_event_timestamp: {type: 'dateTime', nullable: true} }, email_batches: { id: {type: 'string', maxlength: 24, nullable: false, primary: true}, diff --git a/ghost/core/core/server/models/email.js b/ghost/core/core/server/models/email.js index 61d188da7a..7cd8c28e64 100644 --- a/ghost/core/core/server/models/email.js +++ b/ghost/core/core/server/models/email.js @@ -15,7 +15,8 @@ const Email = ghostBookshelf.Model.extend({ delivered_count: 0, opened_count: 0, failed_count: 0, - source_type: 'html' + source_type: 'html', + latest_event_timestamp: null }; }, diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap index 02842b5783..dedc16f76a 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap @@ -22699,7 +22699,7 @@ exports[`Activity Feed API Can filter events by post id 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "17559", + "content-length": "17739", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -23852,7 +23852,7 @@ exports[`Activity Feed API Returns email delivered events in activity feed 2: [h Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1051", + "content-length": "1081", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -23886,7 +23886,7 @@ exports[`Activity Feed API Returns email opened events in activity feed 2: [head Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1048", + "content-length": "1078", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -23944,7 +23944,7 @@ exports[`Activity Feed API Returns email sent events in activity feed 2: [header Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "3860", + "content-length": "3980", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/emails.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/emails.test.js.snap index c6f86209e5..0a9b58a02a 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/emails.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/emails.test.js.snap @@ -499,6 +499,7 @@ Object { "from": null, "html": "

Look! I'm an email

", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "latest_event_timestamp": null, "newsletter_id": null, "opened_count": 1, "plaintext": "Waba-daba-dab-da", @@ -526,6 +527,7 @@ Object { "from": null, "html": "

What's that? Another email!

", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "latest_event_timestamp": null, "newsletter_id": null, "opened_count": 0, "plaintext": "yes this is an email", @@ -560,7 +562,7 @@ exports[`Emails API Can browse emails 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1405", + "content-length": "1465", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -698,6 +700,7 @@ Object { "from": null, "html": "

Look! I'm an email

", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "latest_event_timestamp": null, "newsletter_id": null, "opened_count": 1, "plaintext": "Waba-daba-dab-da", @@ -722,7 +725,7 @@ exports[`Emails API Can read an email 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "642", + "content-length": "672", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -745,6 +748,7 @@ Object { "from": null, "html": "

What's that? Another email!

", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "latest_event_timestamp": null, "newsletter_id": null, "opened_count": 0, "plaintext": "yes this is an email", @@ -769,7 +773,7 @@ exports[`Emails API Can retry a failed email 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "688", + "content-length": "718", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -792,6 +796,7 @@ Object { "from": "support@example.com", "html": "

Hey Jamie, Hey Jamie,

Unsubscribe", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "latest_event_timestamp": null, "newsletter_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "opened_count": 0, "plaintext": "Hey Jamie, Hey Jamie @@ -817,7 +822,7 @@ exports[`Emails API Does default replacements on the HTML body of an old email 2 Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "923", + "content-length": "953", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap index ab363e53ee..2ca9ad2913 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap @@ -2932,6 +2932,68 @@ Object { } `; +exports[`Members API Can filter by conversion attribution 1: [body] 1`] = ` +Object { + "members": Array [ + Object { + "avatar_image": null, + "comped": false, + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "email": "member1@test.com", + "email_count": 0, + "email_open_rate": null, + "email_opened_count": 0, + "email_suppression": Object { + "info": null, + "suppressed": false, + }, + "geolocation": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "labels": Any, + "last_seen_at": null, + "name": "Mr Egg", + "newsletters": Array [ + Object { + "description": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Daily newsletter", + "status": "active", + }, + ], + "note": null, + "status": "free", + "subscribed": true, + "subscriptions": Any, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + ], + "meta": Object { + "pagination": Object { + "limit": 15, + "next": null, + "page": 1, + "pages": 1, + "prev": null, + "total": 1, + }, + }, +} +`; + +exports[`Members API Can filter by conversion attribution 2: [headers] 1`] = ` +Object { + "access-control-allow-origin": "http://127.0.0.1:2369", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "830", + "content-type": "application/json; charset=utf-8", + "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Accept-Version, Origin, Accept-Encoding", + "x-powered-by": "Express", +} +`; + exports[`Members API Can filter by paid status 1: [body] 1`] = ` Object { "members": Array [ 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 ccb1443be2..193ec9514b 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 = 'ccf3893bc3f8930f0d1188e646abda6d'; + const currentSchemaHash = '7495ca1ca1127247a2638a19c0c59b8e'; const currentFixturesHash = 'a489d615989eab1023d4b8af0ecee7fd'; const currentSettingsHash = '5c957ceb48c4878767d7d3db484c592d'; const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';