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.
Files changed (83) hide show
  1. package/Gruntfile.js +1 -1
  2. package/core/built/assets/{chunk.3.6e2ed2d00856e12bd81a.js → chunk.3.52b444495dfcf50afb0b.js} +20 -20
  3. package/core/built/assets/ghost-dark-155e039c0d991b7af75dea8cd3846b11.css +1 -0
  4. package/core/built/assets/{ghost.min-aafce1ab3f2ab6b4a385e8b888548e15.js → ghost.min-30e597cb65b62b31a9422ca9c0eb2890.js} +707 -633
  5. package/core/built/assets/ghost.min-bd8cd0185fd5dfc8291502f801e443e6.css +1 -0
  6. package/core/built/assets/icons/clock.svg +1 -1
  7. package/core/built/assets/icons/email-at.svg +1 -0
  8. package/core/built/assets/icons/email-body.svg +1 -0
  9. package/core/built/assets/icons/email-footer.svg +1 -0
  10. package/core/built/assets/icons/email-header.svg +1 -0
  11. package/core/built/assets/icons/email-member.svg +1 -0
  12. package/core/built/assets/icons/email-name.svg +1 -0
  13. package/core/built/assets/icons/member.svg +1 -3
  14. package/core/built/assets/icons/send-email.svg +1 -1
  15. package/core/built/assets/img/community-background-3f501ff1d764d0cb81f7c2cbacfc6503.jpg +0 -0
  16. package/core/built/assets/img/newsletter-1-197ae8063dfb2e22278d355198029c9e.jpg +0 -0
  17. package/core/built/assets/img/newsletter-2-5a2c7693ea9380d4282061302c01267a.jpg +0 -0
  18. package/core/built/assets/img/resource-1-722f202795856e4a5596c8a3b7bedc43.jpg +0 -0
  19. package/core/built/assets/{vendor.min-eaf9e7b39e2ba76722eabc7a814e0ff1.js → vendor.min-97fd438f4772c5ec6bb30ad779b8530e.js} +829 -493
  20. package/core/frontend/apps/amp/lib/views/amp.hbs +5 -3
  21. package/core/frontend/helpers/get.js +1 -1
  22. package/core/frontend/services/routing/controllers/unsubscribe.js +22 -0
  23. package/core/server/api/canary/members.js +3 -0
  24. package/core/server/api/canary/newsletters.js +54 -4
  25. package/core/server/api/canary/stats.js +9 -0
  26. package/core/server/api/canary/utils/serializers/input/members.js +22 -0
  27. package/core/server/api/canary/utils/serializers/output/mappers/pages.js +1 -0
  28. package/core/server/api/canary/utils/serializers/output/mappers/posts.js +2 -0
  29. package/core/server/api/canary/utils/serializers/output/members.js +13 -5
  30. package/core/server/api/shared/http.js +0 -3
  31. package/core/server/api/v2/utils/serializers/output/utils/mapper.js +2 -0
  32. package/core/server/api/v3/utils/serializers/output/utils/mapper.js +3 -0
  33. package/core/server/data/migrations/utils.js +40 -0
  34. package/core/server/data/migrations/versions/4.43/2022-03-28-19-26-recreate-newsletter-table.js +1 -1
  35. package/core/server/data/migrations/versions/4.46/2022-04-13-12-00-add-created-at-newsletters.js +6 -0
  36. package/core/server/data/migrations/versions/4.46/2022-04-13-12-01-add-updated-at-newsletters.js +6 -0
  37. package/core/server/data/migrations/versions/4.46/2022-04-13-12-02-fill-created-at-newsletters.js +19 -0
  38. package/core/server/data/migrations/versions/4.46/2022-04-13-12-03-drop-nullable-created-at-newsletters.js +3 -0
  39. package/core/server/data/migrations/versions/4.46/2022-04-13-12-08-newsletters-show-header-name.js +7 -0
  40. package/core/server/data/migrations/versions/4.46/2022-04-13-12-57-add-uuid-column-to-newsletters.js +8 -0
  41. package/core/server/data/migrations/versions/4.46/2022-04-13-12-58-fill-uuid-for-newsletters.js +19 -0
  42. package/core/server/data/migrations/versions/4.46/2022-04-13-12-59-drop-nullable-uuid-newsletters.js +3 -0
  43. package/core/server/data/migrations/versions/4.46/2022-04-13-13-00-add-default-newsletter.js +92 -0
  44. package/core/server/data/migrations/versions/4.46/2022-04-20-08-39-map-subscribers-to-default-newsletter.js +66 -0
  45. package/core/server/data/migrations/versions/4.46/2022-04-22-07-43-add-newsletter-id-to-subscribe-events.js +9 -0
  46. package/core/server/data/migrations/versions/4.46/2022-04-27-07-59-set-newsletter-id-subscribe-events.js +31 -0
  47. package/core/server/data/schema/commands.js +14 -0
  48. package/core/server/data/schema/fixtures/fixtures.json +2 -1
  49. package/core/server/data/schema/schema.js +8 -3
  50. package/core/server/models/base/plugins/generate-slug.js +2 -2
  51. package/core/server/models/email.js +4 -0
  52. package/core/server/models/member-subscribe-event.js +4 -0
  53. package/core/server/models/member.js +26 -0
  54. package/core/server/models/newsletter.js +97 -14
  55. package/core/server/models/post.js +6 -3
  56. package/core/server/services/api-version-compatibility/index.js +22 -5
  57. package/core/server/services/auth/members/index.js +1 -1
  58. package/core/server/services/mega/email-preview.js +4 -1
  59. package/core/server/services/mega/mega.js +83 -26
  60. package/core/server/services/mega/post-email-serializer.js +17 -14
  61. package/core/server/services/mega/template.js +24 -3
  62. package/core/server/services/members/api.js +2 -2
  63. package/core/server/services/members/middleware.js +63 -4
  64. package/core/server/services/members/service.js +7 -4
  65. package/core/server/services/newsletters/emails/verify-email.js +166 -0
  66. package/core/server/services/newsletters/index.js +14 -7
  67. package/core/server/services/newsletters/service.js +237 -6
  68. package/core/server/services/posts/posts-service.js +7 -9
  69. package/core/server/services/users.js +20 -20
  70. package/core/server/web/admin/views/default-prod.html +4 -4
  71. package/core/server/web/admin/views/default.html +4 -4
  72. package/core/server/web/api/app.js +4 -3
  73. package/core/server/web/api/canary/admin/app.js +2 -3
  74. package/core/server/web/api/canary/admin/routes.js +2 -0
  75. package/core/server/web/api/v2/admin/app.js +2 -3
  76. package/core/server/web/api/v3/admin/app.js +2 -3
  77. package/core/server/web/members/app.js +5 -0
  78. package/core/shared/config/defaults.json +1 -1
  79. package/core/shared/labs.js +2 -2
  80. package/package.json +59 -59
  81. package/yarn.lock +794 -525
  82. package/core/built/assets/ghost-dark-887882218a8f9a4a367de52212d27917.css +0 -1
  83. 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"></amp-img>
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.substr(1));
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 models.Newsletter.add(frame.data.newsletters[0], frame.options);
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 models.Newsletter.edit(frame.data.newsletters[0], frame.options);
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() {
@@ -6,6 +6,7 @@ module.exports = async (model, frame, options) => {
6
6
  delete jsonModel.email_subject;
7
7
  delete jsonModel.email_recipient_filter;
8
8
  delete jsonModel.email_only;
9
+ delete jsonModel.newsletter_id;
9
10
 
10
11
  return jsonModel;
11
12
  };
@@ -57,6 +57,8 @@ module.exports = async (model, frame, options = {}) => {
57
57
  }
58
58
  date.forPost(jsonModel);
59
59
  gating.forPost(jsonModel, frame);
60
+
61
+ delete jsonModel.newsletter_id;
60
62
  }
61
63
 
62
64
  // Transforms post/page metadata to flat structure
@@ -132,11 +132,19 @@ function serializeMember(member, options) {
132
132
  serialized.products = json.products;
133
133
  }
134
134
 
135
- if (json.newsletters && labsService.isSet('multipleNewsletters')) {
136
- json.newsletters.sort((a, b) => {
137
- return a.sort_order - b.sort_order;
138
- });
139
- serialized.newsletters = json.newsletters;
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) => {
@@ -48,6 +48,8 @@ const mapPost = (model, frame) => {
48
48
  }
49
49
  date.forPost(jsonModel);
50
50
  gating.forPost(jsonModel, frame);
51
+
52
+ delete jsonModel.newsletter_id;
51
53
  }
52
54
 
53
55
  // Transforms post/page metadata to flat structure
@@ -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
  }
@@ -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,
@@ -0,0 +1,6 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('newsletters', 'created_at', {
4
+ type: 'dateTime',
5
+ nullable: true
6
+ });
@@ -0,0 +1,6 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('newsletters', 'updated_at', {
4
+ type: 'dateTime',
5
+ nullable: true
6
+ });
@@ -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
+ );
@@ -0,0 +1,3 @@
1
+ const {createDropNullableMigration} = require('../../utils');
2
+
3
+ module.exports = createDropNullableMigration('newsletters', 'created_at');
@@ -0,0 +1,7 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('newsletters', 'show_header_name', {
4
+ type: 'boolean',
5
+ nullable: false,
6
+ defaultTo: true
7
+ });
@@ -0,0 +1,8 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('newsletters', 'uuid', {
4
+ type: 'string',
5
+ maxlength: 36,
6
+ nullable: true,
7
+ unique: true
8
+ });
@@ -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,3 @@
1
+ const {createDropNullableMigration} = require('../../utils');
2
+
3
+ module.exports = createDropNullableMigration('newsletters', 'uuid');
@@ -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
+ });