ghost 4.45.0 → 4.46.2
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/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-aafce1ab3f2ab6b4a385e8b888548e15.js → ghost.min-30e597cb65b62b31a9422ca9c0eb2890.js} +707 -633
- 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/community-background-3f501ff1d764d0cb81f7c2cbacfc6503.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-eaf9e7b39e2ba76722eabc7a814e0ff1.js → vendor.min-97fd438f4772c5ec6bb30ad779b8530e.js} +829 -493
- 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/server/api/canary/members.js +3 -0
- package/core/server/api/canary/newsletters.js +54 -4
- package/core/server/api/canary/stats.js +9 -0
- 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/shared/http.js +0 -3
- 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/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.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/fixtures/fixtures.json +2 -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/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 +6 -3
- package/core/server/services/api-version-compatibility/index.js +22 -5
- 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 +63 -4
- package/core/server/services/members/service.js +7 -4
- 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/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 +4 -3
- package/core/server/web/api/canary/admin/app.js +2 -3
- package/core/server/web/api/canary/admin/routes.js +2 -0
- package/core/server/web/api/v2/admin/app.js +2 -3
- package/core/server/web/api/v3/admin/app.js +2 -3
- package/core/server/web/members/app.js +5 -0
- package/core/shared/config/defaults.json +1 -1
- package/core/shared/labs.js +2 -2
- package/package.json +59 -59
- package/yarn.lock +794 -525
- package/core/built/assets/ghost-dark-887882218a8f9a4a367de52212d27917.css +0 -1
- package/core/built/assets/ghost.min-0b3ecc9dd9e8b3b380d93f1839213af5.css +0 -1
|
@@ -987,7 +987,7 @@
|
|
|
987
987
|
<header class="page-header">
|
|
988
988
|
<a href="{{@site.url}}">
|
|
989
989
|
{{#if @site.icon}}
|
|
990
|
-
<amp-img class="site-icon" src="{{img_url @site.icon absolute="true"}}" width="50" height="50" layout="fixed"></amp-img>
|
|
990
|
+
<amp-img class="site-icon" src="{{img_url @site.icon absolute="true"}}" width="50" height="50" layout="fixed" alt="{{@site.title}}"></amp-img>
|
|
991
991
|
{{else}}
|
|
992
992
|
{{@site.title}}
|
|
993
993
|
{{/if}}
|
|
@@ -1006,7 +1006,9 @@
|
|
|
1006
1006
|
</header>
|
|
1007
1007
|
{{#if feature_image}}
|
|
1008
1008
|
<figure class="post-image">
|
|
1009
|
-
<amp-img src="{{img_url feature_image absolute="true"}}" width="600" height="340" layout="responsive"
|
|
1009
|
+
<amp-img src="{{img_url feature_image absolute="true"}}" width="600" height="340" layout="responsive"
|
|
1010
|
+
alt="{{#if feature_image_alt}}{{feature_image_alt}}{{else}}{{title}}{{/if}}"
|
|
1011
|
+
></amp-img>
|
|
1010
1012
|
</figure>
|
|
1011
1013
|
{{/if}}
|
|
1012
1014
|
<section class="post-content">
|
|
@@ -1020,7 +1022,7 @@
|
|
|
1020
1022
|
{{/post}}
|
|
1021
1023
|
<footer class="page-footer">
|
|
1022
1024
|
{{#if @site.icon}}
|
|
1023
|
-
<amp-img class="site-icon" src="{{img_url @site.icon absolute="true"}}" width="50" height="50" layout="fixed"></amp-img>
|
|
1025
|
+
<amp-img class="site-icon" src="{{img_url @site.icon absolute="true"}}" width="50" height="50" layout="fixed" alt="{{@site.title}}"></amp-img>
|
|
1024
1026
|
{{/if}}
|
|
1025
1027
|
<h3>{{@site.title}}</h3>
|
|
1026
1028
|
{{#if @site.description}}
|
|
@@ -80,7 +80,7 @@ function resolvePaths(globals, data, value) {
|
|
|
80
80
|
path = path.replace(/\.\[/g, '[');
|
|
81
81
|
|
|
82
82
|
if (path.charAt(0) === '@') {
|
|
83
|
-
result = jsonpath.query(globals, path.
|
|
83
|
+
result = jsonpath.query(globals, path.slice(1));
|
|
84
84
|
} else {
|
|
85
85
|
// Do the query, which always returns an array of matches
|
|
86
86
|
result = jsonpath.query(data, path);
|
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
const debug = require('@tryghost/debug')('services:routing:controllers:unsubscribe');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const url = require('url');
|
|
4
|
+
|
|
5
|
+
const urlUtils = require('../../../../shared/url-utils');
|
|
3
6
|
const megaService = require('../../../../server/services/mega');
|
|
4
7
|
const renderer = require('../../rendering');
|
|
8
|
+
const labs = require('../../../../shared/labs');
|
|
5
9
|
|
|
6
10
|
module.exports = async function unsubscribeController(req, res) {
|
|
7
11
|
debug('unsubscribeController');
|
|
8
12
|
|
|
13
|
+
if (labs.isSet('multipleNewslettersUI')) {
|
|
14
|
+
const {query} = url.parse(req.url, true);
|
|
15
|
+
|
|
16
|
+
if (!query || !query.uuid) {
|
|
17
|
+
res.writeHead(400);
|
|
18
|
+
return res.end('Email address not found.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const redirectUrl = new URL(urlUtils.urlFor('home', true));
|
|
22
|
+
redirectUrl.searchParams.append('uuid', query.uuid);
|
|
23
|
+
if (query.newsletter) {
|
|
24
|
+
redirectUrl.searchParams.append('newsletter', query.newsletter);
|
|
25
|
+
}
|
|
26
|
+
redirectUrl.searchParams.append('action', 'unsubscribe');
|
|
27
|
+
|
|
28
|
+
return res.redirect(302, redirectUrl.href);
|
|
29
|
+
}
|
|
30
|
+
|
|
9
31
|
let data = {};
|
|
10
32
|
|
|
11
33
|
try {
|
|
@@ -366,6 +366,9 @@ module.exports = {
|
|
|
366
366
|
if (labsService.isSet('multipleProducts')) {
|
|
367
367
|
frame.options.withRelated.push('products');
|
|
368
368
|
}
|
|
369
|
+
if (labsService.isSet('multipleNewsletters')) {
|
|
370
|
+
frame.options.withRelated.push('newsletters');
|
|
371
|
+
}
|
|
369
372
|
const page = await membersService.api.members.list(frame.options);
|
|
370
373
|
|
|
371
374
|
return page;
|
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
const models = require('../../models');
|
|
2
2
|
const tpl = require('@tryghost/tpl');
|
|
3
3
|
const errors = require('@tryghost/errors');
|
|
4
|
+
const allowedIncludes = ['count.posts', 'count.members'];
|
|
4
5
|
|
|
5
6
|
const messages = {
|
|
6
7
|
newsletterNotFound: 'Newsletter not found.'
|
|
7
8
|
};
|
|
9
|
+
const newslettersService = require('../../services/newsletters');
|
|
8
10
|
|
|
9
11
|
module.exports = {
|
|
10
12
|
docName: 'newsletters',
|
|
11
13
|
|
|
12
14
|
browse: {
|
|
13
15
|
options: [
|
|
16
|
+
'include',
|
|
14
17
|
'filter',
|
|
15
18
|
'fields',
|
|
16
19
|
'limit',
|
|
17
20
|
'order',
|
|
18
21
|
'page'
|
|
19
22
|
],
|
|
23
|
+
validation: {
|
|
24
|
+
options: {
|
|
25
|
+
include: {
|
|
26
|
+
values: allowedIncludes
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
20
30
|
permissions: true,
|
|
21
31
|
query(frame) {
|
|
22
32
|
return models.Newsletter.findPage(frame.options);
|
|
@@ -25,6 +35,7 @@ module.exports = {
|
|
|
25
35
|
|
|
26
36
|
read: {
|
|
27
37
|
options: [
|
|
38
|
+
'include',
|
|
28
39
|
'fields',
|
|
29
40
|
'debug',
|
|
30
41
|
// NOTE: only for internal context
|
|
@@ -36,6 +47,13 @@ module.exports = {
|
|
|
36
47
|
'slug',
|
|
37
48
|
'uuid'
|
|
38
49
|
],
|
|
50
|
+
validation: {
|
|
51
|
+
options: {
|
|
52
|
+
include: {
|
|
53
|
+
values: allowedIncludes
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
39
57
|
permissions: true,
|
|
40
58
|
async query(frame) {
|
|
41
59
|
const newsletter = models.Newsletter.findOne(frame.data, frame.options);
|
|
@@ -51,27 +69,59 @@ module.exports = {
|
|
|
51
69
|
|
|
52
70
|
add: {
|
|
53
71
|
statusCode: 201,
|
|
72
|
+
headers: {
|
|
73
|
+
cacheInvalidate: true
|
|
74
|
+
},
|
|
75
|
+
options: [
|
|
76
|
+
'include',
|
|
77
|
+
'opt_in_existing'
|
|
78
|
+
],
|
|
79
|
+
validation: {
|
|
80
|
+
options: {
|
|
81
|
+
include: {
|
|
82
|
+
values: allowedIncludes
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
54
86
|
permissions: true,
|
|
55
87
|
async query(frame) {
|
|
56
|
-
return
|
|
88
|
+
return newslettersService.add(frame.data.newsletters[0], frame.options);
|
|
57
89
|
}
|
|
58
90
|
},
|
|
59
91
|
|
|
60
92
|
edit: {
|
|
61
|
-
headers: {
|
|
93
|
+
headers: {
|
|
94
|
+
cacheInvalidate: true
|
|
95
|
+
},
|
|
62
96
|
options: [
|
|
63
|
-
'id'
|
|
97
|
+
'id',
|
|
98
|
+
'include'
|
|
64
99
|
],
|
|
65
100
|
validation: {
|
|
66
101
|
options: {
|
|
67
102
|
id: {
|
|
68
103
|
required: true
|
|
104
|
+
},
|
|
105
|
+
include: {
|
|
106
|
+
values: allowedIncludes
|
|
69
107
|
}
|
|
70
108
|
}
|
|
71
109
|
},
|
|
72
110
|
permissions: true,
|
|
73
111
|
async query(frame) {
|
|
74
|
-
return
|
|
112
|
+
return newslettersService.edit(frame.data.newsletters[0], frame.options);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
verifyPropertyUpdate: {
|
|
117
|
+
permissions: {
|
|
118
|
+
method: 'edit'
|
|
119
|
+
},
|
|
120
|
+
data: [
|
|
121
|
+
'token'
|
|
122
|
+
],
|
|
123
|
+
async query(frame) {
|
|
124
|
+
return newslettersService.verifyPropertyUpdate(frame.data.token);
|
|
75
125
|
}
|
|
76
126
|
}
|
|
77
127
|
};
|
|
@@ -19,5 +19,14 @@ module.exports = {
|
|
|
19
19
|
async query() {
|
|
20
20
|
return await statsService.getMRRHistory();
|
|
21
21
|
}
|
|
22
|
+
},
|
|
23
|
+
subscriptions: {
|
|
24
|
+
permissions: {
|
|
25
|
+
docName: 'members',
|
|
26
|
+
method: 'browse'
|
|
27
|
+
},
|
|
28
|
+
async query() {
|
|
29
|
+
return await statsService.getSubscriptionCountHistory();
|
|
30
|
+
}
|
|
22
31
|
}
|
|
23
32
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const debug = require('@tryghost/debug')('api:canary:utils:serializers:input:members');
|
|
3
|
+
const mapNQLKeyValues = require('@tryghost/nql').utils.mapKeyValues;
|
|
4
|
+
const labsService = require('../../../../../../shared/labs');
|
|
3
5
|
|
|
4
6
|
function defaultRelations(frame) {
|
|
5
7
|
if (frame.options.withRelated) {
|
|
@@ -17,6 +19,26 @@ module.exports = {
|
|
|
17
19
|
browse(apiConfig, frame) {
|
|
18
20
|
debug('browse');
|
|
19
21
|
defaultRelations(frame);
|
|
22
|
+
|
|
23
|
+
if (!frame.options.order) {
|
|
24
|
+
frame.options.autoOrder = 'created_at DESC, id DESC';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (labsService.isSet('multipleNewsletters')) {
|
|
28
|
+
frame.options.mongoTransformer = mapNQLKeyValues({
|
|
29
|
+
key: {
|
|
30
|
+
from: 'subscribed',
|
|
31
|
+
to: 'newsletters.status'
|
|
32
|
+
},
|
|
33
|
+
values: [{
|
|
34
|
+
from: true,
|
|
35
|
+
to: 'active'
|
|
36
|
+
}, {
|
|
37
|
+
from: false,
|
|
38
|
+
to: {$ne: 'active'}
|
|
39
|
+
}]
|
|
40
|
+
});
|
|
41
|
+
}
|
|
20
42
|
},
|
|
21
43
|
|
|
22
44
|
read() {
|
|
@@ -132,11 +132,19 @@ function serializeMember(member, options) {
|
|
|
132
132
|
serialized.products = json.products;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
if (
|
|
136
|
-
json.newsletters
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
if (labsService.isSet('multipleNewsletters')) {
|
|
136
|
+
if (json.newsletters) {
|
|
137
|
+
serialized.newsletters = json.newsletters
|
|
138
|
+
.filter(newsletter => newsletter.status === 'active')
|
|
139
|
+
.sort((a, b) => {
|
|
140
|
+
return a.sort_order - b.sort_order;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// override the `subscribed` param to mean "subscribed to any active newsletter"
|
|
144
|
+
serialized.subscribed = false;
|
|
145
|
+
if (Array.isArray(serialized.newsletters) && serialized.newsletters.length > 0) {
|
|
146
|
+
serialized.subscribed = true;
|
|
147
|
+
}
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
return serialized;
|
|
@@ -82,9 +82,6 @@ const http = (apiImpl) => {
|
|
|
82
82
|
res.status(statusCode);
|
|
83
83
|
|
|
84
84
|
// CASE: generate headers based on the api ctrl configuration
|
|
85
|
-
if (req && req.headers && req.headers['accept-version'] && res.locals) {
|
|
86
|
-
headers['Content-Version'] = `v${res.locals.safeVersion}`;
|
|
87
|
-
}
|
|
88
85
|
res.set(headers);
|
|
89
86
|
|
|
90
87
|
const send = (format) => {
|
|
@@ -45,6 +45,8 @@ const mapPost = (model, frame) => {
|
|
|
45
45
|
}
|
|
46
46
|
date.forPost(jsonModel);
|
|
47
47
|
gating.forPost(jsonModel, frame);
|
|
48
|
+
|
|
49
|
+
delete jsonModel.newsletter_id;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
if (typeof jsonModel.email_recipient_filter === 'undefined') {
|
|
@@ -100,6 +102,7 @@ const mapPage = (model, frame) => {
|
|
|
100
102
|
delete jsonModel.email_subject;
|
|
101
103
|
delete jsonModel.send_email_when_published;
|
|
102
104
|
delete jsonModel.email_recipient_filter;
|
|
105
|
+
delete jsonModel.newsletter_id;
|
|
103
106
|
|
|
104
107
|
return jsonModel;
|
|
105
108
|
};
|
|
@@ -440,6 +440,44 @@ function createDropColumnMigration(table, column, columnDefinition) {
|
|
|
440
440
|
);
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
/**
|
|
444
|
+
* @param {string} table
|
|
445
|
+
* @param {string} column
|
|
446
|
+
*
|
|
447
|
+
* @returns {Migration}
|
|
448
|
+
*/
|
|
449
|
+
function createSetNullableMigration(table, column) {
|
|
450
|
+
return createNonTransactionalMigration(
|
|
451
|
+
async function up(knex) {
|
|
452
|
+
logging.info(`Setting nullable: ${table}.${column}`);
|
|
453
|
+
await commands.setNullable(table, column, knex);
|
|
454
|
+
},
|
|
455
|
+
async function down(knex) {
|
|
456
|
+
logging.info(`Dropping nullable: ${table}.${column}`);
|
|
457
|
+
await commands.dropNullable(table, column, knex);
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* @param {string} table
|
|
464
|
+
* @param {string} column
|
|
465
|
+
*
|
|
466
|
+
* @returns {Migration}
|
|
467
|
+
*/
|
|
468
|
+
function createDropNullableMigration(table, column) {
|
|
469
|
+
return createNonTransactionalMigration(
|
|
470
|
+
async function up(knex) {
|
|
471
|
+
logging.info(`Dropping nullable: ${table}.${column}`);
|
|
472
|
+
await commands.dropNullable(table, column, knex);
|
|
473
|
+
},
|
|
474
|
+
async function down(knex) {
|
|
475
|
+
logging.info(`Setting nullable: ${table}.${column}`);
|
|
476
|
+
await commands.setNullable(table, column, knex);
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
443
481
|
/**
|
|
444
482
|
* Creates a migration which will insert a new setting in settings table
|
|
445
483
|
* @param {object} settingSpec - setting key, value, group and type
|
|
@@ -505,6 +543,8 @@ module.exports = {
|
|
|
505
543
|
combineNonTransactionalMigrations,
|
|
506
544
|
createAddColumnMigration,
|
|
507
545
|
createDropColumnMigration,
|
|
546
|
+
createSetNullableMigration,
|
|
547
|
+
createDropNullableMigration,
|
|
508
548
|
meta: {
|
|
509
549
|
MIGRATION_USER
|
|
510
550
|
}
|
package/core/server/data/migrations/versions/4.43/2022-03-28-19-26-recreate-newsletter-table.js
CHANGED
|
@@ -8,7 +8,7 @@ module.exports = recreateTable('newsletters', {
|
|
|
8
8
|
sender_name: {type: 'string', maxlength: 191, nullable: false},
|
|
9
9
|
sender_email: {type: 'string', maxlength: 191, nullable: true},
|
|
10
10
|
sender_reply_to: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'newsletter', validations: {isIn: [['newsletter', 'support']]}},
|
|
11
|
-
status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'active'},
|
|
11
|
+
status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'active', validations: {isIn: [['active', 'archived']]}},
|
|
12
12
|
visibility: {
|
|
13
13
|
type: 'string',
|
|
14
14
|
maxlength: 50,
|
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
|
+
});
|