ghost 5.19.3 → 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.
- package/components/tryghost-adapter-manager-5.21.0.tgz +0 -0
- package/components/{tryghost-api-framework-5.19.3.tgz → tryghost-api-framework-5.21.0.tgz} +0 -0
- package/components/tryghost-api-version-compatibility-service-5.21.0.tgz +0 -0
- package/components/tryghost-audience-feedback-5.21.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.21.0.tgz +0 -0
- package/components/tryghost-constants-5.21.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.21.0.tgz +0 -0
- package/components/tryghost-data-generator-5.21.0.tgz +0 -0
- package/components/tryghost-domain-events-5.21.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.21.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.21.0.tgz +0 -0
- package/components/tryghost-email-content-generator-5.21.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.21.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.21.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.21.0.tgz +0 -0
- package/components/{tryghost-job-manager-5.19.3.tgz → tryghost-job-manager-5.21.0.tgz} +0 -0
- package/components/tryghost-link-redirects-5.21.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.21.0.tgz +0 -0
- package/components/tryghost-link-tracking-5.21.0.tgz +0 -0
- package/components/tryghost-magic-link-5.21.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.21.0.tgz +0 -0
- package/components/tryghost-member-analytics-service-5.21.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.21.0.tgz +0 -0
- package/components/tryghost-member-events-5.21.0.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-5.21.0.tgz +0 -0
- package/components/tryghost-members-api-5.21.0.tgz +0 -0
- package/components/tryghost-members-csv-5.21.0.tgz +0 -0
- package/components/tryghost-members-events-service-5.21.0.tgz +0 -0
- package/components/tryghost-members-importer-5.21.0.tgz +0 -0
- package/components/tryghost-members-offers-5.21.0.tgz +0 -0
- package/components/tryghost-members-payments-5.21.0.tgz +0 -0
- package/components/{tryghost-members-ssr-5.19.3.tgz → tryghost-members-ssr-5.21.0.tgz} +0 -0
- package/components/tryghost-members-stripe-service-5.21.0.tgz +0 -0
- package/components/tryghost-minifier-5.21.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.21.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.21.0.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.21.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.21.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.21.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.21.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.21.0.tgz +0 -0
- package/components/{tryghost-package-json-5.19.3.tgz → tryghost-package-json-5.21.0.tgz} +0 -0
- package/components/tryghost-referrers-5.21.0.tgz +0 -0
- package/components/tryghost-security-5.21.0.tgz +0 -0
- package/components/tryghost-session-service-5.21.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.21.0.tgz +0 -0
- package/components/tryghost-staff-service-5.21.0.tgz +0 -0
- package/components/tryghost-stats-service-5.21.0.tgz +0 -0
- package/components/tryghost-tiers-5.21.0.tgz +0 -0
- package/components/{tryghost-update-check-service-5.19.3.tgz → tryghost-update-check-service-5.21.0.tgz} +0 -0
- package/components/tryghost-verification-trigger-5.21.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.21.0.tgz +0 -0
- package/core/boot.js +2 -0
- package/core/built/admin/assets/{chunk.143.c035c61595ed02eee886.js → chunk.143.9cddfa7bd1a8b9cf3d4b.js} +7 -7
- package/core/built/admin/assets/{chunk.178.998dfbcebcec635146b1.js → chunk.178.6de14cfdb28df721b66e.js} +4 -4
- package/core/built/admin/assets/{chunk.613.f1d519ad47e7f9024263.js → chunk.613.695f31829550fb00d43c.js} +352 -421
- package/core/built/admin/assets/{chunk.613.f1d519ad47e7f9024263.js.LICENSE.txt → chunk.613.695f31829550fb00d43c.js.LICENSE.txt} +0 -0
- package/core/built/admin/assets/{ghost-5ce6f5a730c83c91fc258b12c537ea35.js → ghost-192fee3b46a193df1e65c49a67a7d694.js} +2866 -2707
- package/core/built/admin/assets/ghost-9873519a8ad69b5b23284f0a9e050bc6.css +1 -0
- package/core/built/admin/assets/ghost-dark-190bdce42b125c3d4be930bd7599b442.css +1 -0
- package/core/built/admin/assets/{vendor-5c7d7063620bec13668c4370145cd4b4.js → vendor-26cca1d4d56660dc6e915a12ccc3b330.js} +1079 -1032
- package/core/built/admin/index.html +7 -7
- package/core/cli/generate-data.js +51 -0
- package/core/frontend/helpers/ghost_head.js +1 -1
- package/core/server/api/endpoints/links.js +34 -1
- package/core/server/api/endpoints/members.js +1 -4
- package/core/server/api/endpoints/posts-public.js +1 -1
- package/core/server/api/endpoints/posts.js +2 -1
- package/core/server/api/endpoints/tiers-public.js +2 -14
- package/core/server/api/endpoints/tiers.js +5 -51
- package/core/server/api/endpoints/utils/serializers/input/posts.js +21 -1
- package/core/server/api/endpoints/utils/serializers/input/settings.js +1 -0
- package/core/server/api/endpoints/utils/serializers/input/tiers.js +18 -27
- package/core/server/api/endpoints/utils/serializers/output/index.js +4 -0
- package/core/server/api/endpoints/utils/serializers/output/links.js +5 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +89 -15
- package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +20 -6
- package/core/server/api/endpoints/utils/serializers/output/mappers/snippets.js +2 -2
- package/core/server/api/endpoints/utils/serializers/output/members.js +6 -5
- package/core/server/api/endpoints/utils/serializers/output/tiers.js +15 -55
- package/core/server/data/db/backup.js +17 -10
- package/core/server/data/importer/importers/data/custom-theme-settings.js +81 -0
- package/core/server/data/importer/importers/data/data-importer.js +2 -0
- package/core/server/data/migrations/utils/permissions.js +35 -24
- package/core/server/data/migrations/versions/5.20/2022-10-18-05-39-drop-nullable-tier-id.js +3 -0
- package/core/server/data/migrations/versions/5.20/2022-10-18-10-13-add-ghost-subscription-id-column-to-mscs.js +10 -0
- package/core/server/data/migrations/versions/5.20/2022-10-19-11-17-add-link-browse-permissions.js +10 -0
- package/core/server/data/migrations/versions/5.20/2022-10-20-02-52-add-link-edit-permissions.js +10 -0
- package/core/server/data/migrations/versions/5.21/2022-10-24-07-23-disable-feedback-enabled.js +20 -0
- package/core/server/data/migrations/versions/5.21/2022-10-25-12-05-backfill-missed-products-columns.js +35 -0
- package/core/server/data/migrations/versions/5.21/2022-10-26-04-49-add-batch-id-members-created-events.js +7 -0
- package/core/server/data/migrations/versions/5.21/2022-10-26-04-49-add-batch-id-subscription-created-events.js +7 -0
- package/core/server/data/migrations/versions/5.21/2022-10-26-04-50-member-subscription-created-batch-id.js +72 -0
- package/core/server/data/migrations/versions/5.21/2022-10-26-09-32-add-feedback-enabled-column-to-emails.js +7 -0
- package/core/server/data/migrations/versions/5.21/2022-10-27-09-50-add-member-track-source-setting.js +8 -0
- package/core/server/data/schema/commands.js +107 -48
- package/core/server/data/schema/default-settings/default-settings.json +8 -0
- package/core/server/data/schema/fixtures/fixture-manager.js +16 -14
- package/core/server/data/schema/fixtures/fixtures.json +14 -2
- package/core/server/data/schema/schema.js +7 -3
- package/core/server/models/base/plugins/actions.js +1 -1
- package/core/server/models/base/plugins/crud.js +12 -0
- package/core/server/models/email-recipient.js +14 -0
- package/core/server/models/email.js +2 -5
- package/core/server/models/member-click-event.js +37 -0
- package/core/server/models/member-created-event.js +23 -0
- package/core/server/models/member-paid-subscription-event.js +19 -0
- package/core/server/models/member.js +6 -0
- package/core/server/models/post.js +33 -2
- package/core/server/models/redirect.js +1 -0
- package/core/server/models/subscription-created-event.js +7 -0
- package/core/server/services/audience-feedback/index.js +2 -0
- package/core/server/services/link-redirection/LinkRedirectRepository.js +16 -5
- package/core/server/services/link-tracking/PostLinkRepository.js +26 -2
- package/core/server/services/link-tracking/index.js +3 -1
- package/core/server/services/mega/feedback-buttons.js +87 -16
- package/core/server/services/mega/mega.js +1 -0
- package/core/server/services/mega/template.js +3 -0
- package/core/server/services/member-attribution/index.js +3 -1
- package/core/server/services/members/api.js +4 -1
- package/core/server/services/members/service.js +8 -1
- package/core/server/services/newsletters/index.js +3 -1
- package/core/server/services/newsletters/service.js +11 -1
- package/core/server/services/tiers/TierRepository.js +116 -0
- package/core/server/services/tiers/index.js +1 -0
- package/core/server/services/tiers/service.js +32 -0
- package/core/server/services/url/UrlGenerator.js +4 -2
- package/core/server/web/api/endpoints/admin/routes.js +1 -0
- package/core/shared/config/defaults.json +1 -1
- package/core/shared/labs.js +7 -8
- package/ghost.js +1 -0
- package/package.json +117 -113
- package/yarn.lock +1187 -1129
- package/components/tryghost-adapter-manager-5.19.3.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.19.3.tgz +0 -0
- package/components/tryghost-audience-feedback-5.19.3.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.19.3.tgz +0 -0
- package/components/tryghost-constants-5.19.3.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.19.3.tgz +0 -0
- package/components/tryghost-domain-events-5.19.3.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.19.3.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.19.3.tgz +0 -0
- package/components/tryghost-email-content-generator-5.19.3.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.19.3.tgz +0 -0
- package/components/tryghost-extract-api-key-5.19.3.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.19.3.tgz +0 -0
- package/components/tryghost-link-redirects-5.19.3.tgz +0 -0
- package/components/tryghost-link-replacer-5.19.3.tgz +0 -0
- package/components/tryghost-link-tracking-5.19.3.tgz +0 -0
- package/components/tryghost-magic-link-5.19.3.tgz +0 -0
- package/components/tryghost-mailgun-client-5.19.3.tgz +0 -0
- package/components/tryghost-member-analytics-service-5.19.3.tgz +0 -0
- package/components/tryghost-member-attribution-5.19.3.tgz +0 -0
- package/components/tryghost-member-events-5.19.3.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-5.19.3.tgz +0 -0
- package/components/tryghost-members-api-5.19.3.tgz +0 -0
- package/components/tryghost-members-csv-5.19.3.tgz +0 -0
- package/components/tryghost-members-events-service-5.19.3.tgz +0 -0
- package/components/tryghost-members-importer-5.19.3.tgz +0 -0
- package/components/tryghost-members-offers-5.19.3.tgz +0 -0
- package/components/tryghost-members-payments-5.19.3.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.19.3.tgz +0 -0
- package/components/tryghost-minifier-5.19.3.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.19.3.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.19.3.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.19.3.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.19.3.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.19.3.tgz +0 -0
- package/components/tryghost-mw-vhost-5.19.3.tgz +0 -0
- package/components/tryghost-oembed-service-5.19.3.tgz +0 -0
- package/components/tryghost-referrers-5.19.3.tgz +0 -0
- package/components/tryghost-security-5.19.3.tgz +0 -0
- package/components/tryghost-session-service-5.19.3.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.19.3.tgz +0 -0
- package/components/tryghost-staff-service-5.19.3.tgz +0 -0
- package/components/tryghost-stats-service-5.19.3.tgz +0 -0
- package/components/tryghost-verification-trigger-5.19.3.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.19.3.tgz +0 -0
- package/core/built/admin/assets/ghost-982146a4ada3a5af1981d1919ae01d08.css +0 -1
- package/core/built/admin/assets/ghost-dark-41929e4857de411a23597a9de49a4e4f.css +0 -1
|
@@ -8,6 +8,10 @@ const MemberPaidSubscriptionEvent = ghostBookshelf.Model.extend({
|
|
|
8
8
|
return this.belongsTo('Member', 'member_id', 'id');
|
|
9
9
|
},
|
|
10
10
|
|
|
11
|
+
stripeSubscription() {
|
|
12
|
+
return this.belongsTo('StripeCustomerSubscription', 'subscription_id', 'id');
|
|
13
|
+
},
|
|
14
|
+
|
|
11
15
|
subscriptionCreatedEvent() {
|
|
12
16
|
return this.belongsTo('SubscriptionCreatedEvent', 'subscription_id', 'subscription_id');
|
|
13
17
|
},
|
|
@@ -25,6 +29,21 @@ const MemberPaidSubscriptionEvent = ghostBookshelf.Model.extend({
|
|
|
25
29
|
.groupByRaw('currency, DATE(created_at)')
|
|
26
30
|
.orderByRaw('DATE(created_at)');
|
|
27
31
|
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
filterRelations() {
|
|
35
|
+
return {
|
|
36
|
+
subscriptionCreatedEvent: {
|
|
37
|
+
// Mongo-knex doesn't support belongsTo relations
|
|
38
|
+
tableName: 'members_subscription_created_events',
|
|
39
|
+
tableNameAs: 'subscriptionCreatedEvent',
|
|
40
|
+
type: 'manyToMany',
|
|
41
|
+
joinTable: 'members_paid_subscription_events',
|
|
42
|
+
joinFrom: 'id',
|
|
43
|
+
joinToForeign: 'subscription_id',
|
|
44
|
+
joinTo: 'subscription_id'
|
|
45
|
+
}
|
|
46
|
+
};
|
|
28
47
|
}
|
|
29
48
|
}, {
|
|
30
49
|
permittedOptions(methodName) {
|
|
@@ -114,6 +114,12 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
114
114
|
joinTable: 'email_recipients',
|
|
115
115
|
joinFrom: 'member_id',
|
|
116
116
|
joinTo: 'email_id'
|
|
117
|
+
},
|
|
118
|
+
feedback: {
|
|
119
|
+
tableName: 'members_feedback',
|
|
120
|
+
tableNameAs: 'feedback',
|
|
121
|
+
type: 'oneToOne',
|
|
122
|
+
joinFrom: 'member_id'
|
|
117
123
|
}
|
|
118
124
|
};
|
|
119
125
|
},
|
|
@@ -236,6 +236,17 @@ Post = ghostBookshelf.Model.extend({
|
|
|
236
236
|
},
|
|
237
237
|
|
|
238
238
|
orderRawQuery: function orderRawQuery(field, direction, withRelated) {
|
|
239
|
+
if (field === 'sentiment') {
|
|
240
|
+
if (withRelated.includes('count.sentiment')) {
|
|
241
|
+
// Internally sentiment can be included via the count.sentiment relation. We can do a quick optimisation of the query in that case.
|
|
242
|
+
return {
|
|
243
|
+
orderByRaw: `count__sentiment ${direction}`
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
orderByRaw: `(select AVG(score) from \`members_feedback\` where posts.id = members_feedback.post_id) ${direction}`
|
|
248
|
+
};
|
|
249
|
+
}
|
|
239
250
|
if (field === 'email.open_rate' && withRelated && withRelated.indexOf('email') > -1) {
|
|
240
251
|
return {
|
|
241
252
|
// *1.0 is needed on one of the columns to prevent sqlite from
|
|
@@ -1346,6 +1357,26 @@ Post = ghostBookshelf.Model.extend({
|
|
|
1346
1357
|
.as('count__paid_conversions');
|
|
1347
1358
|
});
|
|
1348
1359
|
},
|
|
1360
|
+
/**
|
|
1361
|
+
* Combination of sigups and paid conversions, but unique per member
|
|
1362
|
+
*/
|
|
1363
|
+
conversions(modelOrCollection) {
|
|
1364
|
+
modelOrCollection.query('columns', 'posts.*', (qb) => {
|
|
1365
|
+
qb.count('*')
|
|
1366
|
+
.from('k')
|
|
1367
|
+
.with('k', (q) => {
|
|
1368
|
+
q.select('member_id')
|
|
1369
|
+
.from('members_subscription_created_events')
|
|
1370
|
+
.whereRaw('posts.id = members_subscription_created_events.attribution_id')
|
|
1371
|
+
.union(function () {
|
|
1372
|
+
this.select('member_id')
|
|
1373
|
+
.from('members_created_events')
|
|
1374
|
+
.whereRaw('posts.id = members_created_events.attribution_id');
|
|
1375
|
+
});
|
|
1376
|
+
})
|
|
1377
|
+
.as('count__conversions');
|
|
1378
|
+
});
|
|
1379
|
+
},
|
|
1349
1380
|
clicks(modelOrCollection) {
|
|
1350
1381
|
modelOrCollection.query('columns', 'posts.*', (qb) => {
|
|
1351
1382
|
qb.countDistinct('members_click_events.member_id')
|
|
@@ -1357,7 +1388,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
1357
1388
|
},
|
|
1358
1389
|
sentiment(modelOrCollection) {
|
|
1359
1390
|
modelOrCollection.query('columns', 'posts.*', (qb) => {
|
|
1360
|
-
qb.select(qb.client.raw('ROUND(AVG(score) * 100)'))
|
|
1391
|
+
qb.select(qb.client.raw('COALESCE(ROUND(AVG(score) * 100), 0)'))
|
|
1361
1392
|
.from('members_feedback')
|
|
1362
1393
|
.whereRaw('posts.id = members_feedback.post_id')
|
|
1363
1394
|
.as('count__sentiment');
|
|
@@ -1368,7 +1399,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
1368
1399
|
qb.count('*')
|
|
1369
1400
|
.from('members_feedback')
|
|
1370
1401
|
.whereRaw('posts.id = members_feedback.post_id AND members_feedback.score = 0')
|
|
1371
|
-
.as('
|
|
1402
|
+
.as('count__negative_feedback');
|
|
1372
1403
|
});
|
|
1373
1404
|
},
|
|
1374
1405
|
positive_feedback(modelOrCollection) {
|
|
@@ -53,6 +53,7 @@ const Redirect = ghostBookshelf.Model.extend({
|
|
|
53
53
|
qb.countDistinct('members_click_events.member_id')
|
|
54
54
|
.from('members_click_events')
|
|
55
55
|
.whereRaw('redirects.id = members_click_events.redirect_id')
|
|
56
|
+
.whereRaw('redirects.updated_at <= members_click_events.created_at')
|
|
56
57
|
.as('count__clicks');
|
|
57
58
|
});
|
|
58
59
|
}
|
|
@@ -8,6 +8,13 @@ const SubscriptionCreatedEvent = ghostBookshelf.Model.extend({
|
|
|
8
8
|
return this.belongsTo('Member', 'member_id', 'id');
|
|
9
9
|
},
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* The member created event that happend at the same time (if any)
|
|
13
|
+
*/
|
|
14
|
+
memberCreatedEvent() {
|
|
15
|
+
return this.belongsTo('MemberCreatedEvent', 'batch_id', 'batch_id');
|
|
16
|
+
},
|
|
17
|
+
|
|
11
18
|
subscription() {
|
|
12
19
|
return this.belongsTo('StripeCustomerSubscription', 'subscription_id', 'id');
|
|
13
20
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const urlUtils = require('../../../shared/url-utils');
|
|
2
|
+
const urlService = require('../../services/url');
|
|
2
3
|
const FeedbackRepository = require('./FeedbackRepository');
|
|
3
4
|
|
|
4
5
|
class AudienceFeedbackServiceWrapper {
|
|
@@ -22,6 +23,7 @@ class AudienceFeedbackServiceWrapper {
|
|
|
22
23
|
|
|
23
24
|
// Expose the service
|
|
24
25
|
this.service = new AudienceFeedbackService({
|
|
26
|
+
urlService,
|
|
25
27
|
config: {
|
|
26
28
|
baseURL: new URL(urlUtils.urlFor('home', true))
|
|
27
29
|
}
|
|
@@ -18,7 +18,7 @@ module.exports = class LinkRedirectRepository {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* @param {InstanceType<LinkRedirect>} linkRedirect
|
|
21
|
+
* @param {InstanceType<LinkRedirect>} linkRedirect
|
|
22
22
|
* @returns {Promise<void>}
|
|
23
23
|
*/
|
|
24
24
|
async save(linkRedirect) {
|
|
@@ -36,10 +36,14 @@ module.exports = class LinkRedirectRepository {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
fromModel(model) {
|
|
39
|
+
// Store if link has been edited
|
|
40
|
+
const edited = model.get('created_at')?.getTime() !== model.get('updated_at')?.getTime();
|
|
41
|
+
|
|
39
42
|
return new LinkRedirect({
|
|
40
43
|
id: model.id,
|
|
41
44
|
from: new URL(this.#trimLeadingSlash(model.get('from')), this.#urlUtils.urlFor('home', true)),
|
|
42
|
-
to: new URL(model.get('to'))
|
|
45
|
+
to: new URL(model.get('to')),
|
|
46
|
+
edited
|
|
43
47
|
});
|
|
44
48
|
}
|
|
45
49
|
|
|
@@ -55,10 +59,17 @@ module.exports = class LinkRedirectRepository {
|
|
|
55
59
|
return result;
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
async getFilteredIds(options) {
|
|
63
|
+
const linkRows = await this.#LinkRedirect.getFilteredCollectionQuery(options)
|
|
64
|
+
.select('redirects.id')
|
|
65
|
+
.distinct();
|
|
66
|
+
return linkRows.map(row => row.id);
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
/**
|
|
59
|
-
*
|
|
60
|
-
* @param {URL} url
|
|
61
|
-
* @returns {Promise<InstanceType<LinkRedirect>|undefined>} linkRedirect
|
|
70
|
+
*
|
|
71
|
+
* @param {URL} url
|
|
72
|
+
* @returns {Promise<InstanceType<LinkRedirect>|undefined>} linkRedirect
|
|
62
73
|
*/
|
|
63
74
|
async getByURL(url) {
|
|
64
75
|
// Strip subdirectory from path
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const {FullPostLink} = require('@tryghost/link-tracking');
|
|
2
|
+
const _ = require('lodash');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* @typedef {import('bson-objectid').default} ObjectID
|
|
@@ -22,8 +23,8 @@ module.exports = class PostLinkRepository {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
*
|
|
26
|
-
* @param {*} options
|
|
26
|
+
*
|
|
27
|
+
* @param {*} options
|
|
27
28
|
* @returns {Promise<InstanceType<FullPostLink>[]>}
|
|
28
29
|
*/
|
|
29
30
|
async getAll(options) {
|
|
@@ -48,6 +49,29 @@ module.exports = class PostLinkRepository {
|
|
|
48
49
|
return result;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
async updateLinks(linkIds, updateData, options) {
|
|
53
|
+
const bulkUpdateOptions = _.pick(options, ['transacting']);
|
|
54
|
+
|
|
55
|
+
const bulkActionResult = await this.#LinkRedirect.bulkEdit(linkIds, 'redirects', {
|
|
56
|
+
...bulkUpdateOptions,
|
|
57
|
+
data: updateData
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
bulk: {
|
|
62
|
+
action: 'updateLink',
|
|
63
|
+
meta: {
|
|
64
|
+
stats: {
|
|
65
|
+
successful: bulkActionResult.successful,
|
|
66
|
+
unsuccessful: bulkActionResult.unsuccessful
|
|
67
|
+
},
|
|
68
|
+
errors: bulkActionResult.errors,
|
|
69
|
+
unsuccessfulData: bulkActionResult.unsuccessfulData
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
51
75
|
/**
|
|
52
76
|
* @param {PostLink} postLink
|
|
53
77
|
* @returns {Promise<void>}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const LinkClickRepository = require('./LinkClickRepository');
|
|
2
2
|
const PostLinkRepository = require('./PostLinkRepository');
|
|
3
3
|
const errors = require('@tryghost/errors');
|
|
4
|
+
const urlUtils = require('../../../shared/url-utils');
|
|
4
5
|
|
|
5
6
|
class LinkTrackingServiceWrapper {
|
|
6
7
|
async init() {
|
|
@@ -38,7 +39,8 @@ class LinkTrackingServiceWrapper {
|
|
|
38
39
|
linkRedirectService: linkRedirection.service,
|
|
39
40
|
linkClickRepository: this.linkClickRepository,
|
|
40
41
|
postLinkRepository,
|
|
41
|
-
DomainEvents
|
|
42
|
+
DomainEvents,
|
|
43
|
+
urlUtils
|
|
42
44
|
});
|
|
43
45
|
|
|
44
46
|
await this.service.init();
|
|
@@ -18,20 +18,32 @@ const generateLinks = (postId, uuid, html) => {
|
|
|
18
18
|
0
|
|
19
19
|
);
|
|
20
20
|
|
|
21
|
-
html = html.replace(templateStrings.like, positiveLink.href);
|
|
22
|
-
html = html.replace(templateStrings.dislike, negativeLink.href);
|
|
21
|
+
html = html.replace(new RegExp(templateStrings.like, 'g'), positiveLink.href);
|
|
22
|
+
html = html.replace(new RegExp(templateStrings.dislike, 'g'), negativeLink.href);
|
|
23
23
|
|
|
24
24
|
return html;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
const getTemplate = (accentColor) => {
|
|
28
|
-
const likeButtonHtml = getButtonHtml(
|
|
29
|
-
|
|
28
|
+
const likeButtonHtml = getButtonHtml(
|
|
29
|
+
templateStrings.like,
|
|
30
|
+
'More like this',
|
|
31
|
+
accentColor,
|
|
32
|
+
'like-icon',
|
|
33
|
+
'https://static.ghost.org/v5.0.0/images/thumbs-up.png'
|
|
34
|
+
);
|
|
35
|
+
const dislikeButtonHtml = getButtonHtml(
|
|
36
|
+
templateStrings.dislike,
|
|
37
|
+
'Less like this',
|
|
38
|
+
accentColor,
|
|
39
|
+
'dislike-icon',
|
|
40
|
+
'https://static.ghost.org/v5.0.0/images/thumbs-down.png'
|
|
41
|
+
);
|
|
30
42
|
|
|
31
43
|
return (`
|
|
32
44
|
<tr>
|
|
33
45
|
<td dir="ltr" width="100%" style="background-color: #ffffff; text-align: center; padding: 40px 4px; border-bottom: 1px solid #e5eff5" align="center">
|
|
34
|
-
<h3 style="text-align: center; margin-bottom: 22px; font-size: 17px; letter-spacing: -0.2px; margin-top: 0 !important;">
|
|
46
|
+
<h3 style="text-align: center; margin-bottom: 22px; font-size: 17px; letter-spacing: -0.2px; margin-top: 0 !important;">Give feedback on this post</h3>
|
|
35
47
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="margin: auto; width: auto !important;">
|
|
36
48
|
<tr>
|
|
37
49
|
${likeButtonHtml}
|
|
@@ -43,19 +55,44 @@ const getTemplate = (accentColor) => {
|
|
|
43
55
|
`);
|
|
44
56
|
};
|
|
45
57
|
|
|
46
|
-
function getButtonHtml(href, buttonText, accentColor) {
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const textColor = color.darken(0.6).hex();
|
|
58
|
+
function getButtonHtml(href, buttonText, accentColor, className, iconUrl) {
|
|
59
|
+
const bgColor = getButtonLightTheme(accentColor).backgroundColor;
|
|
60
|
+
const textColor = getButtonLightTheme(accentColor).color;
|
|
50
61
|
|
|
51
62
|
return (`
|
|
52
|
-
|
|
53
|
-
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="width: auto
|
|
63
|
+
<td dir="ltr" valign="top" align="center" style="vertical-align: top; color: ${textColor}; font-family: inherit; font-size: 14px; text-align: center; padding: 0 8px;" nowrap>
|
|
64
|
+
<table class="feedback-buttons" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="background-color: ${bgColor}; overflow: hidden; border-radius: 22px;border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
|
|
54
65
|
<tr>
|
|
55
|
-
<td
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
<td width="16" height="38" style="paddig-left:10px;"></td>
|
|
67
|
+
<td class=${className} background=${iconUrl} bgcolor="${textColor}" width="24" height="38" valign="top" style="background-image: url(${iconUrl});vertical-align: middle; text-align: center;background-size: cover; background-position: 0 50%; background-repeat:no-repeat;">
|
|
68
|
+
<!--[if gte mso 9]>
|
|
69
|
+
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:24px;height:38px;">
|
|
70
|
+
<v:fill origin="0.5, 0.5" position="0.5, 0.5" type="tile" src=${iconUrl} color="${textColor}" size="1,1" aspect="atleast" />
|
|
71
|
+
<v:textbox inset="0,0,0,0">
|
|
72
|
+
<![endif]-->
|
|
73
|
+
<div>
|
|
74
|
+
<a style="background-color: ${bgColor};border: none; width: 24px; height: 38px; display: block" href=${href} target="_blank"></a>
|
|
75
|
+
</div>
|
|
76
|
+
<!--[if gte mso 9]>
|
|
77
|
+
</v:textbox>
|
|
78
|
+
</v:rect>
|
|
79
|
+
<![endif]-->
|
|
80
|
+
</td>
|
|
81
|
+
<td style="text-align: right;font-size: 18px; vertical-align: middle; color: ${textColor}!important; background-position: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';">
|
|
82
|
+
<div style="color: ${textColor}"><!--[if mso]>
|
|
83
|
+
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href=${href} style="height:38px;v-text-anchor:middle;width:120px;" stroke="f">
|
|
84
|
+
<w:anchorlock/>
|
|
85
|
+
<center>
|
|
86
|
+
<![endif]-->
|
|
87
|
+
<a
|
|
88
|
+
href=${href}
|
|
89
|
+
target="_blank"
|
|
90
|
+
style="padding: 0 8px 0 8px;border-radius: 0 22px 22px 0;color:${textColor}!important;display:inline-block;font-family: inherit;font-size:14px;font-weight:bold;line-height:38px;text-align:left;text-decoration:none;width:100px;-webkit-text-size-adjust:none;">
|
|
91
|
+
${buttonText}</a>
|
|
92
|
+
<!--[if mso]>
|
|
93
|
+
</center>
|
|
94
|
+
</v:rect>
|
|
95
|
+
<![endif]--></div>
|
|
59
96
|
</td>
|
|
60
97
|
</tr>
|
|
61
98
|
</table>
|
|
@@ -63,7 +100,41 @@ function getButtonHtml(href, buttonText, accentColor) {
|
|
|
63
100
|
`);
|
|
64
101
|
}
|
|
65
102
|
|
|
103
|
+
function getButtonLightTheme(accentColor) {
|
|
104
|
+
const color = new Color(accentColor);
|
|
105
|
+
const backgroundColor = `${accentColor}10`;
|
|
106
|
+
const textColor = color.darken(0.6).hex();
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
color: textColor,
|
|
110
|
+
backgroundColor
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getButtonsHeadStyles() {
|
|
115
|
+
return (`
|
|
116
|
+
.like-icon {
|
|
117
|
+
mix-blend-mode: darken;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.dislike-icon {
|
|
121
|
+
mix-blend-mode: darken;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@media (prefers-color-scheme: dark) {
|
|
125
|
+
.like-icon {
|
|
126
|
+
mix-blend-mode: initial !important;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.dislike-icon {
|
|
130
|
+
mix-blend-mode: initial !important;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
`);
|
|
134
|
+
}
|
|
135
|
+
|
|
66
136
|
module.exports = {
|
|
67
137
|
generateLinks,
|
|
68
|
-
getTemplate
|
|
138
|
+
getTemplate,
|
|
139
|
+
getButtonsHeadStyles
|
|
69
140
|
};
|
|
@@ -240,6 +240,7 @@ const addEmail = async (postModel, options) => {
|
|
|
240
240
|
submitted_at: moment().toDate(),
|
|
241
241
|
track_opens: !!settingsCache.get('email_track_opens'),
|
|
242
242
|
track_clicks: !!settingsCache.get('email_track_clicks'),
|
|
243
|
+
feedback_enabled: !!newsletter.get('feedback_enabled'),
|
|
243
244
|
recipient_filter: emailRecipientFilter,
|
|
244
245
|
newsletter_id: newsletter.id
|
|
245
246
|
}, knexOptions);
|
|
@@ -37,6 +37,7 @@ module.exports = ({post, site, newsletter, templateSettings}) => {
|
|
|
37
37
|
<head>
|
|
38
38
|
<meta name="viewport" content="width=device-width" />
|
|
39
39
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
40
|
+
<!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch><o:AllowPNG/></o:OfficeDocumentSettings></xml><![endif]-->
|
|
40
41
|
<title>${cleanPost.title}</title>
|
|
41
42
|
<style>
|
|
42
43
|
/* -------------------------------------
|
|
@@ -1161,6 +1162,8 @@ ${ templateSettings.showBadge ? `
|
|
|
1161
1162
|
}
|
|
1162
1163
|
` : ''}
|
|
1163
1164
|
|
|
1165
|
+
${iff(templateSettings.feedbackEnabled, feedbackButtons.getButtonsHeadStyles(templateSettings.accentColor), '')}
|
|
1166
|
+
|
|
1164
1167
|
</style>
|
|
1165
1168
|
</head>
|
|
1166
1169
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const urlService = require('../url');
|
|
2
2
|
const urlUtils = require('../../../shared/url-utils');
|
|
3
|
+
const settingsCache = require('../../../shared/settings-cache');
|
|
3
4
|
|
|
4
5
|
class MemberAttributionServiceWrapper {
|
|
5
6
|
init() {
|
|
@@ -38,7 +39,8 @@ class MemberAttributionServiceWrapper {
|
|
|
38
39
|
SubscriptionCreatedEvent: models.SubscriptionCreatedEvent,
|
|
39
40
|
Integration: models.Integration
|
|
40
41
|
},
|
|
41
|
-
attributionBuilder: this.attributionBuilder
|
|
42
|
+
attributionBuilder: this.attributionBuilder,
|
|
43
|
+
isTrackingEnabled: !!settingsCache.get('members_track_sources')
|
|
42
44
|
});
|
|
43
45
|
}
|
|
44
46
|
}
|
|
@@ -13,6 +13,7 @@ const SingleUseTokenProvider = require('./SingleUseTokenProvider');
|
|
|
13
13
|
const urlUtils = require('../../../shared/url-utils');
|
|
14
14
|
const labsService = require('../../../shared/labs');
|
|
15
15
|
const offersService = require('../offers');
|
|
16
|
+
const tiersService = require('../tiers');
|
|
16
17
|
const newslettersService = require('../newsletters');
|
|
17
18
|
const memberAttributionService = require('../member-attribution');
|
|
18
19
|
|
|
@@ -194,9 +195,11 @@ function createApiInstance(config) {
|
|
|
194
195
|
StripePrice: models.StripePrice,
|
|
195
196
|
Product: models.Product,
|
|
196
197
|
Settings: models.Settings,
|
|
197
|
-
Comment: models.Comment
|
|
198
|
+
Comment: models.Comment,
|
|
199
|
+
MemberFeedback: models.MemberFeedback
|
|
198
200
|
},
|
|
199
201
|
stripeAPIService: stripeService.api,
|
|
202
|
+
tiersService: tiersService,
|
|
200
203
|
offersAPI: offersService.api,
|
|
201
204
|
labsService: labsService,
|
|
202
205
|
newslettersService: newslettersService,
|
|
@@ -16,6 +16,7 @@ const config = require('../../../shared/config');
|
|
|
16
16
|
const models = require('../../models');
|
|
17
17
|
const {GhostMailer} = require('../mail');
|
|
18
18
|
const jobsService = require('../jobs');
|
|
19
|
+
const tiersService = require('../tiers');
|
|
19
20
|
const VerificationTrigger = require('@tryghost/verification-trigger');
|
|
20
21
|
const DatabaseInfo = require('@tryghost/database-info');
|
|
21
22
|
const settingsHelpers = require('../settings-helpers');
|
|
@@ -47,7 +48,13 @@ let verificationTrigger;
|
|
|
47
48
|
const membersImporter = new MembersCSVImporter({
|
|
48
49
|
storagePath: config.getContentPath('data'),
|
|
49
50
|
getTimezone: () => settingsCache.get('timezone'),
|
|
50
|
-
|
|
51
|
+
getMembersRepository: async () => {
|
|
52
|
+
const api = await module.exports.api;
|
|
53
|
+
return api.members;
|
|
54
|
+
},
|
|
55
|
+
getDefaultTier: () => {
|
|
56
|
+
return tiersService.api.readDefaultTier();
|
|
57
|
+
},
|
|
51
58
|
sendEmail: ghostMailer.send.bind(ghostMailer),
|
|
52
59
|
isSet: labsService.isSet.bind(labsService),
|
|
53
60
|
addJob: jobsService.addJob.bind(jobsService),
|
|
@@ -4,6 +4,7 @@ const mail = require('../mail');
|
|
|
4
4
|
const models = require('../../models');
|
|
5
5
|
const urlUtils = require('../../../shared/url-utils');
|
|
6
6
|
const limitService = require('../limits');
|
|
7
|
+
const labs = require('../../../shared/labs');
|
|
7
8
|
|
|
8
9
|
const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
|
|
9
10
|
|
|
@@ -13,5 +14,6 @@ module.exports = new NewslettersService({
|
|
|
13
14
|
mail,
|
|
14
15
|
singleUseTokenProvider: new SingleUseTokenProvider(models.SingleUseToken, MAGIC_LINK_TOKEN_VALIDITY),
|
|
15
16
|
urlUtils,
|
|
16
|
-
limitService
|
|
17
|
+
limitService,
|
|
18
|
+
labs
|
|
17
19
|
});
|
|
@@ -21,13 +21,16 @@ class NewslettersService {
|
|
|
21
21
|
* @param {Object} options.singleUseTokenProvider
|
|
22
22
|
* @param {Object} options.urlUtils
|
|
23
23
|
* @param {ILimitService} options.limitService
|
|
24
|
+
* @param {Object} options.labs
|
|
24
25
|
*/
|
|
25
|
-
constructor({NewsletterModel, MemberModel, mail, singleUseTokenProvider, urlUtils, limitService}) {
|
|
26
|
+
constructor({NewsletterModel, MemberModel, mail, singleUseTokenProvider, urlUtils, limitService, labs}) {
|
|
26
27
|
this.NewsletterModel = NewsletterModel;
|
|
27
28
|
this.MemberModel = MemberModel;
|
|
28
29
|
this.urlUtils = urlUtils;
|
|
29
30
|
/** @private */
|
|
30
31
|
this.limitService = limitService;
|
|
32
|
+
/** @private */
|
|
33
|
+
this.labs = labs;
|
|
31
34
|
|
|
32
35
|
/* email verification setup */
|
|
33
36
|
|
|
@@ -251,6 +254,13 @@ class NewslettersService {
|
|
|
251
254
|
}
|
|
252
255
|
}
|
|
253
256
|
|
|
257
|
+
if (cleanedAttrs.feedback_enabled) {
|
|
258
|
+
if (!this.labs.isSet('audienceFeedback')) {
|
|
259
|
+
// Not allowed to set to true
|
|
260
|
+
cleanedAttrs.feedback_enabled = false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
254
264
|
return {cleanedAttrs, emailsToVerify};
|
|
255
265
|
}
|
|
256
266
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const {Tier} = require('@tryghost/tiers');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('@tryghost/tiers/lib/TiersAPI').ITierRepository} ITierRepository
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @implements {ITierRepository}
|
|
9
|
+
*/
|
|
10
|
+
module.exports = class TierRepository {
|
|
11
|
+
/** @type {Object} */
|
|
12
|
+
#ProductModel;
|
|
13
|
+
|
|
14
|
+
/** @type {import('@tryghost/domain-events')} */
|
|
15
|
+
#DomainEvents;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} deps
|
|
19
|
+
* @param {object} deps.ProductModel Bookshelf Model
|
|
20
|
+
* @param {import('@tryghost/domain-events')} deps.DomainEvents
|
|
21
|
+
*/
|
|
22
|
+
constructor(deps) {
|
|
23
|
+
this.#ProductModel = deps.ProductModel;
|
|
24
|
+
this.#DomainEvents = deps.DomainEvents;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @private
|
|
29
|
+
*/
|
|
30
|
+
mapToTier(model) {
|
|
31
|
+
const json = model.toJSON();
|
|
32
|
+
return {
|
|
33
|
+
id: json.id,
|
|
34
|
+
name: json.name,
|
|
35
|
+
slug: json.slug,
|
|
36
|
+
status: json.active ? 'active' : 'archived',
|
|
37
|
+
welcomePageURL: json.welcome_page_url,
|
|
38
|
+
visibility: json.visibility,
|
|
39
|
+
trialDays: json.trial_days,
|
|
40
|
+
description: json.description,
|
|
41
|
+
type: json.type,
|
|
42
|
+
currency: json.currency,
|
|
43
|
+
monthlyPrice: json.monthly_price,
|
|
44
|
+
yearlyPrice: json.yearly_price,
|
|
45
|
+
createdAt: json.created_at,
|
|
46
|
+
updatedAt: json.updated_at,
|
|
47
|
+
benefits: json.benefits.map(item => item.name)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {object} [options]
|
|
53
|
+
* @param {string} [options.filter]
|
|
54
|
+
* @returns {Promise<import('@tryghost/tiers/lib/Tier')[]>}
|
|
55
|
+
*/
|
|
56
|
+
async getAll(options = {}) {
|
|
57
|
+
const collection = await this.#ProductModel.findAll({...options, withRelated: ['benefits']});
|
|
58
|
+
|
|
59
|
+
const result = [];
|
|
60
|
+
|
|
61
|
+
for (const model of collection.models) {
|
|
62
|
+
const tier = await Tier.create(this.mapToTier(model));
|
|
63
|
+
result.push(tier);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {import('bson-objectid').default} id
|
|
71
|
+
* @returns {Promise<import('@tryghost/tiers/lib/Tier')>}
|
|
72
|
+
*/
|
|
73
|
+
async getById(id) {
|
|
74
|
+
const model = await this.#ProductModel.findOne({id: id.toHexString()}, {withRelated: ['benefits']});
|
|
75
|
+
|
|
76
|
+
return await Tier.create(this.mapToTier(model));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {import('@tryghost/tiers/lib/Tier')} tier
|
|
81
|
+
* @returns {Promise<void>}
|
|
82
|
+
*/
|
|
83
|
+
async save(tier) {
|
|
84
|
+
const data = {
|
|
85
|
+
id: tier.id.toHexString(),
|
|
86
|
+
name: tier.name,
|
|
87
|
+
slug: tier.slug,
|
|
88
|
+
active: tier.status === 'active',
|
|
89
|
+
welcome_page_url: tier.welcomePageURL,
|
|
90
|
+
visibility: tier.visibility,
|
|
91
|
+
trial_days: tier.trialDays,
|
|
92
|
+
description: tier.description,
|
|
93
|
+
type: tier.type,
|
|
94
|
+
currency: tier.currency,
|
|
95
|
+
monthly_price: tier.monthlyPrice,
|
|
96
|
+
yearly_price: tier.yearlyPrice,
|
|
97
|
+
created_at: tier.createdAt,
|
|
98
|
+
updated_at: tier.updatedAt,
|
|
99
|
+
benefits: tier.benefits.map(name => ({name}))
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const existing = await this.#ProductModel.findOne({id: data.id}, {require: false});
|
|
103
|
+
|
|
104
|
+
if (!existing) {
|
|
105
|
+
await this.#ProductModel.add(data);
|
|
106
|
+
} else {
|
|
107
|
+
await this.#ProductModel.edit(data, {
|
|
108
|
+
id: data.id
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const event of tier.events) {
|
|
113
|
+
this.#DomainEvents.dispatch(event);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./service');
|