ghost 4.41.3 → 4.43.0

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 (147) hide show
  1. package/content/themes/casper/package.json +2 -3
  2. package/content/themes/casper/partials/post-card.hbs +1 -1
  3. package/core/built/assets/ghost-dark-1933079797e24ccb8839657020830be5.css +1 -0
  4. package/core/built/assets/{ghost.min-1abf114ca26a71e8e1f09054f3592614.js → ghost.min-2a278873d60d6a13a4c05a396e5bed5e.js} +533 -398
  5. package/core/built/assets/ghost.min-38f3c38c0c6a1864f57079b068a0b0ce.css +1 -0
  6. package/core/built/assets/{vendor.min-9094db77ba3190cb10876f8e42e1d90d.js → vendor.min-21f79c68a284acb1b70039f3f63e5507.js} +68 -68
  7. package/core/built/assets/{vendor.min-2c8ad32b7960bb605ebc20097fee5ebd.css → vendor.min-ba66b98f7c24fa40e061c7ffc94f4e23.css} +214 -0
  8. package/core/frontend/apps/amp/lib/helpers/amp_analytics.js +1 -1
  9. package/core/frontend/apps/amp/lib/helpers/amp_components.js +1 -1
  10. package/core/frontend/apps/amp/lib/helpers/amp_content.js +1 -1
  11. package/core/frontend/apps/amp/lib/helpers/amp_style.js +1 -1
  12. package/core/frontend/apps/amp/lib/router.js +6 -5
  13. package/core/frontend/apps/private-blogging/lib/helpers/input_password.js +1 -1
  14. package/core/frontend/apps/private-blogging/lib/router.js +2 -2
  15. package/core/frontend/helpers/asset.js +1 -1
  16. package/core/frontend/helpers/author.js +1 -1
  17. package/core/frontend/helpers/authors.js +1 -1
  18. package/core/frontend/helpers/body_class.js +1 -1
  19. package/core/frontend/helpers/cancel_link.js +1 -1
  20. package/core/frontend/helpers/concat.js +1 -1
  21. package/core/frontend/helpers/content.js +1 -1
  22. package/core/frontend/helpers/date.js +1 -1
  23. package/core/frontend/helpers/encode.js +1 -1
  24. package/core/frontend/helpers/excerpt.js +1 -1
  25. package/core/frontend/helpers/facebook_url.js +1 -1
  26. package/core/frontend/helpers/foreach.js +2 -2
  27. package/core/frontend/helpers/get.js +1 -1
  28. package/core/frontend/helpers/ghost_foot.js +1 -1
  29. package/core/frontend/helpers/ghost_head.js +1 -1
  30. package/core/frontend/helpers/lang.js +1 -1
  31. package/core/frontend/helpers/link.js +1 -1
  32. package/core/frontend/helpers/link_class.js +1 -1
  33. package/core/frontend/helpers/match.js +1 -1
  34. package/core/frontend/helpers/navigation.js +1 -1
  35. package/core/frontend/helpers/pagination.js +1 -1
  36. package/core/frontend/helpers/plural.js +1 -1
  37. package/core/frontend/helpers/post_class.js +1 -1
  38. package/core/frontend/helpers/prev_post.js +6 -5
  39. package/core/frontend/helpers/price.js +1 -0
  40. package/core/frontend/helpers/products.js +1 -1
  41. package/core/frontend/helpers/reading_time.js +2 -2
  42. package/core/frontend/helpers/t.js +1 -1
  43. package/core/frontend/helpers/tags.js +1 -1
  44. package/core/frontend/helpers/tiers.js +1 -1
  45. package/core/frontend/helpers/title.js +1 -1
  46. package/core/frontend/helpers/twitter_url.js +1 -1
  47. package/core/frontend/helpers/url.js +1 -1
  48. package/core/frontend/meta/url.js +4 -4
  49. package/core/{server/data/schema → frontend/services/data}/checks.js +4 -4
  50. package/core/frontend/services/{routing/helpers → data}/entry-lookup.js +3 -3
  51. package/core/frontend/services/{routing/helpers → data}/fetch-data.js +3 -3
  52. package/core/frontend/services/data/index.js +5 -0
  53. package/core/frontend/services/{rendering.js → handlebars.js} +2 -1
  54. package/core/frontend/services/helpers/handlebars.js +1 -1
  55. package/core/frontend/services/proxy.js +2 -4
  56. package/core/frontend/services/{routing/helpers → rendering}/context.js +0 -0
  57. package/core/frontend/services/{routing/helpers → rendering}/error.js +0 -0
  58. package/core/frontend/services/{routing/helpers → rendering}/format-response.js +1 -1
  59. package/core/frontend/services/{routing/helpers → rendering}/index.js +0 -8
  60. package/core/frontend/services/{routing/helpers → rendering}/render-entries.js +1 -1
  61. package/core/frontend/services/{routing/helpers → rendering}/render-entry.js +1 -1
  62. package/core/frontend/services/{routing/helpers → rendering}/renderer.js +1 -1
  63. package/core/frontend/services/{routing/helpers → rendering}/secure.js +0 -0
  64. package/core/frontend/services/{routing/helpers → rendering}/templates.js +2 -2
  65. package/core/frontend/services/routing/CollectionRouter.js +1 -1
  66. package/core/frontend/services/routing/controllers/channel.js +9 -9
  67. package/core/frontend/services/routing/controllers/collection.js +9 -9
  68. package/core/frontend/services/routing/controllers/email-post.js +5 -6
  69. package/core/frontend/services/routing/controllers/entry.js +6 -6
  70. package/core/frontend/services/routing/controllers/preview.js +5 -6
  71. package/core/frontend/services/routing/controllers/rss.js +4 -3
  72. package/core/frontend/services/routing/controllers/static.js +5 -5
  73. package/core/frontend/services/routing/controllers/unsubscribe.js +2 -2
  74. package/core/frontend/services/routing/index.js +0 -4
  75. package/core/frontend/web/middleware/error-handler.js +2 -2
  76. package/core/server/api/canary/email-preview.js +2 -1
  77. package/core/server/api/canary/{email.js → emails.js} +0 -0
  78. package/core/server/api/canary/index.js +9 -1
  79. package/core/server/api/canary/members.js +0 -45
  80. package/core/server/api/canary/newsletters.js +45 -0
  81. package/core/server/api/canary/stats.js +23 -0
  82. package/core/server/api/canary/utils/serializers/output/email-previews.js +7 -0
  83. package/core/server/api/canary/utils/serializers/output/index.js +2 -22
  84. package/core/server/api/canary/utils/serializers/output/mappers/index.js +1 -0
  85. package/core/server/api/canary/utils/serializers/output/mappers/snippets.js +36 -0
  86. package/core/server/api/canary/utils/serializers/output/members.js +5 -2
  87. package/core/server/api/canary/utils/serializers/output/oembed.js +2 -2
  88. package/core/server/api/canary/utils/serializers/output/redirects.js +2 -2
  89. package/core/server/api/canary/utils/serializers/output/schedules.js +2 -2
  90. package/core/server/api/canary/utils/serializers/output/slack.js +2 -2
  91. package/core/server/api/canary/utils/serializers/output/themes.js +2 -2
  92. package/core/server/api/canary/utils/serializers/output/users.js +0 -23
  93. package/core/server/api/canary/utils/validators/input/index.js +6 -0
  94. package/core/server/api/shared/http.js +52 -51
  95. package/core/server/api/shared/serializers/handle.js +25 -26
  96. package/core/server/data/exporter/table-lists.js +2 -0
  97. package/core/server/data/migrations/utils.js +34 -2
  98. package/core/server/data/migrations/versions/4.42/2022-03-21-17-17-add.js +25 -0
  99. package/core/server/data/migrations/versions/4.42/2022-03-30-15-44-add-newsletter-permissions.js +28 -0
  100. package/core/server/data/migrations/versions/4.43/2022-03-28-19-26-recreate-newsletter-table.js +29 -0
  101. package/core/server/data/migrations/versions/4.43/2022-03-29-14-45-add-members-newsletters-table.js +7 -0
  102. package/core/server/data/migrations/versions/4.43/2022-04-01-10-13-add-post-newsletter-relation.js +108 -0
  103. package/core/server/data/migrations/versions/4.43/2022-04-06-09-47-add-type-column-to-paid-subscription-events.js +7 -0
  104. package/core/server/data/migrations/versions/4.43/2022-04-06-14-56-add-email-newsletter-relation.js +8 -0
  105. package/core/server/data/migrations/versions/4.43/2022-04-08-10-45-add-subscription-id-to-mrr-events.js +7 -0
  106. package/core/server/data/schema/commands.js +19 -14
  107. package/core/server/data/schema/index.js +0 -1
  108. package/core/server/data/schema/schema.js +36 -0
  109. package/core/server/models/base/bookshelf.js +1 -1
  110. package/core/server/models/base/plugins/crud.js +8 -0
  111. package/core/server/models/member.js +18 -1
  112. package/core/server/models/newsletter.js +43 -0
  113. package/core/server/models/post.js +4 -1
  114. package/core/server/services/auth/setup.js +4 -1
  115. package/core/server/services/mega/template.js +25 -13
  116. package/core/server/services/members/api.js +3 -1
  117. package/core/server/services/members/middleware.js +13 -3
  118. package/core/server/services/members/service.js +2 -1
  119. package/core/server/services/members/utils.js +13 -1
  120. package/core/server/services/newsletters/index.js +10 -0
  121. package/core/server/services/newsletters/service.js +24 -0
  122. package/core/server/services/slack.js +11 -3
  123. package/core/server/services/stats/index.js +1 -0
  124. package/core/server/services/stats/lib/members-stats-service.js +161 -0
  125. package/core/server/services/stats/lib/mrr-stats-service.js +154 -0
  126. package/core/server/services/stats/service.js +8 -0
  127. package/core/server/services/stripe/service.js +1 -0
  128. package/core/server/services/webhooks/webhooks-service.js +3 -1
  129. package/core/server/web/admin/views/default-prod.html +5 -5
  130. package/core/server/web/admin/views/default.html +5 -5
  131. package/core/server/web/api/canary/admin/routes.js +9 -2
  132. package/core/shared/config/defaults.json +2 -2
  133. package/core/shared/config/env/config.development.json +26 -0
  134. package/core/shared/config/env/config.production.json +21 -0
  135. package/core/shared/config/env/config.testing-mysql.json +59 -0
  136. package/core/shared/config/env/config.testing.json +58 -0
  137. package/package.json +50 -50
  138. package/yarn.lock +700 -769
  139. package/content/themes/casper/assets/css/csscomb.json +0 -240
  140. package/core/built/assets/ghost-dark-146c4c688b47d45c4aa018ee0f79cebc.css +0 -1
  141. package/core/built/assets/ghost.min-a73b150c7eecc4641d377cc73fb5eecd.css +0 -1
  142. package/core/server/api/canary/utils/serializers/output/email-preview.js +0 -10
  143. package/core/server/api/canary/utils/serializers/output/emails.js +0 -22
  144. package/core/server/api/canary/utils/serializers/output/identities.js +0 -7
  145. package/core/server/api/canary/utils/serializers/output/member-signin-urls.js +0 -7
  146. package/core/server/api/canary/utils/serializers/output/snippets.js +0 -107
  147. package/core/server/api/canary/utils/serializers/output/webhooks.js +0 -15
@@ -0,0 +1,45 @@
1
+ const models = require('../../models');
2
+
3
+ module.exports = {
4
+ docName: 'newsletters',
5
+
6
+ browse: {
7
+ options: [
8
+ 'filter',
9
+ 'fields',
10
+ 'limit',
11
+ 'order',
12
+ 'page'
13
+ ],
14
+ permissions: true,
15
+ query(frame) {
16
+ return models.Newsletter.findPage(frame.options);
17
+ }
18
+ },
19
+
20
+ add: {
21
+ statusCode: 201,
22
+ permissions: true,
23
+ async query(frame) {
24
+ return models.Newsletter.add(frame.data.newsletters[0], frame.options);
25
+ }
26
+ },
27
+
28
+ edit: {
29
+ headers: {},
30
+ options: [
31
+ 'id'
32
+ ],
33
+ validation: {
34
+ options: {
35
+ id: {
36
+ required: true
37
+ }
38
+ }
39
+ },
40
+ permissions: true,
41
+ async query(frame) {
42
+ return models.Newsletter.edit(frame.data.newsletters[0], frame.options);
43
+ }
44
+ }
45
+ };
@@ -0,0 +1,23 @@
1
+ const statsService = require('../../services/stats');
2
+
3
+ module.exports = {
4
+ docName: 'stats',
5
+ memberCountHistory: {
6
+ permissions: {
7
+ docName: 'members',
8
+ method: 'browse'
9
+ },
10
+ async query() {
11
+ return await statsService.members.getCountHistory();
12
+ }
13
+ },
14
+ mrr: {
15
+ permissions: {
16
+ docName: 'members',
17
+ method: 'browse'
18
+ },
19
+ async query() {
20
+ return await statsService.mrr.getHistory();
21
+ }
22
+ }
23
+ };
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ // @TODO remove for 5.0
3
+ // This should be a default serializer, not a passthrough, as per the read endpoint
4
+ sendTestEmail(data, apiConfig, frame) {
5
+ frame.response = data;
6
+ }
7
+ };
@@ -41,10 +41,6 @@ module.exports = {
41
41
  return require('./schedules');
42
42
  },
43
43
 
44
- get webhooks() {
45
- return require('./webhooks');
46
- },
47
-
48
44
  get posts() {
49
45
  return require('./posts');
50
46
  },
@@ -73,14 +69,6 @@ module.exports = {
73
69
  return require('./tiers');
74
70
  },
75
71
 
76
- get member_signin_urls() {
77
- return require('./member-signin-urls');
78
- },
79
-
80
- get identities() {
81
- return require('./identities');
82
- },
83
-
84
72
  get images() {
85
73
  return require('./images');
86
74
  },
@@ -121,16 +109,8 @@ module.exports = {
121
109
  return require('./site');
122
110
  },
123
111
 
124
- get email_preview() {
125
- return require('./email-preview');
126
- },
127
-
128
- get emails() {
129
- return require('./emails');
130
- },
131
-
132
- get snippets() {
133
- return require('./snippets');
112
+ get email_previews() {
113
+ return require('./email-previews');
134
114
  },
135
115
 
136
116
  get custom_theme_settings() {
@@ -7,6 +7,7 @@ module.exports = {
7
7
  pages: require('./pages'),
8
8
  posts: require('./posts'),
9
9
  settings: require('./settings'),
10
+ snippets: require('./snippets'),
10
11
  tags: require('./tags'),
11
12
  users: require('./users')
12
13
  };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @param {import('bookshelf').Model} snippet
3
+ * @param {Frame} frame
4
+ *
5
+ * @returns {SerializedSnippet}
6
+ */
7
+ module.exports = (snippet, frame) => {
8
+ const json = snippet.toJSON(frame.options);
9
+
10
+ return {
11
+ id: json.id,
12
+ name: json.name,
13
+ // @ts-ignore
14
+ mobiledoc: json.mobiledoc,
15
+ created_at: json.created_at,
16
+ updated_at: json.updated_at,
17
+ created_by: json.created_by,
18
+ updated_by: json.updated_by
19
+ };
20
+ };
21
+
22
+ /**
23
+ * @typedef {Object} SerializedSnippet
24
+ * @prop {string} id
25
+ * @prop {string=} name
26
+ * @prop {string=} mobiledoc
27
+ * @prop {string} created_at
28
+ * @prop {string} updated_at
29
+ * @prop {string} created_by
30
+ * @prop {string} updated_by
31
+ */
32
+
33
+ /**
34
+ * @typedef {Object<string, any>} Frame
35
+ * @prop {Object} options
36
+ */
@@ -1,6 +1,7 @@
1
1
  //@ts-check
2
2
  const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:members');
3
3
  const {unparse} = require('@tryghost/members-csv');
4
+ const labsService = require('../../../../../../shared/labs');
4
5
 
5
6
  module.exports = {
6
7
  hasActiveStripeSubscriptions: createSerializer('hasActiveStripeSubscriptions', passthrough),
@@ -20,8 +21,6 @@ module.exports = {
20
21
  importCSV: createSerializer('importCSV', passthrough),
21
22
  memberStats: createSerializer('memberStats', passthrough),
22
23
  mrrStats: createSerializer('mrrStats', passthrough),
23
- subscriberStats: createSerializer('subscriberStats', passthrough),
24
- grossVolumeStats: createSerializer('grossVolumeStats', passthrough),
25
24
  activityFeed: createSerializer('activityFeed', passthrough)
26
25
  };
27
26
 
@@ -133,6 +132,10 @@ function serializeMember(member, options) {
133
132
  serialized.products = json.products;
134
133
  }
135
134
 
135
+ if (json.newsletters && labsService.isSet('multipleNewsletters')) {
136
+ serialized.newsletters = json.newsletters;
137
+ }
138
+
136
139
  return serialized;
137
140
  }
138
141
 
@@ -1,8 +1,8 @@
1
1
  const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:oembed');
2
2
 
3
3
  module.exports = {
4
- all(res, apiConfig, frame) {
4
+ all(data, apiConfig, frame) {
5
5
  debug('all');
6
- frame.response = res;
6
+ frame.response = data;
7
7
  }
8
8
  };
@@ -1,5 +1,5 @@
1
1
  module.exports = {
2
- all(response, apiConfig, frame) {
3
- frame.response = response;
2
+ all(data, apiConfig, frame) {
3
+ frame.response = data;
4
4
  }
5
5
  };
@@ -1,5 +1,5 @@
1
1
  module.exports = {
2
- all(model, apiConfig, frame) {
3
- frame.response = model;
2
+ all(data, apiConfig, frame) {
3
+ frame.response = data;
4
4
  }
5
5
  };
@@ -1,9 +1,9 @@
1
1
  const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:slack');
2
2
 
3
3
  module.exports = {
4
- all(themes, apiConfig, frame) {
4
+ all(data, apiConfig, frame) {
5
5
  debug('all');
6
6
 
7
- frame.response = themes;
7
+ frame.response = data;
8
8
  }
9
9
  };
@@ -1,9 +1,9 @@
1
1
  const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:themes');
2
2
 
3
3
  module.exports = {
4
- all(themes, apiConfig, frame) {
4
+ all(data, apiConfig, frame) {
5
5
  debug('all');
6
6
 
7
- frame.response = themes;
7
+ frame.response = data;
8
8
  }
9
9
  };
@@ -1,34 +1,11 @@
1
1
  const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:users');
2
2
  const tpl = require('@tryghost/tpl');
3
- const mappers = require('./mappers');
4
3
 
5
4
  const messages = {
6
5
  pwdChangedSuccessfully: 'Password changed successfully.'
7
6
  };
8
7
 
9
8
  module.exports = {
10
- browse(models, apiConfig, frame) {
11
- debug('browse');
12
-
13
- frame.response = {
14
- users: models.data.map(model => mappers.users(model, frame)),
15
- meta: models.meta
16
- };
17
- },
18
-
19
- read(model, apiConfig, frame) {
20
- debug('read');
21
-
22
- frame.response = {
23
- users: [mappers.users(model, frame)]
24
- };
25
- },
26
-
27
- edit() {
28
- debug('edit');
29
- this.read(...arguments);
30
- },
31
-
32
9
  destroy(filename, apiConfig, frame) {
33
10
  debug('destroy');
34
11
 
@@ -1,3 +1,9 @@
1
+ // ESLint Override Notice
2
+ // This is a valid index.js file - it just exports a lot of stuff!
3
+ // Long term we would like to change the API architecture to reduce this file,
4
+ // but that's not the problem the index.js max - line eslint "proxy" rule is there to solve.
5
+ /* eslint-disable max-lines */
6
+
1
7
  module.exports = {
2
8
  get passwordreset() {
3
9
  return require('./passwordreset');
@@ -13,7 +13,7 @@ const models = require('../../models');
13
13
  * @return {Function}
14
14
  */
15
15
  const http = (apiImpl) => {
16
- return (req, res, next) => {
16
+ return async (req, res, next) => {
17
17
  debug(`External API request to ${req.url}`);
18
18
  let apiKey = null;
19
19
  let integration = null;
@@ -60,69 +60,70 @@ const http = (apiImpl) => {
60
60
  data: apiImpl.data
61
61
  });
62
62
 
63
- apiImpl(frame)
64
- .then((result) => {
65
- debug(`External API request to ${frame.docName}.${frame.method}`);
66
- return shared.headers.get(result, apiImpl.headers, frame)
67
- .then(headers => ({result, headers}));
68
- })
69
- .then(({result, headers}) => {
70
- // CASE: api ctrl wants to handle the express response (e.g. streams)
71
- if (typeof result === 'function') {
72
- debug('ctrl function call');
73
- return result(req, res, next);
74
- }
63
+ try {
64
+ const result = await apiImpl(frame);
75
65
 
76
- let statusCode = 200;
77
- if (typeof apiImpl.statusCode === 'function') {
78
- statusCode = apiImpl.statusCode(result);
79
- } else if (apiImpl.statusCode) {
80
- statusCode = apiImpl.statusCode;
81
- }
66
+ debug(`External API request to ${frame.docName}.${frame.method}`);
67
+ const headers = await shared.headers.get(result, apiImpl.headers, frame) || {};
82
68
 
83
- res.status(statusCode);
69
+ // CASE: api ctrl wants to handle the express response (e.g. streams)
70
+ if (typeof result === 'function') {
71
+ debug('ctrl function call');
72
+ return result(req, res, next);
73
+ }
84
74
 
85
- // CASE: generate headers based on the api ctrl configuration
86
- res.set(headers);
75
+ let statusCode = 200;
76
+ if (typeof apiImpl.statusCode === 'function') {
77
+ statusCode = apiImpl.statusCode(result);
78
+ } else if (apiImpl.statusCode) {
79
+ statusCode = apiImpl.statusCode;
80
+ }
87
81
 
88
- const send = (format) => {
89
- if (format === 'plain') {
90
- debug('plain text response');
91
- return res.send(result);
92
- }
82
+ res.status(statusCode);
93
83
 
94
- debug('json response');
95
- res.json(result || {});
96
- };
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
+ res.set(headers);
97
89
 
98
- let responseFormat;
90
+ const send = (format) => {
91
+ if (format === 'plain') {
92
+ debug('plain text response');
93
+ return res.send(result);
94
+ }
99
95
 
100
- if (apiImpl.response){
101
- if (typeof apiImpl.response.format === 'function') {
102
- const apiResponseFormat = apiImpl.response.format();
96
+ debug('json response');
97
+ res.json(result || {});
98
+ };
99
+
100
+ let responseFormat;
101
+
102
+ if (apiImpl.response){
103
+ if (typeof apiImpl.response.format === 'function') {
104
+ const apiResponseFormat = apiImpl.response.format();
103
105
 
104
- if (apiResponseFormat.then) { // is promise
105
- return apiResponseFormat.then((formatName) => {
106
- send(formatName);
107
- });
108
- } else {
109
- responseFormat = apiResponseFormat;
110
- }
106
+ if (apiResponseFormat.then) { // is promise
107
+ return apiResponseFormat.then((formatName) => {
108
+ send(formatName);
109
+ });
111
110
  } else {
112
- responseFormat = apiImpl.response.format;
111
+ responseFormat = apiResponseFormat;
113
112
  }
113
+ } else {
114
+ responseFormat = apiImpl.response.format;
114
115
  }
116
+ }
115
117
 
116
- send(responseFormat);
117
- })
118
- .catch((err) => {
119
- req.frameOptions = {
120
- docName: frame.docName,
121
- method: frame.method
122
- };
118
+ send(responseFormat);
119
+ } catch (err) {
120
+ req.frameOptions = {
121
+ docName: frame.docName,
122
+ method: frame.method
123
+ };
123
124
 
124
- next(err);
125
- });
125
+ next(err);
126
+ }
126
127
  };
127
128
  };
128
129
 
@@ -67,6 +67,19 @@ module.exports.input = (apiConfig, apiSerializers, frame) => {
67
67
  return sequence(tasks);
68
68
  };
69
69
 
70
+ const getBestMatchSerializer = function (apiSerializers, docName, method) {
71
+ if (apiSerializers[docName] && apiSerializers[docName][method]) {
72
+ debug(`Calling ${docName}.${method}`);
73
+ return apiSerializers[docName][method].bind(apiSerializers[docName]);
74
+ } else if (apiSerializers[docName] && apiSerializers[docName].all) {
75
+ debug(`Calling ${docName}.all`);
76
+ return apiSerializers[docName].all.bind(apiSerializers[docName]);
77
+ }
78
+
79
+ debug(`Returning as-is`);
80
+ return false;
81
+ };
82
+
70
83
  /**
71
84
  * @description Shared output serialization handler.
72
85
  *
@@ -101,33 +114,19 @@ module.exports.output = (response = {}, apiConfig, apiSerializers, frame) => {
101
114
  });
102
115
  }
103
116
 
104
- // CASE: custom serializer exists
105
- if (apiSerializers[apiConfig.docName]) {
106
- if (apiSerializers[apiConfig.docName].all) {
107
- tasks.push(function serialiseCustomAll() {
108
- return apiSerializers[apiConfig.docName].all(response, apiConfig, frame);
109
- });
110
- }
117
+ const customSerializer = getBestMatchSerializer(apiSerializers, apiConfig.docName, apiConfig.method);
118
+ const defaultSerializer = getBestMatchSerializer(apiSerializers, 'default', apiConfig.method);
111
119
 
112
- if (apiSerializers[apiConfig.docName][apiConfig.method]) {
113
- tasks.push(function serialiseCustomMethod() {
114
- return apiSerializers[apiConfig.docName][apiConfig.method](response, apiConfig, frame);
115
- });
116
- }
117
-
118
- // CASE: Fall back to default serializer
119
- } else if (apiSerializers.default) {
120
- if (apiSerializers.default.all) {
121
- tasks.push(function serializeDefaultAll() {
122
- return apiSerializers.default.all(response, apiConfig, frame);
123
- });
124
- }
125
-
126
- if (apiSerializers.default[apiConfig.method]) {
127
- tasks.push(function serializeDefaultMethod() {
128
- return apiSerializers.default[apiConfig.method](response, apiConfig, frame);
129
- });
130
- }
120
+ if (customSerializer) {
121
+ // CASE: custom serializer exists
122
+ tasks.push(function doCustomSerializer() {
123
+ return customSerializer(response, apiConfig, frame);
124
+ });
125
+ } else if (defaultSerializer) {
126
+ // CASE: Fall back to default serializer
127
+ tasks.push(function doDefaultSerializer() {
128
+ return defaultSerializer(response, apiConfig, frame);
129
+ });
131
130
  }
132
131
 
133
132
  if (apiSerializers.all && apiSerializers.all.after) {
@@ -15,6 +15,7 @@ const BACKUP_TABLES = [
15
15
  'members_stripe_customers_subscriptions',
16
16
  'migrations',
17
17
  'migrations_lock',
18
+ 'newsletters',
18
19
  'oauth',
19
20
  'permissions',
20
21
  'permissions_roles',
@@ -39,6 +40,7 @@ const BACKUP_TABLES = [
39
40
  'members_paid_subscription_events',
40
41
  'members_subscribe_events',
41
42
  'members_product_events',
43
+ 'members_newsletters',
42
44
  'offers',
43
45
  'offer_redemptions'
44
46
  ];
@@ -65,6 +65,36 @@ function dropTables(names) {
65
65
  );
66
66
  }
67
67
 
68
+ /**
69
+ * Creates a migration which will drop an existing table and then re-add a new table based on provided spec
70
+ * @param {string} name - table name
71
+ * @param {Object} tableSpec - copy of table schema definition as defined in schema.js at the moment of writing the migration,
72
+ * this parameter MUST be present, otherwise @daniellockyer will hunt you down
73
+ *
74
+ * @returns {Object} migration object returning config/up/down properties
75
+ */
76
+ function recreateTable(name, tableSpec) {
77
+ return createNonTransactionalMigration(
78
+ async function up(connection) {
79
+ const exists = await connection.schema.hasTable(name);
80
+
81
+ if (!exists) {
82
+ logging.warn(`Failed to drop table: ${name} - table does not exist`);
83
+ } else {
84
+ logging.info(`Dropping table: ${name}`);
85
+ await commands.deleteTable(name, connection);
86
+ logging.info(`Re-adding table: ${name}`);
87
+ await commands.createTable(name, connection, tableSpec);
88
+ }
89
+ },
90
+ async function down() {
91
+ // noop: we cannot go back to old table schema
92
+ logging.warn(`Ignoring rollback for table recreate: ${name}`);
93
+ return Promise.resolve();
94
+ }
95
+ );
96
+ }
97
+
68
98
  /**
69
99
  * Creates a migration which will add a permission to the database
70
100
  *
@@ -178,7 +208,7 @@ function addPermissionToRole(config) {
178
208
  return;
179
209
  }
180
210
 
181
- logging.warn(`Adding permission(${config.permission}) to role(${config.role})`);
211
+ logging.info(`Adding permission(${config.permission}) to role(${config.role})`);
182
212
  await connection('permissions_roles').insert({
183
213
  id: ObjectId().toHexString(),
184
214
  permission_id: permission.id,
@@ -375,7 +405,8 @@ function createAddColumnMigration(table, column, columnDefinition) {
375
405
  column,
376
406
  dbIsInCorrectState: hasColumn => hasColumn === false,
377
407
  operation: commands.dropColumn,
378
- operationVerb: 'Removing'
408
+ operationVerb: 'Removing',
409
+ columnDefinition
379
410
  })
380
411
  );
381
412
  }
@@ -462,6 +493,7 @@ function addSetting({key, value, type, group}) {
462
493
  module.exports = {
463
494
  addTable,
464
495
  dropTables,
496
+ recreateTable,
465
497
  addPermission,
466
498
  addPermissionToRole,
467
499
  addPermissionWithRoles,
@@ -0,0 +1,25 @@
1
+ // ESLint Override Notice
2
+ // This file was named incorrectly and it didn't flag up in our eslint rules.
3
+ // The ESLint match-regex rule has now been updated to catch this, but this file has to be excluded.
4
+ /* eslint-disable ghost/filenames/match-regex */
5
+
6
+ const {addTable} = require('../../utils');
7
+
8
+ module.exports = addTable('newsletters', {
9
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
10
+ name: {type: 'string', maxlength: 191, nullable: false},
11
+ description: {type: 'string', maxlength: 2000, nullable: true},
12
+ sender_name: {type: 'string', maxlength: 191, nullable: false},
13
+ sender_email: {type: 'string', maxlength: 191, nullable: false, validations: {isEmail: true}},
14
+ sender_reply_to: {type: 'string', maxlength: 191, nullable: false, validations: {isEmail: true}},
15
+ default: {type: 'bool', nullable: false, defaultTo: false},
16
+ status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'active'},
17
+ recipient_filter: {
18
+ type: 'text',
19
+ maxlength: 1000000000,
20
+ nullable: false,
21
+ defaultTo: ''
22
+ },
23
+ subscribe_on_signup: {type: 'bool', nullable: false, defaultTo: false},
24
+ sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
25
+ });
@@ -0,0 +1,28 @@
1
+ const {
2
+ addPermissionWithRoles,
3
+ combineTransactionalMigrations
4
+ } = require('../../utils');
5
+
6
+ module.exports = combineTransactionalMigrations(
7
+ addPermissionWithRoles({
8
+ name: 'Browse newsletters',
9
+ action: 'browse',
10
+ object: 'newsletter'
11
+ }, [
12
+ 'Administrator'
13
+ ]),
14
+ addPermissionWithRoles({
15
+ name: 'Add newsletters',
16
+ action: 'add',
17
+ object: 'newsletter'
18
+ }, [
19
+ 'Administrator'
20
+ ]),
21
+ addPermissionWithRoles({
22
+ name: 'Edit newsletters',
23
+ action: 'edit',
24
+ object: 'newsletter'
25
+ }, [
26
+ 'Administrator'
27
+ ])
28
+ );
@@ -0,0 +1,29 @@
1
+ const {recreateTable} = require('../../utils');
2
+
3
+ module.exports = recreateTable('newsletters', {
4
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
5
+ name: {type: 'string', maxlength: 191, nullable: false, unique: true},
6
+ description: {type: 'string', maxlength: 2000, nullable: true},
7
+ slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
8
+ sender_name: {type: 'string', maxlength: 191, nullable: false},
9
+ sender_email: {type: 'string', maxlength: 191, nullable: true},
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'},
12
+ visibility: {
13
+ type: 'string',
14
+ maxlength: 50,
15
+ nullable: false,
16
+ defaultTo: 'members'
17
+ },
18
+ subscribe_on_signup: {type: 'bool', nullable: false, defaultTo: true},
19
+ sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0},
20
+ header_image: {type: 'string', maxlength: 2000, nullable: true},
21
+ show_header_icon: {type: 'bool', nullable: false, defaultTo: true},
22
+ show_header_title: {type: 'bool', nullable: false, defaultTo: true},
23
+ title_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif', validations: {isIn: ['serif', 'sans_serif']}},
24
+ title_alignment: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'center', validations: {isIn: ['center', 'left']}},
25
+ show_feature_image: {type: 'bool', nullable: false, defaultTo: true},
26
+ body_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif', validations: {isIn: ['serif', 'sans_serif']}},
27
+ footer_content: {type: 'text', maxlength: 1000000000, nullable: true},
28
+ show_badge: {type: 'bool', nullable: false, defaultTo: true}
29
+ });