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.
- package/.c8rc.json +10 -0
- package/content/themes/casper/LICENSE +1 -1
- package/content/themes/casper/README.md +1 -1
- package/content/themes/casper/assets/built/global.css +1 -1
- package/content/themes/casper/assets/built/global.css.map +1 -1
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/global.css +14 -6
- package/content/themes/casper/assets/css/screen.css +9 -1
- package/content/themes/casper/package.json +2 -2
- package/content/themes/casper/partials/post-card.hbs +1 -1
- package/content/themes/casper/post.hbs +18 -19
- package/content/themes/casper/yarn.lock +186 -217
- package/core/built/assets/ghost-dark-9f760f16230b8bc52e188d6ce28516b0.css +1 -0
- package/core/built/assets/{ghost.min-801697772dc605c0dae0abfec54ec591.js → ghost.min-6386b02480494a69c3bfe66206754836.js} +375 -312
- package/core/built/assets/ghost.min-f4c59dd57a2136df8b0a34f87c099034.css +1 -0
- package/core/built/assets/icons/eye.svg +4 -1
- package/core/built/assets/icons/member-add.svg +3 -0
- package/core/built/assets/icons/pin.svg +4 -1
- package/core/built/assets/{vendor.min-2313642ee897688be83924a38d5e62f1.js → vendor.min-c814d3c4b3f543c4cd5ef3aacd0fc645.js} +40 -36
- package/core/frontend/helpers/excerpt.js +7 -4
- package/core/frontend/helpers/get.js +4 -0
- package/core/frontend/helpers/match.js +12 -0
- package/core/frontend/helpers/prev_post.js +11 -1
- package/core/frontend/helpers/tiers.js +59 -0
- package/core/frontend/helpers/tpl/content-cta.hbs +1 -1
- package/core/frontend/services/routing/router-manager.js +1 -1
- package/core/frontend/web/site.js +10 -0
- package/core/server/api/canary/authentication.js +2 -0
- package/core/server/api/canary/index.js +8 -0
- package/core/server/api/canary/members.js +2 -1
- package/core/server/api/canary/products.js +3 -6
- package/core/server/api/canary/tiers-public.js +34 -0
- package/core/server/api/canary/tiers.js +120 -0
- package/core/server/api/canary/utils/serializers/input/index.js +4 -0
- package/core/server/api/canary/utils/serializers/input/tiers.js +36 -0
- package/core/server/api/canary/utils/serializers/output/email-posts.js +7 -1
- package/core/server/api/canary/utils/serializers/output/index.js +4 -0
- package/core/server/api/canary/utils/serializers/output/members.js +5 -0
- package/core/server/api/canary/utils/serializers/output/pages.js +9 -2
- package/core/server/api/canary/utils/serializers/output/posts.js +8 -2
- package/core/server/api/canary/utils/serializers/output/preview.js +7 -1
- package/core/server/api/canary/utils/serializers/output/products.js +3 -1
- package/core/server/api/canary/utils/serializers/output/tiers.js +212 -0
- package/core/server/api/canary/utils/serializers/output/utils/mapper.js +17 -7
- package/core/server/api/canary/utils/validators/input/index.js +4 -0
- package/core/server/api/canary/utils/validators/input/tiers.js +6 -0
- package/core/server/api/v2/settings.js +2 -1
- package/core/server/data/db/connection.js +3 -2
- package/core/server/data/migrations/init/1-create-tables.js +4 -1
- package/core/server/data/migrations/versions/3.29/01-remove-duplicate-subscriptions.js +2 -1
- package/core/server/data/migrations/versions/3.29/02-remove-duplicate-customers.js +2 -1
- package/core/server/data/migrations/versions/3.29/03-remove-orphaned-customers.js +2 -1
- package/core/server/data/migrations/versions/3.29/04-remove-orphaned-subscriptions.js +2 -1
- package/core/server/data/migrations/versions/3.29/05-add-member-constraints.js +3 -2
- package/core/server/data/migrations/versions/3.39/06-add-email-recipient-index.js +4 -3
- package/core/server/data/migrations/versions/4.0/14-remove-orphaned-stripe-records.js +2 -1
- package/core/server/data/migrations/versions/4.0/26-add-cascade-on-delete.js +2 -1
- package/core/server/data/migrations/versions/4.0/29-fix-foreign-key-for-members-stripe-customers-subscriptions.js +2 -1
- package/core/server/data/migrations/versions/4.1/02-add-unique-constraint-for-member-stripe-tables.js +2 -1
- package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js +3 -2
- package/core/server/data/migrations/versions/4.33/2022-01-18-09-07-remove-duplicate-offer-redemptions.js +2 -2
- package/core/server/data/migrations/versions/4.35/2022-02-01-11-48-update-email-recipient-filter-column-type.js +2 -1
- package/core/server/data/migrations/versions/4.35/2022-02-01-12-03-update-recipient-filter-column-type.js +2 -1
- package/core/server/data/migrations/versions/4.37/2022-02-21-09-53-backfill-members-last-seen-at-column.js +32 -0
- package/core/server/data/migrations/versions/4.38/2022-03-01-08-46-add-visibility-to-tiers.js +11 -0
- package/core/server/data/migrations/versions/4.38/2022-03-03-16-12-add-visibility-to-tiers.js +8 -0
- package/core/server/data/migrations/versions/4.38/2022-03-03-16-17-drop-tiers-visible-column.js +7 -0
- package/core/server/data/schema/clients/index.js +1 -1
- package/core/server/data/schema/clients/mysql.js +4 -4
- package/core/server/data/schema/commands.js +61 -70
- package/core/server/data/schema/{default-settings.json → default-settings/default-settings.json} +0 -0
- package/core/server/data/schema/default-settings/index.js +6 -0
- package/core/server/data/schema/fixtures/fixtures.json +4 -2
- package/core/server/data/schema/schema.js +7 -0
- package/core/server/models/product.js +2 -1
- package/core/server/services/auth/api-key/admin.js +15 -6
- package/core/server/services/auth/setup.js +13 -1
- package/core/server/services/email-analytics/lib/event-processor.js +18 -1
- package/core/server/services/members/middleware.js +4 -1
- package/core/server/services/members/service.js +21 -8
- package/core/server/services/route-settings/index.js +1 -1
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/app.js +3 -0
- package/core/server/web/api/canary/admin/middleware.js +2 -0
- package/core/server/web/api/canary/admin/routes.js +7 -0
- package/core/server/web/api/canary/content/routes.js +1 -0
- package/core/server/web/members/app.js +1 -1
- package/core/server/web/shared/middleware/uncapitalise.js +2 -2
- package/core/shared/config/defaults.json +3 -2
- package/core/shared/config/overrides.json +1 -1
- package/core/shared/config/utils.js +5 -1
- package/core/shared/labs.js +4 -1
- package/package.json +62 -61
- package/yarn.lock +729 -614
- package/core/built/assets/ghost-dark-25e568b14d76f6754fa8279cceb265ba.css +0 -1
- 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
|
+
};
|
|
@@ -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
|
|
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 = {
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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;
|
|
@@ -106,7 +106,8 @@ module.exports = {
|
|
|
106
106
|
before(frame) {
|
|
107
107
|
const errors = [];
|
|
108
108
|
|
|
109
|
-
|
|
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 === '
|
|
36
|
-
dbConfig.connection.timezone = '
|
|
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
|
|
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
|
});
|