ghost 4.44.0 → 4.46.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Gruntfile.js +1 -1
- package/core/boot.js +2 -0
- package/core/built/assets/{chunk.3.6e2ed2d00856e12bd81a.js → chunk.3.52b444495dfcf50afb0b.js} +20 -20
- package/core/built/assets/ghost-dark-155e039c0d991b7af75dea8cd3846b11.css +1 -0
- package/core/built/assets/{ghost.min-1e7dce606e92a03207d15ae7eb3d3c23.js → ghost.min-30e597cb65b62b31a9422ca9c0eb2890.js} +777 -632
- package/core/built/assets/ghost.min-bd8cd0185fd5dfc8291502f801e443e6.css +1 -0
- package/core/built/assets/icons/clock.svg +1 -1
- package/core/built/assets/icons/email-at.svg +1 -0
- package/core/built/assets/icons/email-body.svg +1 -0
- package/core/built/assets/icons/email-footer.svg +1 -0
- package/core/built/assets/icons/email-header.svg +1 -0
- package/core/built/assets/icons/email-member.svg +1 -0
- package/core/built/assets/icons/email-name.svg +1 -0
- package/core/built/assets/icons/member.svg +1 -3
- package/core/built/assets/icons/send-email.svg +1 -1
- package/core/built/assets/img/abstract-2-2937e2902b64360d0cbe4cec8bd8479b.jpg +0 -0
- package/core/built/assets/img/abstract-c52b2f4208e7fd2e7b8abd8b1eec4f7b.jpg +0 -0
- package/core/built/assets/img/community-background-3f501ff1d764d0cb81f7c2cbacfc6503.jpg +0 -0
- package/core/built/assets/img/community-be8c1dcecfb157f2bfba5cababc8e686.jpg +0 -0
- package/core/built/assets/img/newsletter-1-197ae8063dfb2e22278d355198029c9e.jpg +0 -0
- package/core/built/assets/img/newsletter-2-5a2c7693ea9380d4282061302c01267a.jpg +0 -0
- package/core/built/assets/img/resource-1-722f202795856e4a5596c8a3b7bedc43.jpg +0 -0
- package/core/built/assets/{vendor.min-fe2c9b1235b4119b5406b788db2db434.js → vendor.min-97fd438f4772c5ec6bb30ad779b8530e.js} +862 -523
- package/core/frontend/apps/amp/lib/helpers/amp_content.js +2 -3
- package/core/frontend/apps/amp/lib/views/amp.hbs +5 -3
- package/core/frontend/helpers/get.js +1 -1
- package/core/frontend/services/routing/controllers/unsubscribe.js +22 -0
- package/core/frontend/web/middleware/cors.js +56 -0
- package/core/frontend/web/middleware/index.js +1 -0
- package/core/frontend/web/middleware/static-theme.js +8 -8
- package/core/frontend/web/site.js +1 -48
- package/core/server/api/canary/members.js +3 -0
- package/core/server/api/canary/newsletters.js +86 -4
- package/core/server/api/canary/stats.js +11 -2
- package/core/server/api/canary/utils/serializers/input/members.js +22 -0
- package/core/server/api/canary/utils/serializers/output/mappers/pages.js +1 -0
- package/core/server/api/canary/utils/serializers/output/mappers/posts.js +2 -0
- package/core/server/api/canary/utils/serializers/output/members.js +13 -5
- package/core/server/api/v2/utils/serializers/output/utils/mapper.js +2 -0
- package/core/server/api/v3/utils/serializers/output/utils/mapper.js +3 -0
- package/core/server/data/importer/importers/data/settings.js +0 -3
- package/core/server/data/migrations/utils.js +40 -0
- package/core/server/data/migrations/versions/4.43/2022-03-28-19-26-recreate-newsletter-table.js +1 -1
- package/core/server/data/migrations/versions/4.45/2022-04-19-12-23-backfill-subscriptions-offers.js +60 -0
- package/core/server/data/migrations/versions/4.45/2022-04-20-11-25-add-newsletter-read-permission.js +9 -0
- package/core/server/data/migrations/versions/4.45/2022-04-21-02-55-add-notifications-key-entry-to-settings-table.js +8 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-00-add-created-at-newsletters.js +6 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-01-add-updated-at-newsletters.js +6 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-02-fill-created-at-newsletters.js +19 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-03-drop-nullable-created-at-newsletters.js +3 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-08-newsletters-show-header-name.js +7 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-57-add-uuid-column-to-newsletters.js +8 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-58-fill-uuid-for-newsletters.js +19 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-12-59-drop-nullable-uuid-newsletters.js +3 -0
- package/core/server/data/migrations/versions/4.46/2022-04-13-13-00-add-default-newsletter.js +92 -0
- package/core/server/data/migrations/versions/4.46/2022-04-20-08-39-map-subscribers-to-default-newsletter.js +66 -0
- package/core/server/data/migrations/versions/4.46/2022-04-22-07-43-add-newsletter-id-to-subscribe-events.js +9 -0
- package/core/server/data/migrations/versions/4.46/2022-04-27-07-59-set-newsletter-id-subscribe-events.js +31 -0
- package/core/server/data/schema/commands.js +14 -0
- package/core/server/data/schema/default-settings/default-settings.json +4 -0
- package/core/server/data/schema/fixtures/fixtures.json +7 -1
- package/core/server/data/schema/schema.js +8 -3
- package/core/server/models/base/plugins/generate-slug.js +2 -2
- package/core/server/models/email.js +4 -0
- package/core/server/models/label.js +1 -1
- package/core/server/models/member-subscribe-event.js +4 -0
- package/core/server/models/member.js +26 -0
- package/core/server/models/newsletter.js +97 -14
- package/core/server/models/post.js +7 -4
- package/core/server/models/role.js +1 -1
- package/core/server/models/tag.js +1 -1
- package/core/server/models/user.js +1 -1
- package/core/server/services/api-version-compatibility/index.js +29 -0
- package/core/server/services/auth/members/index.js +1 -1
- package/core/server/services/mega/email-preview.js +4 -1
- package/core/server/services/mega/mega.js +83 -26
- package/core/server/services/mega/post-email-serializer.js +17 -14
- package/core/server/services/mega/template.js +24 -3
- package/core/server/services/members/api.js +2 -2
- package/core/server/services/members/middleware.js +69 -2
- package/core/server/services/members/service.js +4 -1
- package/core/server/services/newsletters/emails/verify-email.js +166 -0
- package/core/server/services/newsletters/index.js +14 -7
- package/core/server/services/newsletters/service.js +237 -6
- package/core/server/services/posts/posts-service.js +7 -9
- package/core/server/services/stats/service.js +2 -6
- package/core/server/services/users.js +20 -20
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/app.js +3 -0
- package/core/server/web/api/canary/admin/app.js +3 -0
- package/core/server/web/api/canary/admin/routes.js +3 -0
- package/core/server/web/api/canary/content/app.js +3 -0
- package/core/server/web/api/middleware/cors.js +1 -1
- package/core/server/web/api/v2/admin/app.js +3 -0
- package/core/server/web/api/v2/content/app.js +3 -0
- package/core/server/web/api/v3/admin/app.js +3 -0
- package/core/server/web/api/v3/content/app.js +3 -0
- package/core/server/web/members/app.js +5 -0
- package/core/shared/config/defaults.json +1 -1
- package/core/shared/labs.js +4 -2
- package/core/shared/settings-cache/public.js +1 -1
- package/package.json +69 -65
- package/yarn.lock +965 -620
- package/core/built/assets/ghost-dark-470c1ef06b10e5c40ad05f3a642eaaea.css +0 -1
- package/core/built/assets/ghost.min-d0c17e8314b5583c0df5d05fab3c051c.css +0 -1
- package/core/server/services/stats/lib/members-stats-service.js +0 -161
- package/core/server/services/stats/lib/mrr-stats-service.js +0 -154
package/core/server/data/migrations/versions/4.46/2022-04-13-12-02-fill-created-at-newsletters.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
|
|
3
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
4
|
+
|
|
5
|
+
module.exports = createTransactionalMigration(
|
|
6
|
+
async function up(knex) {
|
|
7
|
+
logging.info('Setting missing created_at values for existing newsletters');
|
|
8
|
+
|
|
9
|
+
const now = knex.raw('CURRENT_TIMESTAMP');
|
|
10
|
+
const updatedRows = await knex('newsletters')
|
|
11
|
+
.where('created_at', null)
|
|
12
|
+
.update('created_at', now);
|
|
13
|
+
|
|
14
|
+
logging.info(`Updated ${updatedRows} newsletters with created_at = now`);
|
|
15
|
+
},
|
|
16
|
+
async function down() {
|
|
17
|
+
// Not required: we would lose information here.
|
|
18
|
+
}
|
|
19
|
+
);
|
package/core/server/data/migrations/versions/4.46/2022-04-13-12-58-fill-uuid-for-newsletters.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const uuid = require('uuid');
|
|
3
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
4
|
+
|
|
5
|
+
module.exports = createTransactionalMigration(
|
|
6
|
+
async function up(knex) {
|
|
7
|
+
const newslettersWithoutUUID = await knex.select('id').from('newsletters').whereNull('uuid');
|
|
8
|
+
|
|
9
|
+
logging.info(`Adding uuid field value to ${newslettersWithoutUUID.length} newsletters.`);
|
|
10
|
+
|
|
11
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
12
|
+
for (const newsletter of newslettersWithoutUUID) {
|
|
13
|
+
await knex('newsletters').update('uuid', uuid.v4()).where('id', newsletter.id);
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
async function down() {
|
|
17
|
+
// Not required: we would lose information here.
|
|
18
|
+
}
|
|
19
|
+
);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const ObjectId = require('bson-objectid');
|
|
2
|
+
const uuid = require('uuid');
|
|
3
|
+
const logging = require('@tryghost/logging');
|
|
4
|
+
const startsWith = require('lodash/startsWith');
|
|
5
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
6
|
+
|
|
7
|
+
module.exports = createTransactionalMigration(
|
|
8
|
+
async function up(knex) {
|
|
9
|
+
// This uses the default settings from core/server/data/schema/default-settings/default-settings.json
|
|
10
|
+
const newsletter = {
|
|
11
|
+
id: (new ObjectId()).toHexString(),
|
|
12
|
+
uuid: uuid.v4(),
|
|
13
|
+
name: 'Ghost',
|
|
14
|
+
description: '',
|
|
15
|
+
slug: 'default-newsletter',
|
|
16
|
+
sender_name: null,
|
|
17
|
+
sender_email: null,
|
|
18
|
+
sender_reply_to: 'newsletter',
|
|
19
|
+
status: 'active',
|
|
20
|
+
visibility: 'members',
|
|
21
|
+
subscribe_on_signup: true,
|
|
22
|
+
sort_order: 0,
|
|
23
|
+
body_font_category: 'sans_serif',
|
|
24
|
+
footer_content: '',
|
|
25
|
+
header_image: null,
|
|
26
|
+
show_badge: true,
|
|
27
|
+
show_feature_image: true,
|
|
28
|
+
show_header_icon: true,
|
|
29
|
+
show_header_title: true,
|
|
30
|
+
show_header_name: false,
|
|
31
|
+
title_alignment: 'center',
|
|
32
|
+
title_font_category: 'sans_serif',
|
|
33
|
+
created_at: knex.raw('CURRENT_TIMESTAMP')
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Make sure the newsletter table is empty
|
|
37
|
+
const newsletters = await knex('newsletters').count('*', {as: 'total'});
|
|
38
|
+
|
|
39
|
+
if (newsletters[0].total !== 0) {
|
|
40
|
+
logging.warn('Skipping adding the default newsletter - There is already at least one newsletter');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Get all settings in one query
|
|
45
|
+
const settings = await knex('settings')
|
|
46
|
+
.whereIn('key', [
|
|
47
|
+
'title',
|
|
48
|
+
'description',
|
|
49
|
+
'newsletter_body_font_category',
|
|
50
|
+
'newsletter_footer_content',
|
|
51
|
+
'newsletter_header_image',
|
|
52
|
+
'newsletter_show_badge',
|
|
53
|
+
'newsletter_show_feature_image',
|
|
54
|
+
'newsletter_show_header_icon',
|
|
55
|
+
'newsletter_show_header_title',
|
|
56
|
+
'newsletter_title_alignment',
|
|
57
|
+
'newsletter_title_font_category'
|
|
58
|
+
])
|
|
59
|
+
.select(['key', 'value']);
|
|
60
|
+
|
|
61
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
62
|
+
for (let {key, value} of settings) {
|
|
63
|
+
// Use site title for the newsletter name
|
|
64
|
+
if (key === 'title') {
|
|
65
|
+
key = 'name';
|
|
66
|
+
}
|
|
67
|
+
// Settings have a `newsletter_` prefix which isn't present on the newsletters table
|
|
68
|
+
if (startsWith(key, 'newsletter_')) {
|
|
69
|
+
key = key.slice(11);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (value === null && ['name', 'body_font_category', 'show_badge', 'show_feature_image', 'show_header_icon', 'show_header_title', 'title_alignment', 'title_font_category'].includes(key)) {
|
|
73
|
+
// Prevent setting null to non-nullable columns
|
|
74
|
+
// Default to defaults above in that case
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof newsletter[key] === 'boolean') {
|
|
79
|
+
newsletter[key] = value === 'true';
|
|
80
|
+
} else {
|
|
81
|
+
newsletter[key] = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
logging.info('Adding the default newsletter');
|
|
86
|
+
await knex('newsletters').insert(newsletter);
|
|
87
|
+
},
|
|
88
|
+
async function down(knex) {
|
|
89
|
+
logging.info(`Removing newsletters`);
|
|
90
|
+
await knex('newsletters').delete();
|
|
91
|
+
}
|
|
92
|
+
);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const ObjectID = require('bson-objectid');
|
|
3
|
+
|
|
4
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
5
|
+
|
|
6
|
+
module.exports = createTransactionalMigration(
|
|
7
|
+
async function up(knex) {
|
|
8
|
+
logging.info('Adding existing subscribers to default newsletter');
|
|
9
|
+
|
|
10
|
+
const newsletter = await knex('newsletters')
|
|
11
|
+
.orderBy('sort_order', 'asc')
|
|
12
|
+
.orderBy('created_at', 'asc')
|
|
13
|
+
.first('id', 'name');
|
|
14
|
+
|
|
15
|
+
if (!newsletter) {
|
|
16
|
+
logging.info(`Default newsletter not found - skipping`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// This is at the start of the up() instead of at the end of the down()
|
|
21
|
+
// to maintain idempotency
|
|
22
|
+
logging.info('Removing existing newsletter subscriptions');
|
|
23
|
+
await knex('members_newsletters').delete();
|
|
24
|
+
|
|
25
|
+
logging.info(`Subscribing members to newsletter '${newsletter.name}'`);
|
|
26
|
+
|
|
27
|
+
const memberIds = await knex('members')
|
|
28
|
+
.where({subscribed: true})
|
|
29
|
+
.pluck('id');
|
|
30
|
+
|
|
31
|
+
if (!memberIds.length) {
|
|
32
|
+
logging.info(`No members to subscribe - skipping`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
logging.info(`Found ${memberIds.length} members to subscribe`);
|
|
37
|
+
|
|
38
|
+
const pivotRows = memberIds.map((memberId) => {
|
|
39
|
+
return {
|
|
40
|
+
id: ObjectID().toHexString(),
|
|
41
|
+
member_id: memberId,
|
|
42
|
+
newsletter_id: newsletter.id
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await knex.batchInsert('members_newsletters', pivotRows);
|
|
47
|
+
},
|
|
48
|
+
async function down(knex) {
|
|
49
|
+
logging.info('Syncing subscriptions from newsletters -> members.subscribed');
|
|
50
|
+
await knex('members')
|
|
51
|
+
.whereIn('id', function () {
|
|
52
|
+
this.select('member_id').from('members_newsletters');
|
|
53
|
+
})
|
|
54
|
+
.update({
|
|
55
|
+
subscribed: true
|
|
56
|
+
});
|
|
57
|
+
logging.info('Syncing unsubscribes from newsletters -> members.subscribed');
|
|
58
|
+
await knex('members')
|
|
59
|
+
.whereNotIn('id', function () {
|
|
60
|
+
this.select('member_id').from('members_newsletters');
|
|
61
|
+
})
|
|
62
|
+
.update({
|
|
63
|
+
subscribed: false
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const {createAddColumnMigration} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = createAddColumnMigration('members_subscribe_events', 'newsletter_id', {
|
|
4
|
+
type: 'string',
|
|
5
|
+
maxlength: 24,
|
|
6
|
+
nullable: true,
|
|
7
|
+
references: 'newsletters.id',
|
|
8
|
+
cascadeDelete: false
|
|
9
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
3
|
+
|
|
4
|
+
module.exports = createTransactionalMigration(
|
|
5
|
+
async function up(knex) {
|
|
6
|
+
// Get the default newsletter
|
|
7
|
+
const newsletter = await knex('newsletters')
|
|
8
|
+
.where('status', 'active')
|
|
9
|
+
.orderBy('sort_order', 'asc')
|
|
10
|
+
.orderBy('created_at', 'asc')
|
|
11
|
+
.orderBy('id', 'asc')
|
|
12
|
+
.first('id');
|
|
13
|
+
|
|
14
|
+
if (!newsletter) {
|
|
15
|
+
logging.error(`Default newsletter not found - skipping`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Set subscribe events
|
|
20
|
+
const updatedRows = await knex('members_subscribe_events')
|
|
21
|
+
.update({
|
|
22
|
+
newsletter_id: newsletter.id
|
|
23
|
+
})
|
|
24
|
+
.where('newsletter_id', null);
|
|
25
|
+
|
|
26
|
+
logging.info(`Updated ${updatedRows} members_subscribe_events with default newsletter id`);
|
|
27
|
+
},
|
|
28
|
+
async function down() {
|
|
29
|
+
// Not required
|
|
30
|
+
}
|
|
31
|
+
);
|
|
@@ -59,6 +59,18 @@ function addTableColumn(tableName, table, columnName, columnSpec = schema[tableN
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function setNullable(tableName, column, transaction = db.knex) {
|
|
63
|
+
return transaction.schema.table(tableName, function (table) {
|
|
64
|
+
table.setNullable(column);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function dropNullable(tableName, column, transaction = db.knex) {
|
|
69
|
+
return transaction.schema.table(tableName, function (table) {
|
|
70
|
+
table.dropNullable(column);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
function addColumn(tableName, column, transaction = db.knex, columnSpec) {
|
|
63
75
|
return transaction.schema.table(tableName, function (table) {
|
|
64
76
|
addTableColumn(tableName, table, column, columnSpec);
|
|
@@ -412,6 +424,8 @@ module.exports = {
|
|
|
412
424
|
dropForeign: dropForeign,
|
|
413
425
|
addColumn: addColumn,
|
|
414
426
|
dropColumn: dropColumn,
|
|
427
|
+
setNullable: setNullable,
|
|
428
|
+
dropNullable: dropNullable,
|
|
415
429
|
getColumns: getColumns,
|
|
416
430
|
createColumnMigration,
|
|
417
431
|
// NOTE: below are exposed for testing purposes only
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"entries": [
|
|
25
25
|
{
|
|
26
26
|
"name": "Default Newsletter",
|
|
27
|
-
"slug": "default-newsletter"
|
|
27
|
+
"slug": "default-newsletter",
|
|
28
|
+
"show_header_name": false
|
|
28
29
|
}
|
|
29
30
|
]
|
|
30
31
|
},
|
|
@@ -546,6 +547,11 @@
|
|
|
546
547
|
"action_type": "browse",
|
|
547
548
|
"object_type": "newsletter"
|
|
548
549
|
},
|
|
550
|
+
{
|
|
551
|
+
"name": "Read newsletters",
|
|
552
|
+
"action_type": "read",
|
|
553
|
+
"object_type": "newsletter"
|
|
554
|
+
},
|
|
549
555
|
{
|
|
550
556
|
"name": "Add newsletters",
|
|
551
557
|
"action_type": "add",
|
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
module.exports = {
|
|
11
11
|
newsletters: {
|
|
12
12
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
13
|
+
uuid: {type: 'string', maxlength: 36, nullable: false, unique: true, validations: {isUUID: true}},
|
|
13
14
|
name: {type: 'string', maxlength: 191, nullable: false, unique: true},
|
|
14
15
|
description: {type: 'string', maxlength: 2000, nullable: true},
|
|
15
16
|
slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
|
|
16
17
|
sender_name: {type: 'string', maxlength: 191, nullable: true},
|
|
17
18
|
sender_email: {type: 'string', maxlength: 191, nullable: true},
|
|
18
19
|
sender_reply_to: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'newsletter', validations: {isIn: [['newsletter', 'support']]}},
|
|
19
|
-
status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'active'},
|
|
20
|
+
status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'active', validations: {isIn: [['active', 'archived']]}},
|
|
20
21
|
visibility: {
|
|
21
22
|
type: 'string',
|
|
22
23
|
maxlength: 50,
|
|
@@ -33,7 +34,10 @@ module.exports = {
|
|
|
33
34
|
show_feature_image: {type: 'bool', nullable: false, defaultTo: true},
|
|
34
35
|
body_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif', validations: {isIn: [['serif', 'sans_serif']]}},
|
|
35
36
|
footer_content: {type: 'text', maxlength: 1000000000, nullable: true},
|
|
36
|
-
show_badge: {type: 'bool', nullable: false, defaultTo: true}
|
|
37
|
+
show_badge: {type: 'bool', nullable: false, defaultTo: true},
|
|
38
|
+
show_header_name: {type: 'bool', nullable: false, defaultTo: true},
|
|
39
|
+
created_at: {type: 'dateTime', nullable: false},
|
|
40
|
+
updated_at: {type: 'dateTime', nullable: true}
|
|
37
41
|
},
|
|
38
42
|
posts: {
|
|
39
43
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
@@ -596,7 +600,8 @@ module.exports = {
|
|
|
596
600
|
member_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'members.id', cascadeDelete: true},
|
|
597
601
|
subscribed: {type: 'bool', nullable: false, defaultTo: true},
|
|
598
602
|
created_at: {type: 'dateTime', nullable: false},
|
|
599
|
-
source: {type: 'string', maxlength: 50, nullable: true}
|
|
603
|
+
source: {type: 'string', maxlength: 50, nullable: true},
|
|
604
|
+
newsletter_id: {type: 'string', maxlength: 24, nullable: true, references: 'newsletters.id', cascadeDelete: false}
|
|
600
605
|
},
|
|
601
606
|
stripe_products: {
|
|
602
607
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
@@ -79,7 +79,7 @@ module.exports = function (Bookshelf) {
|
|
|
79
79
|
// If it's a user, let's try to cut it down (unless this is a human request)
|
|
80
80
|
if (baseName === 'user' && options && options.shortSlug && slugTryCount === 1 && slug !== 'ghost-owner') {
|
|
81
81
|
longSlug = slug;
|
|
82
|
-
slug = (slug.indexOf('-') > -1) ? slug.
|
|
82
|
+
slug = (slug.indexOf('-') > -1) ? slug.slice(0, slug.indexOf('-')) : slug;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
if (!_.has(options, 'importing') || !options.importing) {
|
|
@@ -118,4 +118,4 @@ module.exports = function (Bookshelf) {
|
|
|
118
118
|
* @property {boolean} [importing] Set to true to don't cut the slug on import
|
|
119
119
|
* @property {boolean} [shortSlug] If it's a user, let's try to cut it down (unless this is a human request)
|
|
120
120
|
* @property {boolean} [skipDuplicateChecks] Don't append unique identifiers when the slug is not unique (this prevents any database queries)
|
|
121
|
-
*/
|
|
121
|
+
*/
|
|
@@ -54,6 +54,10 @@ const Email = ghostBookshelf.Model.extend({
|
|
|
54
54
|
return this.hasMany('EmailRecipient', 'email_id');
|
|
55
55
|
},
|
|
56
56
|
|
|
57
|
+
newsletter() {
|
|
58
|
+
return this.belongsTo('Newsletter', 'newsletter_id');
|
|
59
|
+
},
|
|
60
|
+
|
|
57
61
|
emitChange: function emitChange(event, options) {
|
|
58
62
|
const eventToTrigger = 'email' + '.' + event;
|
|
59
63
|
ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
|
|
@@ -92,7 +92,7 @@ Label = ghostBookshelf.Model.extend({
|
|
|
92
92
|
permittedOptions: function permittedOptions(methodName) {
|
|
93
93
|
let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
|
|
94
94
|
|
|
95
|
-
//
|
|
95
|
+
// allowlists for the `options` hash argument on methods, by method name.
|
|
96
96
|
// these are the only options that can be passed to Bookshelf / Knex.
|
|
97
97
|
const validOptions = {
|
|
98
98
|
findAll: ['columns'],
|
|
@@ -8,6 +8,10 @@ const MemberSubscribeEvent = ghostBookshelf.Model.extend({
|
|
|
8
8
|
return this.belongsTo('Member', 'member_id', 'id');
|
|
9
9
|
},
|
|
10
10
|
|
|
11
|
+
newsletter() {
|
|
12
|
+
return this.belongsTo('Newsletter', 'newsletter_id', 'id');
|
|
13
|
+
},
|
|
14
|
+
|
|
11
15
|
customQuery(qb, options) {
|
|
12
16
|
if (options.aggregateSubscriptionDeltas) {
|
|
13
17
|
if (options.limit || options.filter) {
|
|
@@ -370,6 +370,32 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
370
370
|
query.transacting(unfilteredOptions.transacting);
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
return query;
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
getNewsletterRelations(data, unfilteredOptions = {}) {
|
|
377
|
+
const query = ghostBookshelf.knex('members_newsletters')
|
|
378
|
+
.select('id')
|
|
379
|
+
.whereIn('member_id', data.memberIds);
|
|
380
|
+
|
|
381
|
+
if (unfilteredOptions.transacting) {
|
|
382
|
+
query.transacting(unfilteredOptions.transacting);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return query;
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
fetchAllSubscribed(unfilteredOptions = {}) {
|
|
389
|
+
// we use raw queries instead of model relationships because model hydration is expensive
|
|
390
|
+
const query = ghostBookshelf.knex('members_newsletters')
|
|
391
|
+
.join('newsletters', 'members_newsletters.newsletter_id', '=', 'newsletters.id')
|
|
392
|
+
.where('newsletters.status', 'active')
|
|
393
|
+
.distinct('member_id as id');
|
|
394
|
+
|
|
395
|
+
if (unfilteredOptions.transacting) {
|
|
396
|
+
query.transacting(unfilteredOptions.transacting);
|
|
397
|
+
}
|
|
398
|
+
|
|
373
399
|
return query;
|
|
374
400
|
}
|
|
375
401
|
});
|
|
@@ -1,21 +1,40 @@
|
|
|
1
1
|
const ghostBookshelf = require('./base');
|
|
2
|
+
const ObjectID = require('bson-objectid');
|
|
3
|
+
const uuid = require('uuid');
|
|
2
4
|
|
|
3
5
|
const Newsletter = ghostBookshelf.Model.extend({
|
|
4
6
|
tableName: 'newsletters',
|
|
5
7
|
|
|
6
|
-
defaults: {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
defaults: function defaults() {
|
|
9
|
+
return {
|
|
10
|
+
uuid: uuid.v4(),
|
|
11
|
+
sender_reply_to: 'newsletter',
|
|
12
|
+
status: 'active',
|
|
13
|
+
visibility: 'members',
|
|
14
|
+
subscribe_on_signup: true,
|
|
15
|
+
sort_order: 0,
|
|
16
|
+
title_font_category: 'sans_serif',
|
|
17
|
+
title_alignment: 'center',
|
|
18
|
+
show_feature_image: true,
|
|
19
|
+
body_font_category: 'sans_serif',
|
|
20
|
+
show_badge: true,
|
|
21
|
+
show_header_icon: true,
|
|
22
|
+
show_header_title: true,
|
|
23
|
+
show_header_name: true
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
members() {
|
|
28
|
+
return this.belongsToMany('Member', 'members_newsletters', 'newsletter_id', 'member_id')
|
|
29
|
+
.query((qb) => {
|
|
30
|
+
// avoids bookshelf adding a `DISTINCT` to the query
|
|
31
|
+
// we know the result set will already be unique and DISTINCT hurts query performance
|
|
32
|
+
qb.columns('members.*');
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
posts() {
|
|
37
|
+
return this.hasMany('Post');
|
|
19
38
|
},
|
|
20
39
|
|
|
21
40
|
async onSaving(model, _attr, options) {
|
|
@@ -36,12 +55,76 @@ const Newsletter = ghostBookshelf.Model.extend({
|
|
|
36
55
|
model.set({slug: cleanSlug});
|
|
37
56
|
}
|
|
38
57
|
}
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
subscribeMembersById(memberIds, unfilteredOptions = {}) {
|
|
61
|
+
let pivotRows = [];
|
|
62
|
+
for (const memberId of memberIds) {
|
|
63
|
+
pivotRows.push({
|
|
64
|
+
id: ObjectID().toHexString(),
|
|
65
|
+
member_id: memberId.id,
|
|
66
|
+
newsletter_id: this.id
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const query = ghostBookshelf.knex.batchInsert('members_newsletters', pivotRows);
|
|
71
|
+
|
|
72
|
+
if (unfilteredOptions.transacting) {
|
|
73
|
+
query.transacting(unfilteredOptions.transacting);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return query;
|
|
39
77
|
}
|
|
40
78
|
}, {
|
|
79
|
+
orderDefaultRaw: function () {
|
|
80
|
+
return 'sort_order ASC, created_at ASC, id ASC';
|
|
81
|
+
},
|
|
82
|
+
|
|
41
83
|
orderDefaultOptions: function orderDefaultOptions() {
|
|
42
84
|
return {
|
|
43
|
-
sort_order: 'ASC'
|
|
85
|
+
sort_order: 'ASC',
|
|
86
|
+
created_at: 'ASC',
|
|
87
|
+
id: 'ASC'
|
|
44
88
|
};
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
getDefaultNewsletter: async function getDefaultNewsletter(unfilteredOptions = {}) {
|
|
92
|
+
const options = {
|
|
93
|
+
filter: 'status:active',
|
|
94
|
+
order: this.orderDefaultRaw(),
|
|
95
|
+
limit: 1
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (unfilteredOptions.transacting) {
|
|
99
|
+
options.transacting = unfilteredOptions.transacting;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const newsletters = await this.findPage(options);
|
|
103
|
+
|
|
104
|
+
if (newsletters.data.length > 0) {
|
|
105
|
+
return newsletters.data[0];
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
getNextAvailableSortOrder: async function getNextAvailableSortOrder(unfilteredOptions = {}) {
|
|
111
|
+
const options = {
|
|
112
|
+
filter: 'status:active',
|
|
113
|
+
order: 'sort_order DESC', // there's no NQL syntax available here
|
|
114
|
+
limit: 1,
|
|
115
|
+
columns: ['sort_order']
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (unfilteredOptions.transacting) {
|
|
119
|
+
options.transacting = unfilteredOptions.transacting;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const lastNewsletter = await this.findPage(options);
|
|
123
|
+
|
|
124
|
+
if (lastNewsletter.data.length > 0) {
|
|
125
|
+
return lastNewsletter.data[0].get('sort_order') + 1;
|
|
126
|
+
}
|
|
127
|
+
return 0;
|
|
45
128
|
}
|
|
46
129
|
});
|
|
47
130
|
|