ghost 5.20.0 → 5.21.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 (139) hide show
  1. package/components/{tryghost-adapter-manager-5.20.0.tgz → tryghost-adapter-manager-5.21.0.tgz} +0 -0
  2. package/components/{tryghost-api-framework-5.20.0.tgz → tryghost-api-framework-5.21.0.tgz} +0 -0
  3. package/components/{tryghost-api-version-compatibility-service-5.20.0.tgz → tryghost-api-version-compatibility-service-5.21.0.tgz} +0 -0
  4. package/components/tryghost-audience-feedback-5.21.0.tgz +0 -0
  5. package/components/{tryghost-bootstrap-socket-5.20.0.tgz → tryghost-bootstrap-socket-5.21.0.tgz} +0 -0
  6. package/components/{tryghost-constants-5.20.0.tgz → tryghost-constants-5.21.0.tgz} +0 -0
  7. package/components/tryghost-custom-theme-settings-service-5.21.0.tgz +0 -0
  8. package/components/tryghost-data-generator-5.21.0.tgz +0 -0
  9. package/components/{tryghost-domain-events-5.20.0.tgz → tryghost-domain-events-5.21.0.tgz} +0 -0
  10. package/components/tryghost-email-analytics-provider-mailgun-5.21.0.tgz +0 -0
  11. package/components/tryghost-email-analytics-service-5.21.0.tgz +0 -0
  12. package/components/{tryghost-email-content-generator-5.20.0.tgz → tryghost-email-content-generator-5.21.0.tgz} +0 -0
  13. package/components/{tryghost-express-dynamic-redirects-5.20.0.tgz → tryghost-express-dynamic-redirects-5.21.0.tgz} +0 -0
  14. package/components/tryghost-extract-api-key-5.21.0.tgz +0 -0
  15. package/components/{tryghost-html-to-plaintext-5.20.0.tgz → tryghost-html-to-plaintext-5.21.0.tgz} +0 -0
  16. package/components/{tryghost-job-manager-5.20.0.tgz → tryghost-job-manager-5.21.0.tgz} +0 -0
  17. package/components/tryghost-link-redirects-5.21.0.tgz +0 -0
  18. package/components/tryghost-link-replacer-5.21.0.tgz +0 -0
  19. package/components/tryghost-link-tracking-5.21.0.tgz +0 -0
  20. package/components/{tryghost-magic-link-5.20.0.tgz → tryghost-magic-link-5.21.0.tgz} +0 -0
  21. package/components/tryghost-mailgun-client-5.21.0.tgz +0 -0
  22. package/components/tryghost-member-analytics-service-5.21.0.tgz +0 -0
  23. package/components/tryghost-member-attribution-5.21.0.tgz +0 -0
  24. package/components/tryghost-member-events-5.21.0.tgz +0 -0
  25. package/components/{tryghost-members-analytics-ingress-5.20.0.tgz → tryghost-members-analytics-ingress-5.21.0.tgz} +0 -0
  26. package/components/tryghost-members-api-5.21.0.tgz +0 -0
  27. package/components/tryghost-members-csv-5.21.0.tgz +0 -0
  28. package/components/tryghost-members-events-service-5.21.0.tgz +0 -0
  29. package/components/tryghost-members-importer-5.21.0.tgz +0 -0
  30. package/components/{tryghost-members-offers-5.20.0.tgz → tryghost-members-offers-5.21.0.tgz} +0 -0
  31. package/components/tryghost-members-payments-5.21.0.tgz +0 -0
  32. package/components/tryghost-members-ssr-5.21.0.tgz +0 -0
  33. package/components/tryghost-members-stripe-service-5.21.0.tgz +0 -0
  34. package/components/tryghost-minifier-5.21.0.tgz +0 -0
  35. package/components/{tryghost-mw-api-version-mismatch-5.20.0.tgz → tryghost-mw-api-version-mismatch-5.21.0.tgz} +0 -0
  36. package/components/tryghost-mw-cache-control-5.21.0.tgz +0 -0
  37. package/components/tryghost-mw-error-handler-5.21.0.tgz +0 -0
  38. package/components/{tryghost-mw-session-from-token-5.20.0.tgz → tryghost-mw-session-from-token-5.21.0.tgz} +0 -0
  39. package/components/{tryghost-mw-update-user-last-seen-5.20.0.tgz → tryghost-mw-update-user-last-seen-5.21.0.tgz} +0 -0
  40. package/components/tryghost-mw-vhost-5.21.0.tgz +0 -0
  41. package/components/{tryghost-oembed-service-5.20.0.tgz → tryghost-oembed-service-5.21.0.tgz} +0 -0
  42. package/components/tryghost-package-json-5.21.0.tgz +0 -0
  43. package/components/tryghost-referrers-5.21.0.tgz +0 -0
  44. package/components/{tryghost-security-5.20.0.tgz → tryghost-security-5.21.0.tgz} +0 -0
  45. package/components/{tryghost-session-service-5.20.0.tgz → tryghost-session-service-5.21.0.tgz} +0 -0
  46. package/components/tryghost-settings-path-manager-5.21.0.tgz +0 -0
  47. package/components/{tryghost-staff-service-5.20.0.tgz → tryghost-staff-service-5.21.0.tgz} +0 -0
  48. package/components/{tryghost-stats-service-5.20.0.tgz → tryghost-stats-service-5.21.0.tgz} +0 -0
  49. package/components/tryghost-tiers-5.21.0.tgz +0 -0
  50. package/components/tryghost-update-check-service-5.21.0.tgz +0 -0
  51. package/components/{tryghost-verification-trigger-5.20.0.tgz → tryghost-verification-trigger-5.21.0.tgz} +0 -0
  52. package/components/tryghost-version-notifications-data-service-5.21.0.tgz +0 -0
  53. package/core/boot.js +2 -0
  54. package/core/built/admin/assets/{chunk.143.d245b085ad1efed4ee76.js → chunk.143.9cddfa7bd1a8b9cf3d4b.js} +5 -5
  55. package/core/built/admin/assets/{chunk.178.c45f56ea31775e509497.js → chunk.178.6de14cfdb28df721b66e.js} +4 -4
  56. package/core/built/admin/assets/{chunk.613.c4d89dc2d28c1b20348f.js → chunk.613.695f31829550fb00d43c.js} +351 -420
  57. package/core/built/admin/assets/{chunk.613.c4d89dc2d28c1b20348f.js.LICENSE.txt → chunk.613.695f31829550fb00d43c.js.LICENSE.txt} +0 -0
  58. package/core/built/admin/assets/{ghost-07e4bbf5029630b3c8a8a50c4b9f2d9e.js → ghost-192fee3b46a193df1e65c49a67a7d694.js} +308 -237
  59. package/core/built/admin/assets/ghost-9873519a8ad69b5b23284f0a9e050bc6.css +1 -0
  60. package/core/built/admin/assets/ghost-dark-190bdce42b125c3d4be930bd7599b442.css +1 -0
  61. package/core/built/admin/assets/{vendor-518b03b02df9a55706d150627ef1004f.js → vendor-26cca1d4d56660dc6e915a12ccc3b330.js} +1038 -1003
  62. package/core/built/admin/index.html +6 -6
  63. package/core/cli/generate-data.js +51 -0
  64. package/core/frontend/helpers/ghost_head.js +1 -1
  65. package/core/server/api/endpoints/links.js +2 -1
  66. package/core/server/api/endpoints/tiers-public.js +2 -14
  67. package/core/server/api/endpoints/tiers.js +5 -51
  68. package/core/server/api/endpoints/utils/serializers/input/posts.js +1 -1
  69. package/core/server/api/endpoints/utils/serializers/input/settings.js +1 -0
  70. package/core/server/api/endpoints/utils/serializers/input/tiers.js +18 -27
  71. package/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +42 -0
  72. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +5 -4
  73. package/core/server/api/endpoints/utils/serializers/output/tiers.js +15 -55
  74. package/core/server/data/db/backup.js +17 -10
  75. package/core/server/data/migrations/versions/5.21/2022-10-24-07-23-disable-feedback-enabled.js +20 -0
  76. package/core/server/data/migrations/versions/5.21/2022-10-25-12-05-backfill-missed-products-columns.js +35 -0
  77. package/core/server/data/migrations/versions/5.21/2022-10-26-04-49-add-batch-id-members-created-events.js +7 -0
  78. package/core/server/data/migrations/versions/5.21/2022-10-26-04-49-add-batch-id-subscription-created-events.js +7 -0
  79. package/core/server/data/migrations/versions/5.21/2022-10-26-04-50-member-subscription-created-batch-id.js +72 -0
  80. package/core/server/data/migrations/versions/5.21/2022-10-26-09-32-add-feedback-enabled-column-to-emails.js +7 -0
  81. package/core/server/data/migrations/versions/5.21/2022-10-27-09-50-add-member-track-source-setting.js +8 -0
  82. package/core/server/data/schema/default-settings/default-settings.json +8 -0
  83. package/core/server/data/schema/fixtures/fixture-manager.js +16 -14
  84. package/core/server/data/schema/schema.js +5 -2
  85. package/core/server/models/base/plugins/crud.js +12 -0
  86. package/core/server/models/email.js +1 -0
  87. package/core/server/models/member-click-event.js +13 -0
  88. package/core/server/models/member-created-event.js +23 -0
  89. package/core/server/models/member-paid-subscription-event.js +4 -0
  90. package/core/server/models/member.js +6 -0
  91. package/core/server/models/post.js +1 -1
  92. package/core/server/models/subscription-created-event.js +7 -0
  93. package/core/server/services/mega/feedback-buttons.js +87 -16
  94. package/core/server/services/mega/mega.js +1 -0
  95. package/core/server/services/mega/template.js +3 -0
  96. package/core/server/services/member-attribution/index.js +3 -1
  97. package/core/server/services/members/api.js +2 -0
  98. package/core/server/services/members/service.js +8 -1
  99. package/core/server/services/newsletters/index.js +3 -1
  100. package/core/server/services/newsletters/service.js +11 -1
  101. package/core/server/services/tiers/TierRepository.js +116 -0
  102. package/core/server/services/tiers/index.js +1 -0
  103. package/core/server/services/tiers/service.js +32 -0
  104. package/core/shared/config/defaults.json +1 -1
  105. package/core/shared/labs.js +6 -7
  106. package/ghost.js +1 -0
  107. package/package.json +115 -112
  108. package/yarn.lock +1170 -1097
  109. package/components/tryghost-audience-feedback-5.20.0.tgz +0 -0
  110. package/components/tryghost-custom-theme-settings-service-5.20.0.tgz +0 -0
  111. package/components/tryghost-email-analytics-provider-mailgun-5.20.0.tgz +0 -0
  112. package/components/tryghost-email-analytics-service-5.20.0.tgz +0 -0
  113. package/components/tryghost-extract-api-key-5.20.0.tgz +0 -0
  114. package/components/tryghost-link-redirects-5.20.0.tgz +0 -0
  115. package/components/tryghost-link-replacer-5.20.0.tgz +0 -0
  116. package/components/tryghost-link-tracking-5.20.0.tgz +0 -0
  117. package/components/tryghost-mailgun-client-5.20.0.tgz +0 -0
  118. package/components/tryghost-member-analytics-service-5.20.0.tgz +0 -0
  119. package/components/tryghost-member-attribution-5.20.0.tgz +0 -0
  120. package/components/tryghost-member-events-5.20.0.tgz +0 -0
  121. package/components/tryghost-members-api-5.20.0.tgz +0 -0
  122. package/components/tryghost-members-csv-5.20.0.tgz +0 -0
  123. package/components/tryghost-members-events-service-5.20.0.tgz +0 -0
  124. package/components/tryghost-members-importer-5.20.0.tgz +0 -0
  125. package/components/tryghost-members-payments-5.20.0.tgz +0 -0
  126. package/components/tryghost-members-ssr-5.20.0.tgz +0 -0
  127. package/components/tryghost-members-stripe-service-5.20.0.tgz +0 -0
  128. package/components/tryghost-minifier-5.20.0.tgz +0 -0
  129. package/components/tryghost-mw-cache-control-5.20.0.tgz +0 -0
  130. package/components/tryghost-mw-error-handler-5.20.0.tgz +0 -0
  131. package/components/tryghost-mw-vhost-5.20.0.tgz +0 -0
  132. package/components/tryghost-package-json-5.20.0.tgz +0 -0
  133. package/components/tryghost-referrers-5.20.0.tgz +0 -0
  134. package/components/tryghost-settings-path-manager-5.20.0.tgz +0 -0
  135. package/components/tryghost-tiers-5.20.0.tgz +0 -0
  136. package/components/tryghost-update-check-service-5.20.0.tgz +0 -0
  137. package/components/tryghost-version-notifications-data-service-5.20.0.tgz +0 -0
  138. package/core/built/admin/assets/ghost-dark-363185f15c782b4b8394c5db23984e7f.css +0 -1
  139. package/core/built/admin/assets/ghost-fd0480352bf27e013b2b00a1bf9ffe84.css +0 -1
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.20%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.21%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -37,7 +37,7 @@
37
37
  </style>
38
38
 
39
39
  <link integrity="" rel="stylesheet" href="assets/vendor-3e6947aa681f0fb82b193090e520dc73.css">
40
- <link integrity="" rel="stylesheet" href="assets/ghost-fd0480352bf27e013b2b00a1bf9ffe84.css" title="light">
40
+ <link integrity="" rel="stylesheet" href="assets/ghost-9873519a8ad69b5b23284f0a9e050bc6.css" title="light">
41
41
 
42
42
 
43
43
  </head>
@@ -56,9 +56,9 @@
56
56
 
57
57
  <div id="ember-basic-dropdown-wormhole"></div>
58
58
 
59
- <script src="assets/vendor-518b03b02df9a55706d150627ef1004f.js"></script>
60
- <script src="assets/chunk.613.c4d89dc2d28c1b20348f.js"></script>
61
- <script src="assets/chunk.143.d245b085ad1efed4ee76.js"></script>
62
- <script src="assets/ghost-07e4bbf5029630b3c8a8a50c4b9f2d9e.js"></script>
59
+ <script src="assets/vendor-26cca1d4d56660dc6e915a12ccc3b330.js"></script>
60
+ <script src="assets/chunk.613.695f31829550fb00d43c.js"></script>
61
+ <script src="assets/chunk.143.9cddfa7bd1a8b9cf3d4b.js"></script>
62
+ <script src="assets/ghost-192fee3b46a193df1e65c49a67a7d694.js"></script>
63
63
  </body>
64
64
  </html>
@@ -0,0 +1,51 @@
1
+ const Command = require('./command');
2
+ const DataGenerator = require('@tryghost/data-generator');
3
+
4
+ module.exports = class REPL extends Command {
5
+ setup() {
6
+ this.help('Generates random data to populate the database for development & testing');
7
+ this.argument('--use-base-data', {type: 'boolean', defaultValue: false, desc: 'Only generate data outside of a defined base data set'});
8
+ }
9
+
10
+ initializeContext(context) {
11
+ const models = require('../server/models');
12
+ const knex = require('../server/data/db/connection');
13
+
14
+ models.init();
15
+
16
+ context.models = models;
17
+ context.m = models;
18
+ context.knex = knex;
19
+ context.k = knex;
20
+ }
21
+
22
+ permittedEnvironments() {
23
+ return ['development', 'local', 'staging', 'production'];
24
+ }
25
+
26
+ async handle(argv = {}) {
27
+ const knex = require('../server/data/db/connection');
28
+ const {tables: schema} = require('../server/data/schema/index');
29
+ const dataGenerator = new DataGenerator({
30
+ useBaseData: argv['use-base-data'],
31
+ knex,
32
+ schema,
33
+ logger: {
34
+ log: this.log,
35
+ ok: this.ok,
36
+ info: this.info,
37
+ warn: this.warn,
38
+ error: this.error,
39
+ fatal: this.fatal,
40
+ debug: this.debug
41
+ },
42
+ modelQuantities: {}
43
+ });
44
+ try {
45
+ await dataGenerator.importData();
46
+ } catch (error) {
47
+ this.fatal('Failed while generating data: ', error);
48
+ }
49
+ knex.destroy();
50
+ }
51
+ };
@@ -234,7 +234,7 @@ module.exports = async function ghost_head(options) { // eslint-disable-line cam
234
234
  head.push(`<script defer src="${getAssetUrl('public/comment-counts.min.js')}" data-ghost-comments-counts-api="${urlUtils.getSiteUrl(true)}members/api/comments/counts/"></script>`);
235
235
  }
236
236
 
237
- if (settingsCache.get('members_enabled')) {
237
+ if (settingsCache.get('members_enabled') && settingsCache.get('members_track_sources')) {
238
238
  head.push(`<script defer src="${getAssetUrl('public/member-attribution.min.js')}"></script>`);
239
239
  }
240
240
 
@@ -1,4 +1,5 @@
1
1
  const linkTrackingService = require('../../services/link-tracking');
2
+ const INVALIDATE_ALL_REDIRECTS = '/r/*';
2
3
 
3
4
  module.exports = {
4
5
  docName: 'links',
@@ -25,7 +26,7 @@ module.exports = {
25
26
  bulkEdit: {
26
27
  statusCode: 200,
27
28
  headers: {
28
- cacheInvalidate: true
29
+ cacheInvalidate: INVALIDATE_ALL_REDIRECTS
29
30
  },
30
31
  options: [
31
32
  'filter'
@@ -1,8 +1,4 @@
1
- // NOTE: We must not cache references to membersService.api
2
- // as it is a getter and may change during runtime.
3
- const membersService = require('../../services/members');
4
-
5
- const allowedIncludes = ['monthly_price', 'yearly_price', 'benefits'];
1
+ const tiersService = require('../../services/tiers');
6
2
 
7
3
  module.exports = {
8
4
  docName: 'tiers',
@@ -11,22 +7,14 @@ module.exports = {
11
7
  options: [
12
8
  'limit',
13
9
  'fields',
14
- 'include',
15
10
  'filter',
16
11
  'order',
17
12
  'debug',
18
13
  'page'
19
14
  ],
20
15
  permissions: true,
21
- validation: {
22
- options: {
23
- include: {
24
- values: allowedIncludes
25
- }
26
- }
27
- },
28
16
  async query(frame) {
29
- const page = await membersService.api.productRepository.list(frame.options);
17
+ const page = await tiersService.api.browse(frame.options);
30
18
 
31
19
  return page;
32
20
  }
@@ -1,13 +1,4 @@
1
- const errors = require('@tryghost/errors');
2
- const membersService = require('../../services/members');
3
-
4
- const tpl = require('@tryghost/tpl');
5
-
6
- const allowedIncludes = ['monthly_price', 'yearly_price', 'benefits'];
7
-
8
- const messages = {
9
- productNotFound: 'Tier not found.'
10
- };
1
+ const tiersService = require('../../services/tiers');
11
2
 
12
3
  module.exports = {
13
4
  docName: 'tiers',
@@ -16,7 +7,6 @@ module.exports = {
16
7
  options: [
17
8
  'limit',
18
9
  'fields',
19
- 'include',
20
10
  'filter',
21
11
  'order',
22
12
  'debug',
@@ -25,48 +15,21 @@ module.exports = {
25
15
  permissions: {
26
16
  docName: 'products'
27
17
  },
28
- validation: {
29
- options: {
30
- include: {
31
- values: allowedIncludes
32
- }
33
- }
34
- },
35
18
  async query(frame) {
36
- const page = await membersService.api.productRepository.list(frame.options);
37
-
19
+ const page = await tiersService.api.browse(frame.options);
38
20
  return page;
39
21
  }
40
22
  },
41
23
 
42
24
  read: {
43
- options: [
44
- 'include'
45
- ],
46
- headers: {},
47
25
  data: [
48
26
  'id'
49
27
  ],
50
- validation: {
51
- options: {
52
- include: {
53
- values: allowedIncludes
54
- }
55
- }
56
- },
57
28
  permissions: {
58
29
  docName: 'products'
59
30
  },
60
31
  async query(frame) {
61
- const model = await membersService.api.productRepository.get(frame.data, frame.options);
62
-
63
- if (!model) {
64
- throw new errors.NotFoundError({
65
- message: tpl(messages.productNotFound)
66
- });
67
- }
68
-
69
- return model;
32
+ return await tiersService.api.read(frame.data.id);
70
33
  }
71
34
  },
72
35
 
@@ -84,11 +47,7 @@ module.exports = {
84
47
  docName: 'products'
85
48
  },
86
49
  async query(frame) {
87
- const model = await membersService.api.productRepository.create(
88
- frame.data,
89
- frame.options
90
- );
91
- return model;
50
+ return await tiersService.api.add(frame.data);
92
51
  }
93
52
  },
94
53
 
@@ -111,12 +70,7 @@ module.exports = {
111
70
  docName: 'products'
112
71
  },
113
72
  async query(frame) {
114
- const model = await membersService.api.productRepository.update(
115
- frame.data,
116
- frame.options
117
- );
118
-
119
- return model;
73
+ return await tiersService.api.edit(frame.options.id, frame.data);
120
74
  }
121
75
  }
122
76
  };
@@ -45,7 +45,7 @@ function defaultRelations(frame) {
45
45
  }
46
46
 
47
47
  if (labs.isSet('audienceFeedback')) {
48
- frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.conversions', 'count.clicks', 'count.sentiment', 'count.positive_feedback'];
48
+ frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.conversions', 'count.clicks', 'count.sentiment', 'count.positive_feedback', 'count.negative_feedback'];
49
49
  } else {
50
50
  frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.paid_conversions', 'count.clicks'];
51
51
  }
@@ -48,6 +48,7 @@ const EDITABLE_SETTINGS = [
48
48
  'mailgun_base_url',
49
49
  'email_track_opens',
50
50
  'email_track_clicks',
51
+ 'members_track_sources',
51
52
  'amp',
52
53
  'amp_gtag_id',
53
54
  'slack_url',
@@ -1,5 +1,4 @@
1
1
  const localUtils = require('../../index');
2
- const labs = require('../../../../../../shared/labs');
3
2
 
4
3
  const forceActiveFilter = (frame) => {
5
4
  if (frame.options.filter) {
@@ -10,39 +9,31 @@ const forceActiveFilter = (frame) => {
10
9
  };
11
10
 
12
11
  function convertTierInput(input) {
13
- const converted = {
14
- id: input.id,
15
- name: input.name,
16
- description: input.description,
17
- slug: input.slug,
18
- active: input.active,
19
- type: input.type,
20
- welcome_page_url: input.welcome_page_url,
21
- created_at: input.created_at,
22
- updated_at: input.updated_at,
23
- visibility: input.visibility
24
- };
12
+ const converted = Object.assign({}, input);
25
13
 
26
- if (labs.isSet('freeTrial')) {
27
- converted.trial_days = input.trial_days || 0;
14
+ if (Reflect.has(converted, 'active')) {
15
+ converted.status = converted.active ? 'active' : 'archived';
16
+ delete converted.active;
28
17
  }
29
18
 
30
- if (input.monthly_price && input.currency) {
31
- converted.monthly_price = {
32
- amount: input.monthly_price,
33
- currency: input.currency
34
- };
19
+ if (Reflect.has(converted, 'welcome_page_url')) {
20
+ converted.welcomePageURL = converted.welcome_page_url;
21
+ delete converted.welcome_page_url;
35
22
  }
36
23
 
37
- if (input.yearly_price && input.currency) {
38
- converted.yearly_price = {
39
- amount: input.yearly_price,
40
- currency: input.currency
41
- };
24
+ if (Reflect.has(converted, 'trial_days')) {
25
+ converted.trialDays = converted.trial_days;
26
+ delete converted.trial_days;
42
27
  }
43
28
 
44
- if (input.benefits) {
45
- converted.benefits = input.benefits.map(name => ({name}));
29
+ if (Reflect.has(converted, 'monthly_price')) {
30
+ converted.monthlyPrice = converted.monthly_price;
31
+ delete converted.monthly_price;
32
+ }
33
+
34
+ if (Reflect.has(converted, 'yearly_price')) {
35
+ converted.yearlyPrice = converted.yearly_price;
36
+ delete converted.yearly_price;
46
37
  }
47
38
 
48
39
  return converted;
@@ -53,6 +53,38 @@ const clickEventMapper = (json, frame) => {
53
53
  response.created_at = data.created_at;
54
54
  }
55
55
 
56
+ if (data.id) {
57
+ response.id = data.id;
58
+ }
59
+
60
+ return {
61
+ ...json,
62
+ data: response
63
+ };
64
+ };
65
+
66
+ const aggregatedClickEventMapper = (json) => {
67
+ const data = json.data;
68
+ const response = {};
69
+
70
+ if (data.member) {
71
+ response.member = _.pick(data.member, memberFields);
72
+ } else {
73
+ response.member = null;
74
+ }
75
+
76
+ if (data.created_at) {
77
+ response.created_at = data.created_at;
78
+ }
79
+
80
+ if (data.id) {
81
+ response.id = data.id;
82
+ }
83
+
84
+ response.count = {
85
+ clicks: data.count?.clicks ?? 0
86
+ };
87
+
56
88
  return {
57
89
  ...json,
58
90
  data: response
@@ -111,12 +143,22 @@ const activityFeedMapper = (event, frame) => {
111
143
  if (event.type === 'click_event') {
112
144
  return clickEventMapper(event, frame);
113
145
  }
146
+ if (event.type === 'aggregated_click_event') {
147
+ return aggregatedClickEventMapper(event, frame);
148
+ }
114
149
  if (event.type === 'feedback_event') {
115
150
  return feedbackEventMapper(event, frame);
116
151
  }
117
152
  if (event.data?.attribution) {
118
153
  event.data.attribution = serializeAttribution(event.data.attribution);
119
154
  }
155
+ // TODO: add dedicated mappers for other event types
156
+ if (event.data?.batch_id) {
157
+ delete event.data.batch_id;
158
+ }
159
+ if (event.data?.subscriptionCreatedEvent) {
160
+ delete event.data.subscriptionCreatedEvent;
161
+ }
120
162
  return event;
121
163
  };
122
164
 
@@ -111,10 +111,7 @@ module.exports = async (model, frame, options = {}) => {
111
111
 
112
112
  if (jsonModel.email && jsonModel.count) {
113
113
  jsonModel.email.opened_count = Math.min(
114
- Math.max(
115
- jsonModel.email.opened_count || 0,
116
- jsonModel.count.clicks || 0
117
- ),
114
+ jsonModel.email.opened_count || 0,
118
115
  jsonModel.email.email_count
119
116
  );
120
117
  }
@@ -140,5 +137,9 @@ module.exports = async (model, frame, options = {}) => {
140
137
  jsonModel.count.positive_feedback = 0;
141
138
  }
142
139
 
140
+ if (jsonModel.count && !jsonModel.count.negative_feedback) {
141
+ jsonModel.count.negative_feedback = 0;
142
+ }
143
+
143
144
  return jsonModel;
144
145
  };
@@ -1,11 +1,6 @@
1
1
  //@ts-check
2
2
  const debug = require('@tryghost/debug')('api:endpoints:utils:serializers:output:tiers');
3
3
 
4
- const allowedIncludes = ['monthly_price', 'yearly_price'];
5
- const localUtils = require('../../index');
6
- const {utils} = require('@tryghost/api-framework');
7
- const labs = require('../../../../../../shared/labs');
8
-
9
4
  module.exports = {
10
5
  browse: createSerializer('browse', paginatedTiers),
11
6
  read: createSerializer('read', singleTier),
@@ -49,11 +44,10 @@ function singleTier(model, _apiConfig, frame) {
49
44
  /**
50
45
  * @param {import('bookshelf').Model} tier
51
46
  * @param {object} options
52
- * @param {object} frame
53
47
  *
54
48
  * @returns {SerializedTier}
55
49
  */
56
- function serializeTier(tier, options, frame) {
50
+ function serializeTier(tier, options) {
57
51
  const json = tier.toJSON(options);
58
52
 
59
53
  const serialized = {
@@ -61,66 +55,32 @@ function serializeTier(tier, options, frame) {
61
55
  name: json.name,
62
56
  description: json.description,
63
57
  slug: json.slug,
64
- active: json.active,
58
+ active: json.status === 'active',
65
59
  type: json.type,
66
- welcome_page_url: json.welcome_page_url,
67
- created_at: json.created_at,
68
- updated_at: json.updated_at,
60
+ welcome_page_url: json.welcomePageURL,
61
+ created_at: json.createdAt,
62
+ updated_at: json.updatedAt,
69
63
  visibility: json.visibility,
70
- benefits: null
64
+ benefits: json.benefits,
65
+ currency: json.currency,
66
+ monthly_price: json.monthlyPrice,
67
+ yearly_price: json.yearlyPrice,
68
+ trial_days: json.trialDays
71
69
  };
72
70
 
73
- if (labs.isSet('freeTrial')) {
74
- serialized.trial_days = json.trial_days;
75
- }
76
-
77
- if (Array.isArray(json.benefits)) {
78
- serialized.benefits = json.benefits.map(benefit => benefit.name);
79
- } else {
71
+ if (!Array.isArray(serialized.benefits)) {
80
72
  serialized.benefits = null;
81
73
  }
82
74
 
83
- if (serialized.type === 'paid') {
84
- serialized.currency = json.monthlyPrice?.currency;
85
- serialized.monthly_price = json.monthlyPrice?.amount;
86
- serialized.yearly_price = json.yearlyPrice?.amount;
87
- }
88
-
89
- if (!localUtils.isContentAPI(frame)) {
90
- const requestedQueryIncludes = frame.original && frame.original.query && frame.original.query.include && frame.original.query.include.split(',') || [];
91
- const requestedOptionsIncludes = utils.options.trimAndLowerCase(frame.original && frame.original.options && frame.original.options.include || []);
92
-
93
- return cleanIncludes(
94
- allowedIncludes,
95
- requestedQueryIncludes.concat(requestedOptionsIncludes),
96
- serialized
97
- );
75
+ if (serialized.type === 'free') {
76
+ delete serialized.currency;
77
+ delete serialized.monthly_price;
78
+ delete serialized.yearly_price;
98
79
  }
99
80
 
100
81
  return serialized;
101
82
  }
102
83
 
103
- /**
104
- * @template Data
105
- *
106
- * @param {string[]} allowed
107
- * @param {string[]} requested
108
- * @param {Data & Object<string, any>} data
109
- *
110
- * @returns {Data}
111
- */
112
- function cleanIncludes(allowed, requested, data) {
113
- const cleaned = {
114
- ...data
115
- };
116
- for (const include of allowed) {
117
- if (!requested.includes(include)) {
118
- delete cleaned[include];
119
- }
120
- }
121
- return cleaned;
122
- }
123
-
124
84
  /**
125
85
  * @template Data
126
86
  * @template Response
@@ -9,12 +9,21 @@ const logging = require('@tryghost/logging');
9
9
  const urlUtils = require('../../../shared/url-utils');
10
10
  const exporter = require('../exporter');
11
11
 
12
- const writeExportFile = function writeExportFile(exportResult) {
12
+ /**
13
+ * @param {object} exportResult
14
+ * @param {string} exportResult.filename
15
+ * @param {object} exportResult.data
16
+ */
17
+ const writeExportFile = async (exportResult) => {
13
18
  const filename = path.resolve(urlUtils.urlJoin(config.get('paths').contentPath, 'data', exportResult.filename));
14
19
 
15
- return Promise.resolve(fs.writeFile(filename, JSON.stringify(exportResult.data))).return(filename);
20
+ await fs.writeFile(filename, JSON.stringify(exportResult.data));
21
+ return filename;
16
22
  };
17
23
 
24
+ /**
25
+ * @param {string} filename
26
+ */
18
27
  const readBackup = async (filename) => {
19
28
  const parsedFileName = path.parse(filename);
20
29
  const sanitized = `${parsedFileName.name}${parsedFileName.ext}`;
@@ -35,21 +44,19 @@ const readBackup = async (filename) => {
35
44
  * does an export, and stores this in a local file
36
45
  * @returns {Promise<*>}
37
46
  */
38
- const backup = function backup(options) {
47
+ const backup = async function backup(options = {}) {
39
48
  logging.info('Creating database backup');
40
- options = options || {};
41
49
 
42
50
  const props = {
43
51
  data: exporter.doExport(options),
44
52
  filename: exporter.fileName(options)
45
53
  };
46
54
 
47
- return Promise.props(props)
48
- .then(writeExportFile)
49
- .then(function successMessage(filename) {
50
- logging.info('Database backup written to: ' + filename);
51
- return filename;
52
- });
55
+ const exportResult = await Promise.props(props);
56
+ const filename = await writeExportFile(exportResult);
57
+
58
+ logging.info('Database backup written to: ' + filename);
59
+ return filename;
53
60
  };
54
61
 
55
62
  module.exports = {
@@ -0,0 +1,20 @@
1
+ const logging = require('@tryghost/logging');
2
+ const {createTransactionalMigration} = require('../../utils');
3
+
4
+ module.exports = createTransactionalMigration(
5
+ async function up(connection) {
6
+ const affectedRows = await connection('newsletters')
7
+ .update({
8
+ feedback_enabled: false
9
+ })
10
+ .where('feedback_enabled', true);
11
+
12
+ if (affectedRows > 0) {
13
+ // Only log if this site was affected by the issue.
14
+ logging.info(`Disabled feedback for ${affectedRows} newsletter(s)`);
15
+ }
16
+ },
17
+ async function down() {
18
+ // no-op: we don't need to change it back
19
+ }
20
+ );
@@ -0,0 +1,35 @@
1
+ const logging = require('@tryghost/logging');
2
+ const {createTransactionalMigration} = require('../../utils');
3
+
4
+ module.exports = createTransactionalMigration(
5
+ async function up(knex) {
6
+ logging.info(`Fixing currency/monthly_price/yearly_price values for default paid tiers`);
7
+
8
+ const currencyUpdated = await knex('products')
9
+ .update('currency', 'usd')
10
+ .where({
11
+ currency: null,
12
+ type: 'paid'
13
+ });
14
+ logging.info(`Updated ${currencyUpdated} tier(s) where currency=null, type=paid to currency=USD`);
15
+
16
+ const monthlyPriceUpdated = await knex('products')
17
+ .update('monthly_price', 500)
18
+ .where({
19
+ monthly_price: null,
20
+ type: 'paid'
21
+ });
22
+ logging.info(`Updated ${monthlyPriceUpdated} tier(s) where monthly_price=null, type=paid to monthly_price=500`);
23
+
24
+ const yearlyPriceUpdated = await knex('products')
25
+ .update('yearly_price', 5000)
26
+ .where({
27
+ yearly_price: null,
28
+ type: 'paid'
29
+ });
30
+ logging.info(`Updated ${yearlyPriceUpdated} tier(s) where yearly_price=null, type=paid to yearly_price=5000`);
31
+ },
32
+ async function down(/* knex */) {
33
+ // no-op: we don't want to revert to bad data
34
+ }
35
+ );
@@ -0,0 +1,7 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('members_created_events', 'batch_id', {
4
+ type: 'string',
5
+ maxlength: 24,
6
+ nullable: true
7
+ });
@@ -0,0 +1,7 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('members_subscription_created_events', 'batch_id', {
4
+ type: 'string',
5
+ maxlength: 24,
6
+ nullable: true
7
+ });