ghost 4.36.3 → 4.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/.c8rc.json +10 -0
  2. package/content/themes/casper/LICENSE +1 -1
  3. package/content/themes/casper/README.md +1 -1
  4. package/content/themes/casper/assets/built/global.css +1 -1
  5. package/content/themes/casper/assets/built/global.css.map +1 -1
  6. package/content/themes/casper/assets/built/screen.css +1 -1
  7. package/content/themes/casper/assets/built/screen.css.map +1 -1
  8. package/content/themes/casper/assets/css/global.css +14 -6
  9. package/content/themes/casper/assets/css/screen.css +9 -1
  10. package/content/themes/casper/package.json +2 -2
  11. package/content/themes/casper/partials/post-card.hbs +1 -1
  12. package/content/themes/casper/post.hbs +18 -19
  13. package/content/themes/casper/yarn.lock +186 -217
  14. package/core/built/assets/ghost-dark-9f760f16230b8bc52e188d6ce28516b0.css +1 -0
  15. package/core/built/assets/{ghost.min-801697772dc605c0dae0abfec54ec591.js → ghost.min-6386b02480494a69c3bfe66206754836.js} +375 -312
  16. package/core/built/assets/ghost.min-f4c59dd57a2136df8b0a34f87c099034.css +1 -0
  17. package/core/built/assets/icons/eye.svg +4 -1
  18. package/core/built/assets/icons/member-add.svg +3 -0
  19. package/core/built/assets/icons/pin.svg +4 -1
  20. package/core/built/assets/{vendor.min-2313642ee897688be83924a38d5e62f1.js → vendor.min-c814d3c4b3f543c4cd5ef3aacd0fc645.js} +40 -36
  21. package/core/frontend/helpers/excerpt.js +7 -4
  22. package/core/frontend/helpers/get.js +4 -0
  23. package/core/frontend/helpers/match.js +12 -0
  24. package/core/frontend/helpers/prev_post.js +11 -1
  25. package/core/frontend/helpers/tiers.js +59 -0
  26. package/core/frontend/helpers/tpl/content-cta.hbs +1 -1
  27. package/core/frontend/services/routing/router-manager.js +1 -1
  28. package/core/frontend/web/site.js +10 -0
  29. package/core/server/api/canary/authentication.js +2 -0
  30. package/core/server/api/canary/index.js +8 -0
  31. package/core/server/api/canary/members.js +2 -1
  32. package/core/server/api/canary/products.js +3 -6
  33. package/core/server/api/canary/tiers-public.js +34 -0
  34. package/core/server/api/canary/tiers.js +120 -0
  35. package/core/server/api/canary/utils/serializers/input/index.js +4 -0
  36. package/core/server/api/canary/utils/serializers/input/tiers.js +36 -0
  37. package/core/server/api/canary/utils/serializers/output/email-posts.js +7 -1
  38. package/core/server/api/canary/utils/serializers/output/index.js +4 -0
  39. package/core/server/api/canary/utils/serializers/output/members.js +5 -0
  40. package/core/server/api/canary/utils/serializers/output/pages.js +9 -2
  41. package/core/server/api/canary/utils/serializers/output/posts.js +8 -2
  42. package/core/server/api/canary/utils/serializers/output/preview.js +7 -1
  43. package/core/server/api/canary/utils/serializers/output/products.js +3 -1
  44. package/core/server/api/canary/utils/serializers/output/tiers.js +212 -0
  45. package/core/server/api/canary/utils/serializers/output/utils/mapper.js +17 -7
  46. package/core/server/api/canary/utils/validators/input/index.js +4 -0
  47. package/core/server/api/canary/utils/validators/input/tiers.js +6 -0
  48. package/core/server/api/v2/settings.js +2 -1
  49. package/core/server/data/db/connection.js +3 -2
  50. package/core/server/data/migrations/init/1-create-tables.js +4 -1
  51. package/core/server/data/migrations/versions/3.29/01-remove-duplicate-subscriptions.js +2 -1
  52. package/core/server/data/migrations/versions/3.29/02-remove-duplicate-customers.js +2 -1
  53. package/core/server/data/migrations/versions/3.29/03-remove-orphaned-customers.js +2 -1
  54. package/core/server/data/migrations/versions/3.29/04-remove-orphaned-subscriptions.js +2 -1
  55. package/core/server/data/migrations/versions/3.29/05-add-member-constraints.js +3 -2
  56. package/core/server/data/migrations/versions/3.39/06-add-email-recipient-index.js +4 -3
  57. package/core/server/data/migrations/versions/4.0/14-remove-orphaned-stripe-records.js +2 -1
  58. package/core/server/data/migrations/versions/4.0/26-add-cascade-on-delete.js +2 -1
  59. package/core/server/data/migrations/versions/4.0/29-fix-foreign-key-for-members-stripe-customers-subscriptions.js +2 -1
  60. package/core/server/data/migrations/versions/4.1/02-add-unique-constraint-for-member-stripe-tables.js +2 -1
  61. package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js +3 -2
  62. package/core/server/data/migrations/versions/4.33/2022-01-18-09-07-remove-duplicate-offer-redemptions.js +2 -2
  63. package/core/server/data/migrations/versions/4.35/2022-02-01-11-48-update-email-recipient-filter-column-type.js +2 -1
  64. package/core/server/data/migrations/versions/4.35/2022-02-01-12-03-update-recipient-filter-column-type.js +2 -1
  65. package/core/server/data/migrations/versions/4.37/2022-02-21-09-53-backfill-members-last-seen-at-column.js +32 -0
  66. package/core/server/data/migrations/versions/4.38/2022-03-01-08-46-add-visibility-to-tiers.js +11 -0
  67. package/core/server/data/migrations/versions/4.38/2022-03-03-16-12-add-visibility-to-tiers.js +8 -0
  68. package/core/server/data/migrations/versions/4.38/2022-03-03-16-17-drop-tiers-visible-column.js +7 -0
  69. package/core/server/data/schema/clients/index.js +1 -1
  70. package/core/server/data/schema/clients/mysql.js +4 -4
  71. package/core/server/data/schema/commands.js +61 -70
  72. package/core/server/data/schema/{default-settings.json → default-settings/default-settings.json} +0 -0
  73. package/core/server/data/schema/default-settings/index.js +6 -0
  74. package/core/server/data/schema/fixtures/fixtures.json +4 -2
  75. package/core/server/data/schema/schema.js +7 -0
  76. package/core/server/models/product.js +2 -1
  77. package/core/server/services/auth/api-key/admin.js +15 -6
  78. package/core/server/services/auth/setup.js +13 -1
  79. package/core/server/services/email-analytics/lib/event-processor.js +18 -1
  80. package/core/server/services/members/middleware.js +4 -1
  81. package/core/server/services/members/service.js +21 -8
  82. package/core/server/services/route-settings/index.js +1 -1
  83. package/core/server/web/admin/views/default-prod.html +4 -4
  84. package/core/server/web/admin/views/default.html +4 -4
  85. package/core/server/web/api/app.js +3 -0
  86. package/core/server/web/api/canary/admin/middleware.js +2 -0
  87. package/core/server/web/api/canary/admin/routes.js +7 -0
  88. package/core/server/web/api/canary/content/routes.js +1 -0
  89. package/core/server/web/members/app.js +1 -1
  90. package/core/server/web/shared/middleware/uncapitalise.js +2 -2
  91. package/core/shared/config/defaults.json +3 -2
  92. package/core/shared/config/overrides.json +1 -1
  93. package/core/shared/config/utils.js +5 -1
  94. package/core/shared/labs.js +4 -1
  95. package/package.json +62 -61
  96. package/yarn.lock +729 -614
  97. package/core/built/assets/ghost-dark-25e568b14d76f6754fa8279cceb265ba.css +0 -1
  98. package/core/built/assets/ghost.min-75ed7451ca633bae1b345eb57e2c28e0.css +0 -1
@@ -0,0 +1,34 @@
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'];
6
+
7
+ module.exports = {
8
+ docName: 'tiers',
9
+
10
+ browse: {
11
+ options: [
12
+ 'limit',
13
+ 'fields',
14
+ 'include',
15
+ 'filter',
16
+ 'order',
17
+ 'debug',
18
+ 'page'
19
+ ],
20
+ permissions: true,
21
+ validation: {
22
+ options: {
23
+ include: {
24
+ values: allowedIncludes
25
+ }
26
+ }
27
+ },
28
+ async query(frame) {
29
+ const page = await membersService.api.productRepository.list(frame.options);
30
+
31
+ return page;
32
+ }
33
+ }
34
+ };
@@ -0,0 +1,120 @@
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
+ };
11
+
12
+ module.exports = {
13
+ docName: 'tiers',
14
+
15
+ browse: {
16
+ options: [
17
+ 'limit',
18
+ 'fields',
19
+ 'include',
20
+ 'filter',
21
+ 'order',
22
+ 'debug',
23
+ 'page'
24
+ ],
25
+ permissions: {
26
+ docName: 'products'
27
+ },
28
+ validation: {
29
+ options: {
30
+ include: {
31
+ values: allowedIncludes
32
+ }
33
+ }
34
+ },
35
+ async query(frame) {
36
+ const page = await membersService.api.productRepository.list(frame.options);
37
+
38
+ return page;
39
+ }
40
+ },
41
+
42
+ read: {
43
+ options: [
44
+ 'include'
45
+ ],
46
+ headers: {},
47
+ data: [
48
+ 'id'
49
+ ],
50
+ validation: {
51
+ options: {
52
+ include: {
53
+ values: allowedIncludes
54
+ }
55
+ }
56
+ },
57
+ permissions: true,
58
+ async query(frame) {
59
+ const model = await membersService.api.productRepository.get(frame.data, frame.options);
60
+
61
+ if (!model) {
62
+ throw new errors.NotFoundError({
63
+ message: tpl(messages.productNotFound)
64
+ });
65
+ }
66
+
67
+ return model;
68
+ }
69
+ },
70
+
71
+ add: {
72
+ statusCode: 201,
73
+ headers: {
74
+ cacheInvalidate: true
75
+ },
76
+ validation: {
77
+ data: {
78
+ name: {required: true}
79
+ }
80
+ },
81
+ permissions: {
82
+ docName: 'products'
83
+ },
84
+ async query(frame) {
85
+ const model = await membersService.api.productRepository.create(
86
+ frame.data,
87
+ frame.options
88
+ );
89
+ return model;
90
+ }
91
+ },
92
+
93
+ edit: {
94
+ statusCode: 200,
95
+ options: [
96
+ 'id'
97
+ ],
98
+ headers: {
99
+ cacheInvalidate: true
100
+ },
101
+ validation: {
102
+ options: {
103
+ id: {
104
+ required: true
105
+ }
106
+ }
107
+ },
108
+ permissions: {
109
+ docName: 'products'
110
+ },
111
+ async query(frame) {
112
+ const model = await membersService.api.productRepository.update(
113
+ frame.data,
114
+ frame.options
115
+ );
116
+
117
+ return model;
118
+ }
119
+ }
120
+ };
@@ -43,6 +43,10 @@ module.exports = {
43
43
  return require('./products');
44
44
  },
45
45
 
46
+ get tiers() {
47
+ return require('./tiers');
48
+ },
49
+
46
50
  get webhooks() {
47
51
  return require('./webhooks');
48
52
  }
@@ -0,0 +1,36 @@
1
+ module.exports = {
2
+ all(_apiConfig, frame) {
3
+ if (!frame.options.withRelated) {
4
+ return;
5
+ }
6
+
7
+ frame.options.withRelated = frame.options.withRelated.map((relation) => {
8
+ if (relation === 'stripe_prices') {
9
+ return 'stripePrices';
10
+ }
11
+ if (relation === 'monthly_price') {
12
+ return 'monthlyPrice';
13
+ }
14
+ if (relation === 'yearly_price') {
15
+ return 'yearlyPrice';
16
+ }
17
+ return relation;
18
+ });
19
+ },
20
+
21
+ add(_apiConfig, frame) {
22
+ if (frame.data.products) {
23
+ frame.data = frame.data.products[0];
24
+ return;
25
+ }
26
+ frame.data = frame.data.tiers[0];
27
+ },
28
+
29
+ edit(_apiConfig, frame) {
30
+ if (frame.data.products) {
31
+ frame.data = frame.data.products[0];
32
+ return;
33
+ }
34
+ frame.data = frame.data.tiers[0];
35
+ }
36
+ };
@@ -1,9 +1,15 @@
1
1
  const mapper = require('./utils/mapper');
2
2
  const gating = require('./utils/post-gating');
3
+ const membersService = require('../../../../../services/members');
3
4
 
4
5
  module.exports = {
5
6
  async read(model, apiConfig, frame) {
6
- const emailPost = await mapper.mapPost(model, frame);
7
+ const tiersModels = await membersService.api.productRepository.list({
8
+ withRelated: ['monthlyPrice', 'yearlyPrice']
9
+ });
10
+ const tiers = tiersModels.data && tiersModels.data.map(tierModel => tierModel.toJSON());
11
+
12
+ const emailPost = await mapper.mapPost(model, frame, {tiers});
7
13
  gating.forPost(emailPost, frame);
8
14
 
9
15
  frame.response = {
@@ -73,6 +73,10 @@ module.exports = {
73
73
  return require('./products');
74
74
  },
75
75
 
76
+ get tiers() {
77
+ return require('./tiers');
78
+ },
79
+
76
80
  get member_signin_urls() {
77
81
  return require('./member-signin_urls');
78
82
  },
@@ -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 labs = require('../../../../../../shared/labs');
4
5
 
5
6
  module.exports = {
6
7
  hasActiveStripeSubscriptions: createSerializer('hasActiveStripeSubscriptions', passthrough),
@@ -127,6 +128,10 @@ function serializeMember(member, options) {
127
128
  status: json.status
128
129
  };
129
130
 
131
+ if (labs.isSet('membersLastSeenFilter')) {
132
+ serialized.last_seen_at = json.last_seen_at;
133
+ }
134
+
130
135
  if (json.products) {
131
136
  serialized.products = json.products;
132
137
  }
@@ -1,5 +1,6 @@
1
1
  const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:pages');
2
2
  const mapper = require('./utils/mapper');
3
+ const membersService = require('../../../../../services/members');
3
4
 
4
5
  module.exports = {
5
6
  async all(models, apiConfig, frame) {
@@ -10,9 +11,15 @@ module.exports = {
10
11
  return;
11
12
  }
12
13
  let pages = [];
14
+
15
+ const tiersModels = await membersService.api.productRepository.list({
16
+ withRelated: ['monthlyPrice', 'yearlyPrice']
17
+ });
18
+ const tiers = tiersModels.data ? tiersModels.data.map(tierModel => tierModel.toJSON()) : [];
19
+
13
20
  if (models.meta) {
14
21
  for (let model of models.data) {
15
- let page = await mapper.mapPage(model, frame);
22
+ let page = await mapper.mapPage(model, frame, {tiers});
16
23
  pages.push(page);
17
24
  }
18
25
  frame.response = {
@@ -22,7 +29,7 @@ module.exports = {
22
29
 
23
30
  return;
24
31
  }
25
- let page = await mapper.mapPage(models, frame);
32
+ let page = await mapper.mapPage(models, frame, {tiers});
26
33
  frame.response = {
27
34
  pages: [page]
28
35
  };
@@ -1,5 +1,6 @@
1
1
  const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:posts');
2
2
  const mapper = require('./utils/mapper');
3
+ const membersService = require('../../../../../services/members');
3
4
 
4
5
  module.exports = {
5
6
  async all(models, apiConfig, frame) {
@@ -10,9 +11,14 @@ module.exports = {
10
11
  return;
11
12
  }
12
13
  let posts = [];
14
+
15
+ const tiersModels = await membersService.api.productRepository.list({
16
+ withRelated: ['monthlyPrice', 'yearlyPrice']
17
+ });
18
+ const tiers = tiersModels.data ? tiersModels.data.map(tierModel => tierModel.toJSON()) : [];
13
19
  if (models.meta) {
14
20
  for (let model of models.data) {
15
- let post = await mapper.mapPost(model, frame);
21
+ let post = await mapper.mapPost(model, frame, {tiers});
16
22
  posts.push(post);
17
23
  }
18
24
  frame.response = {
@@ -22,7 +28,7 @@ module.exports = {
22
28
 
23
29
  return;
24
30
  }
25
- let post = await mapper.mapPost(models, frame);
31
+ let post = await mapper.mapPost(models, frame, {tiers});
26
32
  frame.response = {
27
33
  posts: [post]
28
34
  };
@@ -1,8 +1,14 @@
1
1
  const mapper = require('./utils/mapper');
2
+ const membersService = require('../../../../../services/members');
2
3
 
3
4
  module.exports = {
4
5
  async all(model, apiConfig, frame) {
5
- const data = await mapper.mapPost(model, frame);
6
+ const tiersModels = await membersService.api.productRepository.list({
7
+ withRelated: ['monthlyPrice', 'yearlyPrice']
8
+ });
9
+ const tiers = tiersModels.data ? tiersModels.data.map(tierModel => tierModel.toJSON()) : [];
10
+
11
+ const data = await mapper.mapPost(model, frame, {tiers});
6
12
  frame.response = {
7
13
  preview: [data]
8
14
  };
@@ -1,6 +1,7 @@
1
1
  //@ts-check
2
2
  const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:products');
3
3
  const _ = require('lodash');
4
+ const utils = require('../../../../shared/utils');
4
5
 
5
6
  const allowedIncludes = ['stripe_prices', 'monthly_price', 'yearly_price'];
6
7
 
@@ -22,7 +23,7 @@ module.exports = {
22
23
  */
23
24
  function paginatedProducts(page, _apiConfig, frame) {
24
25
  const requestedQueryIncludes = frame.original && frame.original.query && frame.original.query.include && frame.original.query.include.split(',') || [];
25
- const requestedOptionsIncludes = frame.original && frame.original.options && frame.original.options.include || [];
26
+ const requestedOptionsIncludes = utils.options.trimAndLowerCase(frame.original && frame.original.options && frame.original.options.include || []);
26
27
  return {
27
28
  products: page.data.map((model) => {
28
29
  return cleanIncludes(
@@ -74,6 +75,7 @@ function serializeProduct(product, options, apiType) {
74
75
  description: json.description,
75
76
  slug: json.slug,
76
77
  active: json.active,
78
+ visibility: json.visibility,
77
79
  type: json.type,
78
80
  welcome_page_url: json.welcome_page_url,
79
81
  created_at: json.created_at,
@@ -0,0 +1,212 @@
1
+ //@ts-check
2
+ const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:tiers');
3
+ const _ = require('lodash');
4
+
5
+ const allowedIncludes = ['monthly_price', 'yearly_price'];
6
+ const utils = require('../../../../shared/utils');
7
+
8
+ module.exports = {
9
+ browse: createSerializer('browse', paginatedTiers),
10
+ read: createSerializer('read', singleTier),
11
+ edit: createSerializer('edit', singleTier),
12
+ add: createSerializer('add', singleTier)
13
+ };
14
+
15
+ /**
16
+ * @template PageMeta
17
+ *
18
+ * @param {{data: import('bookshelf').Model[], meta: PageMeta}} page
19
+ * @param {APIConfig} _apiConfig
20
+ * @param {Frame} frame
21
+ *
22
+ * @returns {{tiers: SerializedTier[], meta: PageMeta}}
23
+ */
24
+ function paginatedTiers(page, _apiConfig, frame) {
25
+ const requestedQueryIncludes = frame.original && frame.original.query && frame.original.query.include && frame.original.query.include.split(',') || [];
26
+ const requestedOptionsIncludes = utils.options.trimAndLowerCase(frame.original && frame.original.options && frame.original.options.include || []);
27
+ return {
28
+ tiers: page.data.map((model) => {
29
+ return cleanIncludes(
30
+ allowedIncludes,
31
+ requestedQueryIncludes.concat(requestedOptionsIncludes),
32
+ serializeTier(model, frame.options, frame.apiType)
33
+ );
34
+ }),
35
+ meta: page.meta
36
+ };
37
+ }
38
+
39
+ /**
40
+ * @param {import('bookshelf').Model} model
41
+ * @param {APIConfig} _apiConfig
42
+ * @param {Frame} frame
43
+ *
44
+ * @returns {{tiers: SerializedTier[]}}
45
+ */
46
+ function singleTier(model, _apiConfig, frame) {
47
+ const requestedQueryIncludes = frame.original && frame.original.query && frame.original.query.include && frame.original.query.include.split(',') || [];
48
+ const requestedOptionsIncludes = frame.original && frame.original.options && frame.original.options.include || [];
49
+ return {
50
+ tiers: [
51
+ cleanIncludes(
52
+ allowedIncludes,
53
+ requestedQueryIncludes.concat(requestedOptionsIncludes),
54
+ serializeTier(model, frame.options, frame.apiType)
55
+ )
56
+ ]
57
+ };
58
+ }
59
+
60
+ /**
61
+ * @param {import('bookshelf').Model} tier
62
+ * @param {object} options
63
+ * @param {'content'|'admin'} apiType
64
+ *
65
+ * @returns {SerializedTier}
66
+ */
67
+ function serializeTier(tier, options, apiType) {
68
+ const json = tier.toJSON(options);
69
+
70
+ const hideStripeData = apiType === 'content';
71
+
72
+ const serialized = {
73
+ id: json.id,
74
+ name: json.name,
75
+ description: json.description,
76
+ slug: json.slug,
77
+ active: json.active,
78
+ type: json.type,
79
+ welcome_page_url: json.welcome_page_url,
80
+ created_at: json.created_at,
81
+ updated_at: json.updated_at,
82
+ stripe_prices: json.stripePrices ? json.stripePrices.map(price => serializeStripePrice(price, hideStripeData)) : null,
83
+ monthly_price: serializeStripePrice(json.monthlyPrice, hideStripeData),
84
+ yearly_price: serializeStripePrice(json.yearlyPrice, hideStripeData),
85
+ benefits: json.benefits || null,
86
+ visibility: json.visibility
87
+ };
88
+
89
+ return serialized;
90
+ }
91
+
92
+ /**
93
+ * @param {object} data
94
+ * @param {boolean} hideStripeData
95
+ *
96
+ * @returns {StripePrice}
97
+ */
98
+ function serializeStripePrice(data, hideStripeData) {
99
+ if (_.isEmpty(data)) {
100
+ return null;
101
+ }
102
+ const price = {
103
+ id: data.id,
104
+ stripe_tier_id: data.stripe_product_id,
105
+ stripe_price_id: data.stripe_price_id,
106
+ active: data.active,
107
+ nickname: data.nickname,
108
+ description: data.description,
109
+ currency: data.currency,
110
+ amount: data.amount,
111
+ type: data.type,
112
+ interval: data.interval,
113
+ created_at: data.created_at,
114
+ updated_at: data.updated_at
115
+ };
116
+
117
+ if (hideStripeData) {
118
+ delete price.stripe_price_id;
119
+ delete price.stripe_tier_id;
120
+ }
121
+
122
+ return price;
123
+ }
124
+
125
+ /**
126
+ * @template Data
127
+ *
128
+ * @param {string[]} allowed
129
+ * @param {string[]} requested
130
+ * @param {Data & Object<string, any>} data
131
+ *
132
+ * @returns {Data}
133
+ */
134
+ function cleanIncludes(allowed, requested, data) {
135
+ const cleaned = {
136
+ ...data
137
+ };
138
+ for (const include of allowed) {
139
+ if (!requested.includes(include)) {
140
+ delete cleaned[include];
141
+ }
142
+ }
143
+ return cleaned;
144
+ }
145
+
146
+ /**
147
+ * @template Data
148
+ * @template Response
149
+ * @param {string} debugString
150
+ * @param {(data: Data, apiConfig: APIConfig, frame: Frame) => Response} serialize - A function to serialize the data into an object suitable for API response
151
+ *
152
+ * @returns {(data: Data, apiConfig: APIConfig, frame: Frame) => void}
153
+ */
154
+ function createSerializer(debugString, serialize) {
155
+ return function serializer(data, apiConfig, frame) {
156
+ debug(debugString);
157
+ const response = serialize(data, apiConfig, frame);
158
+ frame.response = response;
159
+ };
160
+ }
161
+
162
+ /**
163
+ * @typedef {Object} SerializedTier
164
+ * @prop {string} id
165
+ * @prop {string} name
166
+ * @prop {string} slug
167
+ * @prop {string} description
168
+ * @prop {boolean} active
169
+ * @prop {string} type
170
+ * @prop {string} welcome_page_url
171
+ * @prop {Date} created_at
172
+ * @prop {Date} updated_at
173
+ * @prop {StripePrice} [monthly_price]
174
+ * @prop {StripePrice} [yearly_price]
175
+ * @prop {Benefit[]} [benefits]
176
+ */
177
+
178
+ /**
179
+ * @typedef {object} Benefit
180
+ * @prop {string} id
181
+ * @prop {string} name
182
+ * @prop {string} slug
183
+ * @prop {Date} created_at
184
+ * @prop {Date} updated_at
185
+ */
186
+
187
+ /**
188
+ * @typedef {object} StripePrice
189
+ * @prop {string} id
190
+ * @prop {string|null} stripe_tier_id
191
+ * @prop {string|null} stripe_price_id
192
+ * @prop {boolean} active
193
+ * @prop {string} nickname
194
+ * @prop {string} description
195
+ * @prop {string} currency
196
+ * @prop {number} amount
197
+ * @prop {'recurring'|'one-time'} type
198
+ * @prop {'day'|'week'|'month'|'year'} interval
199
+ * @prop {Date} created_at
200
+ * @prop {Date} updated_at
201
+ */
202
+
203
+ /**
204
+ * @typedef {Object} APIConfig
205
+ * @prop {string} docName
206
+ * @prop {string} method
207
+ */
208
+
209
+ /**
210
+ * @typedef {Object<string, any>} Frame
211
+ * @prop {Object} options
212
+ */
@@ -31,7 +31,8 @@ const mapTag = (model, frame) => {
31
31
  return jsonModel;
32
32
  };
33
33
 
34
- const mapPost = async (model, frame) => {
34
+ const mapPost = async (model, frame, options = {}) => {
35
+ const {tiers: tiersData} = options || {};
35
36
  const extendedOptions = Object.assign(_.cloneDeep(frame.options), {
36
37
  extraProperties: ['canonical_url']
37
38
  });
@@ -45,12 +46,21 @@ const mapPost = async (model, frame) => {
45
46
  // Attach tiers to custom nql visibility filter
46
47
  if (labsService.isSet('multipleProducts')
47
48
  && jsonModel.visibility
48
- && !['members', 'public', 'paid', 'tiers'].includes(jsonModel.visibility)
49
49
  ) {
50
- const tiers = await postsService.getProductsFromVisibilityFilter(jsonModel.visibility);
50
+ if (['members', 'public'].includes(jsonModel.visibility) && jsonModel.tiers) {
51
+ jsonModel.tiers = tiersData || [];
52
+ }
53
+
54
+ if (jsonModel.visibility === 'paid' && jsonModel.tiers) {
55
+ jsonModel.tiers = tiersData ? tiersData.filter(t => t.type === 'paid') : [];
56
+ }
51
57
 
52
- jsonModel.visibility = 'tiers';
53
- jsonModel.tiers = tiers;
58
+ if (!['members', 'public', 'paid', 'tiers'].includes(jsonModel.visibility)) {
59
+ const tiers = await postsService.getProductsFromVisibilityFilter(jsonModel.visibility);
60
+
61
+ jsonModel.visibility = 'tiers';
62
+ jsonModel.tiers = tiers;
63
+ }
54
64
  }
55
65
 
56
66
  if (utils.isContentAPI(frame)) {
@@ -103,8 +113,8 @@ const mapPost = async (model, frame) => {
103
113
  return jsonModel;
104
114
  };
105
115
 
106
- const mapPage = async (model, frame) => {
107
- const jsonModel = await mapPost(model, frame);
116
+ const mapPage = async (model, frame, options) => {
117
+ const jsonModel = await mapPost(model, frame, options);
108
118
 
109
119
  delete jsonModel.email_subject;
110
120
  delete jsonModel.email_recipient_filter;
@@ -27,6 +27,10 @@ module.exports = {
27
27
  return require('./members');
28
28
  },
29
29
 
30
+ get tiers() {
31
+ return require('./tiers');
32
+ },
33
+
30
34
  get media() {
31
35
  return require('./media');
32
36
  },
@@ -0,0 +1,6 @@
1
+ const jsonSchema = require('../utils/json-schema');
2
+
3
+ module.exports = {
4
+ add: jsonSchema.validate,
5
+ edit: jsonSchema.validate
6
+ };
@@ -106,7 +106,8 @@ module.exports = {
106
106
  before(frame) {
107
107
  const errors = [];
108
108
 
109
- frame.data.settings.map((setting) => {
109
+ // Using eslint disable line here as we are about to drop v2 - no point in fixing
110
+ frame.data.settings.map((setting) => { /* eslint-disable-line array-callback-return */
110
111
  if (setting.group === 'core' && !(frame.options.context && frame.options.context.internal)) {
111
112
  errors.push(new NoPermissionError({
112
113
  message: tpl(messages.accessCoreSettingFromExtReq)
@@ -32,9 +32,10 @@ function configure(dbConfig) {
32
32
  process.env.BTHREADS_BACKEND = 'child_process';
33
33
  }
34
34
 
35
- if (client === 'mysql') {
36
- dbConfig.connection.timezone = 'UTC';
35
+ if (client === 'mysql2') {
36
+ dbConfig.connection.timezone = 'Z';
37
37
  dbConfig.connection.charset = 'utf8mb4';
38
+ dbConfig.connection.decimalNumbers = true;
38
39
 
39
40
  // NOTE: disabled so that worker processes can use the db without
40
41
  // requiring logging and causing file desriptor leaks.
@@ -7,7 +7,10 @@ const schemaTables = Object.keys(schema);
7
7
  module.exports.up = async (options) => {
8
8
  const connection = options.connection;
9
9
 
10
- await Promise.mapSeries(schemaTables, async (table) => {
10
+ const existingTables = await commands.getTables(connection);
11
+ const missingTables = schemaTables.filter(t => !existingTables.includes(t));
12
+
13
+ await Promise.mapSeries(missingTables, async (table) => {
11
14
  logging.info('Creating table: ' + table);
12
15
  await commands.createTable(table, connection);
13
16
  });