ghost 5.9.4 → 5.10.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-api-framework-0.0.0.tgz +0 -0
- package/components/tryghost-domain-events-0.0.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-0.0.0.tgz +0 -0
- package/components/tryghost-email-content-generator-0.0.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-0.0.0.tgz +0 -0
- package/components/tryghost-job-manager-0.0.0.tgz +0 -0
- package/components/tryghost-magic-link-0.0.0.tgz +0 -0
- package/components/tryghost-mailgun-client-0.0.0.tgz +0 -0
- package/components/tryghost-member-attribution-0.0.0.tgz +0 -0
- package/components/tryghost-member-events-0.0.0.tgz +0 -0
- package/components/tryghost-members-api-0.0.0.tgz +0 -0
- package/components/tryghost-members-events-service-0.0.0.tgz +0 -0
- package/components/tryghost-members-importer-0.0.0.tgz +0 -0
- package/components/tryghost-members-offers-0.0.0.tgz +0 -0
- package/components/tryghost-members-ssr-0.0.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-0.0.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-0.0.0.tgz +0 -0
- package/components/tryghost-oembed-service-0.0.0.tgz +0 -0
- package/components/tryghost-security-0.0.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-0.0.0.tgz +0 -0
- package/components/tryghost-update-check-service-0.0.0.tgz +0 -0
- package/components/tryghost-verification-trigger-0.0.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-0.0.0.tgz +0 -0
- 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/screen.css +8 -5
- package/content/themes/casper/package.json +1 -1
- package/core/boot.js +2 -0
- package/core/bridge.js +2 -0
- package/core/built/admin/assets/{chunk.143.1c158e8ef19f10e5439c.js → chunk.143.6a3c46a89c731b86a730.js} +6 -6
- package/core/built/admin/assets/{chunk.174.eec7f6398cef4c3e2485.js → chunk.174.0364e8abdae8210d8e6d.js} +31 -29
- package/core/built/admin/assets/{chunk.178.506264293194a4922091.js → chunk.178.8a19c35ce1a7cf4249ce.js} +4 -4
- package/core/built/admin/assets/{chunk.351.73f27952f867334a8228.js → chunk.351.ea4a4ff4b40d5f2ad141.js} +22 -19
- package/core/built/admin/assets/{chunk.351.73f27952f867334a8228.js.LICENSE.txt → chunk.351.ea4a4ff4b40d5f2ad141.js.LICENSE.txt} +0 -0
- package/core/built/admin/assets/{ghost-facfdf4a7d9759c5b681340805f21fd8.css → ghost-13baab17b3f54b21f341fb8f36f83110.css} +1 -1
- package/core/built/admin/assets/{ghost-b441c9cfa2e31453e86460e50ae7e378.js → ghost-ced03a7ac75c3148e0ea7d1bf51e39fc.js} +319 -282
- package/core/built/admin/assets/{ghost-dark-4080c8f100997d4b8947f5da0e7946a1.css → ghost-dark-b0500577a42e2770994e6aef0e70f182.css} +1 -1
- package/core/built/admin/assets/icons/ghost-orb-pink.svg +10 -0
- package/core/built/admin/assets/img/logos/orb-pink-3-a2c52eb9fda9f2401ea706c3f24976ff.png +0 -0
- package/core/built/admin/assets/{vendor-516c9e43b4aeb92079dc1ab92c9ce492.js → vendor-a1ae7a38d5c38fcba5609eed4e37f02a.js} +73 -70
- package/core/built/admin/index.html +6 -6
- package/core/frontend/helpers/ghost_head.js +4 -0
- package/core/frontend/helpers/search.js +42 -0
- package/core/frontend/services/member-attribution-assets/index.js +4 -0
- package/core/frontend/services/member-attribution-assets/service.js +83 -0
- package/core/frontend/src/member-attribution/.eslintrc +10 -0
- package/core/frontend/src/member-attribution/member-attribution.js +90 -0
- package/core/frontend/web/site.js +3 -0
- package/core/server/adapters/cache/ImageSizesCacheSyncInMemory.js +7 -0
- package/core/server/adapters/cache/SettingsCacheSyncInMemory.js +7 -0
- package/core/server/api/endpoints/posts.js +10 -1
- package/core/server/api/endpoints/utils/serializers/input/posts.js +1 -1
- package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +5 -0
- package/core/server/data/exporter/table-lists.js +2 -0
- package/core/server/data/migrations/versions/5.10/2022-08-15-05-34-add-expiry-at-column-to-members-products.js +6 -0
- package/core/server/data/migrations/versions/5.10/2022-08-16-14-25-add-member-created-events-table.js +11 -0
- package/core/server/data/migrations/versions/5.10/2022-08-16-14-25-add-subscription-created-events-table.js +11 -0
- package/core/server/data/migrations/versions/5.10/2022-08-19-14-15-fix-comments-deletion-strategy.js +45 -0
- package/core/server/data/schema/schema.js +21 -2
- package/core/server/lib/image/cached-image-size-from-url.js +52 -28
- package/core/server/lib/image/image-utils.js +5 -2
- package/core/server/lib/image/index.js +14 -1
- package/core/server/models/api-key.js +3 -18
- package/core/server/models/base/plugins/actions.js +32 -0
- package/core/server/models/integration.js +3 -0
- package/core/server/models/label.js +3 -18
- package/core/server/models/member-created-event.js +26 -0
- package/core/server/models/member.js +36 -4
- package/core/server/models/post.js +26 -18
- package/core/server/models/subscription-created-event.js +30 -0
- package/core/server/models/tag.js +3 -18
- package/core/server/models/user.js +3 -18
- package/core/server/models/webhook.js +3 -0
- package/core/server/services/comments/emails.js +3 -3
- package/core/server/services/explore/service.js +3 -2
- package/core/server/services/member-attribution/index.js +24 -0
- package/core/server/services/members/api.js +3 -1
- package/core/server/services/members/jobs/clean-expired-comped.js +105 -0
- package/core/server/services/members/jobs/index.js +27 -0
- package/core/server/services/members/service.js +14 -8
- package/core/server/services/settings/settings-service.js +1 -1
- package/core/shared/config/defaults.json +6 -2
- package/core/shared/labs.js +4 -1
- package/package.json +6 -5
- package/yarn.lock +349 -507
|
@@ -13,6 +13,9 @@ Label = ghostBookshelf.Model.extend({
|
|
|
13
13
|
|
|
14
14
|
tableName: 'labels',
|
|
15
15
|
|
|
16
|
+
actionsCollectCRUD: true,
|
|
17
|
+
actionsResourceType: 'label',
|
|
18
|
+
|
|
16
19
|
emitChange: function emitChange(event, options) {
|
|
17
20
|
const eventToTrigger = 'label' + '.' + event;
|
|
18
21
|
ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
|
|
@@ -62,24 +65,6 @@ Label = ghostBookshelf.Model.extend({
|
|
|
62
65
|
const attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
|
|
63
66
|
|
|
64
67
|
return attrs;
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
getAction(event, options) {
|
|
68
|
-
const actor = this.getActor(options);
|
|
69
|
-
|
|
70
|
-
// @NOTE: we ignore internal updates (`options.context.internal`) for now
|
|
71
|
-
if (!actor) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// @TODO: implement context
|
|
76
|
-
return {
|
|
77
|
-
event: event,
|
|
78
|
-
resource_id: this.id || this.previous('id'),
|
|
79
|
-
resource_type: 'label',
|
|
80
|
-
actor_id: actor.id,
|
|
81
|
-
actor_type: actor.type
|
|
82
|
-
};
|
|
83
68
|
}
|
|
84
69
|
}, {
|
|
85
70
|
orderDefaultOptions: function orderDefaultOptions() {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const errors = require('@tryghost/errors');
|
|
2
|
+
const ghostBookshelf = require('./base');
|
|
3
|
+
|
|
4
|
+
const MemberCreatedEvent = ghostBookshelf.Model.extend({
|
|
5
|
+
tableName: 'members_created_events',
|
|
6
|
+
|
|
7
|
+
member() {
|
|
8
|
+
return this.belongsTo('Member', 'member_id', 'id');
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
attribution() {
|
|
12
|
+
return this.belongsTo('Post', 'attribution_id', 'id');
|
|
13
|
+
}
|
|
14
|
+
}, {
|
|
15
|
+
async edit() {
|
|
16
|
+
throw new errors.IncorrectUsageError({message: 'Cannot edit MemberCreatedEvent'});
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
async destroy() {
|
|
20
|
+
throw new errors.IncorrectUsageError({message: 'Cannot destroy MemberCreatedEvent'});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
MemberCreatedEvent: ghostBookshelf.model('MemberCreatedEvent', MemberCreatedEvent)
|
|
26
|
+
};
|
|
@@ -3,6 +3,7 @@ const uuid = require('uuid');
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
4
|
const config = require('../../shared/config');
|
|
5
5
|
const {gravatar} = require('../lib/image');
|
|
6
|
+
const labs = require('../../shared/labs');
|
|
6
7
|
|
|
7
8
|
const Member = ghostBookshelf.Model.extend({
|
|
8
9
|
tableName: 'members',
|
|
@@ -102,12 +103,16 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
102
103
|
|
|
103
104
|
products() {
|
|
104
105
|
return this.belongsToMany('Product', 'members_products', 'member_id', 'product_id')
|
|
105
|
-
.withPivot('sort_order')
|
|
106
|
+
.withPivot('sort_order', 'expiry_at')
|
|
106
107
|
.query('orderBy', 'sort_order', 'ASC')
|
|
107
108
|
.query((qb) => {
|
|
108
109
|
// avoids bookshelf adding a `DISTINCT` to the query
|
|
109
110
|
// we know the result set will already be unique and DISTINCT hurts query performance
|
|
110
|
-
|
|
111
|
+
if (labs.isSet('compExpiring')) {
|
|
112
|
+
qb.columns('products.*', 'expiry_at');
|
|
113
|
+
} else {
|
|
114
|
+
qb.columns('products.*');
|
|
115
|
+
}
|
|
111
116
|
});
|
|
112
117
|
},
|
|
113
118
|
|
|
@@ -155,6 +160,21 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
155
160
|
return this.hasMany('EmailRecipient', 'member_id', 'id');
|
|
156
161
|
},
|
|
157
162
|
|
|
163
|
+
async updateTierExpiry(products = [], options = {}) {
|
|
164
|
+
if (!labs.isSet('compExpiring')) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
for (const product of products) {
|
|
168
|
+
if (product?.expiry_at) {
|
|
169
|
+
const expiry = new Date(product.expiry_at);
|
|
170
|
+
const queryOptions = _.extend({}, options, {
|
|
171
|
+
query: {where: {product_id: product.id}}
|
|
172
|
+
});
|
|
173
|
+
await this.products().updatePivot({expiry_at: expiry}, queryOptions);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
|
|
158
178
|
serialize(options) {
|
|
159
179
|
const defaultSerializedObject = ghostBookshelf.Model.prototype.serialize.call(this, options);
|
|
160
180
|
|
|
@@ -344,7 +364,13 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
344
364
|
return this.add(data, Object.assign({transacting}, unfilteredOptions));
|
|
345
365
|
});
|
|
346
366
|
}
|
|
347
|
-
|
|
367
|
+
|
|
368
|
+
return ghostBookshelf.Model.add.call(this, data, unfilteredOptions).then(async (member) => {
|
|
369
|
+
if (data.products) {
|
|
370
|
+
await member.updateTierExpiry(data.products, _.pick(unfilteredOptions, 'transacting'));
|
|
371
|
+
}
|
|
372
|
+
return member;
|
|
373
|
+
});
|
|
348
374
|
},
|
|
349
375
|
|
|
350
376
|
edit(data, unfilteredOptions = {}) {
|
|
@@ -353,7 +379,13 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
353
379
|
return this.edit(data, Object.assign({transacting}, unfilteredOptions));
|
|
354
380
|
});
|
|
355
381
|
}
|
|
356
|
-
|
|
382
|
+
|
|
383
|
+
return ghostBookshelf.Model.edit.call(this, data, unfilteredOptions).then(async (member) => {
|
|
384
|
+
if (data.products) {
|
|
385
|
+
await member.updateTierExpiry(data.products, _.pick(unfilteredOptions, 'transacting'));
|
|
386
|
+
}
|
|
387
|
+
return member;
|
|
388
|
+
});
|
|
357
389
|
},
|
|
358
390
|
|
|
359
391
|
destroy(unfilteredOptions = {}) {
|
|
@@ -38,6 +38,11 @@ Post = ghostBookshelf.Model.extend({
|
|
|
38
38
|
|
|
39
39
|
tableName: 'posts',
|
|
40
40
|
|
|
41
|
+
actionsCollectCRUD: true,
|
|
42
|
+
actionsResourceType: function () {
|
|
43
|
+
return this.get('type') || this.previous('type');
|
|
44
|
+
},
|
|
45
|
+
|
|
41
46
|
/**
|
|
42
47
|
* @NOTE
|
|
43
48
|
*
|
|
@@ -962,24 +967,6 @@ Post = ghostBookshelf.Model.extend({
|
|
|
962
967
|
|
|
963
968
|
delete options.status;
|
|
964
969
|
return filter;
|
|
965
|
-
},
|
|
966
|
-
|
|
967
|
-
getAction(event, options) {
|
|
968
|
-
const actor = this.getActor(options);
|
|
969
|
-
|
|
970
|
-
// @NOTE: we ignore internal updates (`options.context.internal`) for now
|
|
971
|
-
if (!actor) {
|
|
972
|
-
return;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// @TODO: implement context
|
|
976
|
-
return {
|
|
977
|
-
event: event,
|
|
978
|
-
resource_id: this.id || this.previous('id'),
|
|
979
|
-
resource_type: 'post',
|
|
980
|
-
actor_id: actor.id,
|
|
981
|
-
actor_type: actor.type
|
|
982
|
-
};
|
|
983
970
|
}
|
|
984
971
|
}, {
|
|
985
972
|
allowedFormats: ['mobiledoc', 'html', 'plaintext'],
|
|
@@ -1262,6 +1249,27 @@ Post = ghostBookshelf.Model.extend({
|
|
|
1262
1249
|
return Promise.reject(new errors.NoPermissionError({
|
|
1263
1250
|
message: tpl(messages.notEnoughPermission)
|
|
1264
1251
|
}));
|
|
1252
|
+
},
|
|
1253
|
+
|
|
1254
|
+
countRelations() {
|
|
1255
|
+
return {
|
|
1256
|
+
signups(modelOrCollection) {
|
|
1257
|
+
modelOrCollection.query('columns', 'posts.*', (qb) => {
|
|
1258
|
+
qb.count('members_created_events.id')
|
|
1259
|
+
.from('members_created_events')
|
|
1260
|
+
.whereRaw('posts.id = members_created_events.attribution_id')
|
|
1261
|
+
.as('count__signups');
|
|
1262
|
+
});
|
|
1263
|
+
},
|
|
1264
|
+
conversions(modelOrCollection) {
|
|
1265
|
+
modelOrCollection.query('columns', 'posts.*', (qb) => {
|
|
1266
|
+
qb.count('members_subscription_created_events.id')
|
|
1267
|
+
.from('members_subscription_created_events')
|
|
1268
|
+
.whereRaw('posts.id = members_subscription_created_events.attribution_id')
|
|
1269
|
+
.as('count__conversions');
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1265
1273
|
}
|
|
1266
1274
|
});
|
|
1267
1275
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const errors = require('@tryghost/errors');
|
|
2
|
+
const ghostBookshelf = require('./base');
|
|
3
|
+
|
|
4
|
+
const SubscriptionCreatedEvent = ghostBookshelf.Model.extend({
|
|
5
|
+
tableName: 'members_subscription_created_events',
|
|
6
|
+
|
|
7
|
+
member() {
|
|
8
|
+
return this.belongsTo('Member', 'member_id', 'id');
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
subscription() {
|
|
12
|
+
return this.belongsTo('StripeCustomerSubscription', 'subscription_id', 'id');
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
attribution() {
|
|
16
|
+
return this.belongsTo('Post', 'attribution_id', 'id');
|
|
17
|
+
}
|
|
18
|
+
}, {
|
|
19
|
+
async edit() {
|
|
20
|
+
throw new errors.IncorrectUsageError({message: 'Cannot edit SubscriptionCreatedEvent'});
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
async destroy() {
|
|
24
|
+
throw new errors.IncorrectUsageError({message: 'Cannot destroy SubscriptionCreatedEvent'});
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
SubscriptionCreatedEvent: ghostBookshelf.model('SubscriptionCreatedEvent', SubscriptionCreatedEvent)
|
|
30
|
+
};
|
|
@@ -14,6 +14,9 @@ Tag = ghostBookshelf.Model.extend({
|
|
|
14
14
|
|
|
15
15
|
tableName: 'tags',
|
|
16
16
|
|
|
17
|
+
actionsCollectCRUD: true,
|
|
18
|
+
actionsResourceType: 'tag',
|
|
19
|
+
|
|
17
20
|
defaults: function defaults() {
|
|
18
21
|
return {
|
|
19
22
|
visibility: 'public'
|
|
@@ -138,24 +141,6 @@ Tag = ghostBookshelf.Model.extend({
|
|
|
138
141
|
return attrs;
|
|
139
142
|
},
|
|
140
143
|
|
|
141
|
-
getAction(event, options) {
|
|
142
|
-
const actor = this.getActor(options);
|
|
143
|
-
|
|
144
|
-
// @NOTE: we ignore internal updates (`options.context.internal`) for now
|
|
145
|
-
if (!actor) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// @TODO: implement context
|
|
150
|
-
return {
|
|
151
|
-
event: event,
|
|
152
|
-
resource_id: this.id || this.previous('id'),
|
|
153
|
-
resource_type: 'tag',
|
|
154
|
-
actor_id: actor.id,
|
|
155
|
-
actor_type: actor.type
|
|
156
|
-
};
|
|
157
|
-
},
|
|
158
|
-
|
|
159
144
|
defaultColumnsToFetch() {
|
|
160
145
|
return ['id'];
|
|
161
146
|
}
|
|
@@ -57,6 +57,9 @@ User = ghostBookshelf.Model.extend({
|
|
|
57
57
|
|
|
58
58
|
tableName: 'users',
|
|
59
59
|
|
|
60
|
+
actionsCollectCRUD: true,
|
|
61
|
+
actionsResourceType: 'user',
|
|
62
|
+
|
|
60
63
|
defaults: function defaults() {
|
|
61
64
|
return {
|
|
62
65
|
password: security.identifier.uid(50),
|
|
@@ -363,24 +366,6 @@ User = ghostBookshelf.Model.extend({
|
|
|
363
366
|
delete options.status;
|
|
364
367
|
|
|
365
368
|
return filter;
|
|
366
|
-
},
|
|
367
|
-
|
|
368
|
-
getAction(event, options) {
|
|
369
|
-
const actor = this.getActor(options);
|
|
370
|
-
|
|
371
|
-
// @NOTE: we ignore internal updates (`options.context.internal`) for now
|
|
372
|
-
if (!actor) {
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// @TODO: implement context
|
|
377
|
-
return {
|
|
378
|
-
event: event,
|
|
379
|
-
resource_id: this.id || this.previous('id'),
|
|
380
|
-
resource_type: 'user',
|
|
381
|
-
actor_id: actor.id,
|
|
382
|
-
actor_type: actor.type
|
|
383
|
-
};
|
|
384
369
|
}
|
|
385
370
|
}, {
|
|
386
371
|
orderDefaultOptions: function orderDefaultOptions() {
|
|
@@ -45,7 +45,7 @@ class CommentsServiceEmails {
|
|
|
45
45
|
accentColor: this.settingsCache.get('accent_color'),
|
|
46
46
|
fromEmail: this.notificationFromAddress,
|
|
47
47
|
toEmail: to,
|
|
48
|
-
staffUrl:
|
|
48
|
+
staffUrl: this.urlUtils.urlJoin(this.urlUtils.urlFor('admin', true), '#', `/settings/staff/${author.get('slug')}`)
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
const {html, text} = await this.renderEmailTemplate('new-comment', templateData);
|
|
@@ -132,7 +132,7 @@ class CommentsServiceEmails {
|
|
|
132
132
|
commentHtml: comment.get('html'),
|
|
133
133
|
commentText: htmlToPlaintext.comment(comment.get('html')),
|
|
134
134
|
commentDate: moment(comment.get('created_at')).tz(this.settingsCache.get('timezone')).format('D MMM YYYY'),
|
|
135
|
-
|
|
135
|
+
|
|
136
136
|
reporterName: reporter.name,
|
|
137
137
|
reporterEmail: reporter.email,
|
|
138
138
|
reporter: reporter.name ? `${reporter.name} (${reporter.email})` : reporter.email,
|
|
@@ -144,7 +144,7 @@ class CommentsServiceEmails {
|
|
|
144
144
|
accentColor: this.settingsCache.get('accent_color'),
|
|
145
145
|
fromEmail: this.notificationFromAddress,
|
|
146
146
|
toEmail: to,
|
|
147
|
-
staffUrl:
|
|
147
|
+
staffUrl: this.urlUtils.urlJoin(this.urlUtils.urlFor('admin', true), '#', `/settings/staff/${owner.get('slug')}`)
|
|
148
148
|
};
|
|
149
149
|
|
|
150
150
|
const {html, text} = await this.renderEmailTemplate('report', templateData);
|
|
@@ -26,7 +26,7 @@ module.exports = class ExploreService {
|
|
|
26
26
|
const totalMembers = await this.MembersService.stats.getTotalMembers();
|
|
27
27
|
const mrrStats = await this.StatsService.getMRRHistory();
|
|
28
28
|
|
|
29
|
-
const {description, icon, title, url} = this.PublicConfigService.site;
|
|
29
|
+
const {description, icon, title, url, accent_color: accentColor} = this.PublicConfigService.site;
|
|
30
30
|
|
|
31
31
|
const exploreProperties = {
|
|
32
32
|
version: ghostVersion.full,
|
|
@@ -36,7 +36,8 @@ module.exports = class ExploreService {
|
|
|
36
36
|
description,
|
|
37
37
|
icon,
|
|
38
38
|
title,
|
|
39
|
-
url
|
|
39
|
+
url,
|
|
40
|
+
accentColor
|
|
40
41
|
},
|
|
41
42
|
stripe: {
|
|
42
43
|
configured: this.StripeService.api.configured,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const urlService = require('../url');
|
|
2
|
+
const labsService = require('../../../shared/labs');
|
|
3
|
+
|
|
4
|
+
class MemberAttributionServiceWrapper {
|
|
5
|
+
init() {
|
|
6
|
+
if (this.service) {
|
|
7
|
+
// Prevent creating duplicate DomainEvents subscribers
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const MemberAttributionService = require('@tryghost/member-attribution');
|
|
12
|
+
const models = require('../../models');
|
|
13
|
+
|
|
14
|
+
// For now we don't need to expose anything (yet)
|
|
15
|
+
this.service = new MemberAttributionService({
|
|
16
|
+
MemberCreatedEvent: models.MemberCreatedEvent,
|
|
17
|
+
SubscriptionCreatedEvent: models.SubscriptionCreatedEvent,
|
|
18
|
+
urlService,
|
|
19
|
+
labsService
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = new MemberAttributionServiceWrapper();
|
|
@@ -14,6 +14,7 @@ const urlUtils = require('../../../shared/url-utils');
|
|
|
14
14
|
const labsService = require('../../../shared/labs');
|
|
15
15
|
const offersService = require('../offers');
|
|
16
16
|
const newslettersService = require('../newsletters');
|
|
17
|
+
const memberAttributionService = require('../member-attribution');
|
|
17
18
|
|
|
18
19
|
const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
|
|
19
20
|
|
|
@@ -195,7 +196,8 @@ function createApiInstance(config) {
|
|
|
195
196
|
stripeAPIService: stripeService.api,
|
|
196
197
|
offersAPI: offersService.api,
|
|
197
198
|
labsService: labsService,
|
|
198
|
-
newslettersService: newslettersService
|
|
199
|
+
newslettersService: newslettersService,
|
|
200
|
+
memberAttributionService: memberAttributionService.service
|
|
199
201
|
});
|
|
200
202
|
|
|
201
203
|
return membersApiInstance;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const {parentPort} = require('worker_threads');
|
|
2
|
+
const ObjectId = require('bson-objectid').default;
|
|
3
|
+
const {chunk: chunkArray} = require('lodash');
|
|
4
|
+
const debug = require('@tryghost/debug')('jobs:clean-expired-comped');
|
|
5
|
+
const moment = require('moment');
|
|
6
|
+
|
|
7
|
+
// recurring job to clean expired complimentary subscriptions
|
|
8
|
+
|
|
9
|
+
// Exit early when cancelled to prevent stalling shutdown. No cleanup needed when cancelling as everything is idempotent and will pick up
|
|
10
|
+
// where it left off on next run
|
|
11
|
+
function cancel() {
|
|
12
|
+
if (parentPort) {
|
|
13
|
+
parentPort.postMessage('Expired complimentary subscriptions cleanup cancelled before completion');
|
|
14
|
+
parentPort.postMessage('cancelled');
|
|
15
|
+
} else {
|
|
16
|
+
setTimeout(() => {
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}, 1000);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (parentPort) {
|
|
23
|
+
parentPort.once('message', (message) => {
|
|
24
|
+
if (message === 'cancel') {
|
|
25
|
+
return cancel();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
(async () => {
|
|
31
|
+
const cleanupStartDate = new Date();
|
|
32
|
+
const db = require('../../../data/db');
|
|
33
|
+
debug(`Starting cleanup of expired comp subscriptions`);
|
|
34
|
+
const expiredCompedRows = await db.knex('members_products')
|
|
35
|
+
.where('expiry_at', '<', moment.utc().startOf('day').toISOString())
|
|
36
|
+
.select('*');
|
|
37
|
+
|
|
38
|
+
let deletedExpiredSubs = 0;
|
|
39
|
+
let updatedMembers = 0;
|
|
40
|
+
|
|
41
|
+
// Run cleanup for expired comp subscriptions
|
|
42
|
+
// Removes expired comped entries from members_products table
|
|
43
|
+
// Updates affected members status to free from comped
|
|
44
|
+
// Adds member status event for going from comped to free
|
|
45
|
+
if (expiredCompedRows?.length) {
|
|
46
|
+
const rowIds = expiredCompedRows.map(d => d.id);
|
|
47
|
+
const memberIds = expiredCompedRows.map(d => d.member_id);
|
|
48
|
+
|
|
49
|
+
// Delete all expired comped rows
|
|
50
|
+
deletedExpiredSubs = await db.knex('members_products')
|
|
51
|
+
.whereIn('id', rowIds)
|
|
52
|
+
.del();
|
|
53
|
+
|
|
54
|
+
// fetch all comped members to update
|
|
55
|
+
const membersToUpdate = await db.knex('members')
|
|
56
|
+
.whereIn('id', memberIds)
|
|
57
|
+
.andWhere('status', 'comped');
|
|
58
|
+
|
|
59
|
+
const updateMemberIds = membersToUpdate.map(d => d.id);
|
|
60
|
+
|
|
61
|
+
// Update all comped members to free
|
|
62
|
+
updatedMembers = await db.knex('members')
|
|
63
|
+
.whereIn('id', updateMemberIds)
|
|
64
|
+
.update({
|
|
65
|
+
status: 'free'
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const statusEvents = membersToUpdate.map((member) => {
|
|
69
|
+
const now = db.knex.raw('CURRENT_TIMESTAMP');
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
id: ObjectId().toHexString(),
|
|
73
|
+
member_id: member.id,
|
|
74
|
+
from_status: member.status,
|
|
75
|
+
to_status: 'free',
|
|
76
|
+
created_at: now
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// SQLite >= 3.32.0 can support 32766 host parameters
|
|
81
|
+
// each row uses 5 variables so ⌊32766/5⌋ = 6553
|
|
82
|
+
const chunkSize = 6553;
|
|
83
|
+
|
|
84
|
+
const chunks = chunkArray(statusEvents, chunkSize);
|
|
85
|
+
|
|
86
|
+
// Adds status event for members going comped->free
|
|
87
|
+
for (const chunk of chunks) {
|
|
88
|
+
await db.knex('members_status_events').insert(chunk);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let cleanupEndDate = new Date();
|
|
93
|
+
|
|
94
|
+
debug(`Removed ${deletedExpiredSubs} expired subscriptions, updated ${updatedMembers} members in ${cleanupEndDate.valueOf() - cleanupStartDate.valueOf()}ms`);
|
|
95
|
+
|
|
96
|
+
if (parentPort) {
|
|
97
|
+
parentPort.postMessage(`Removed ${deletedExpiredSubs} expired subscriptions, updated ${updatedMembers} members in ${cleanupEndDate.valueOf() - cleanupStartDate.valueOf()}ms`);
|
|
98
|
+
parentPort.postMessage('done');
|
|
99
|
+
} else {
|
|
100
|
+
// give the logging pipes time finish writing before exit
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}, 1000);
|
|
104
|
+
}
|
|
105
|
+
})();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const jobsService = require('../../jobs');
|
|
3
|
+
|
|
4
|
+
let hasScheduled = false;
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
async scheduleExpiredCompCleanupJob() {
|
|
8
|
+
if (
|
|
9
|
+
!hasScheduled &&
|
|
10
|
+
!process.env.NODE_ENV.startsWith('test')
|
|
11
|
+
) {
|
|
12
|
+
// use a random seconds value to avoid spikes to external APIs on the minute
|
|
13
|
+
const s = Math.floor(Math.random() * 60); // 0-59
|
|
14
|
+
|
|
15
|
+
// Run everyday at 12:05:X AM to clean all expired complimentary subscriptions
|
|
16
|
+
jobsService.addJob({
|
|
17
|
+
at: `${s} 5 0 * * *`,
|
|
18
|
+
job: path.resolve(__dirname, 'clean-expired-comped.js'),
|
|
19
|
+
name: 'clean-expired-comped'
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
hasScheduled = true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return hasScheduled;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -6,6 +6,7 @@ const db = require('../../data/db');
|
|
|
6
6
|
const MembersConfigProvider = require('./config');
|
|
7
7
|
const MembersCSVImporter = require('@tryghost/members-importer');
|
|
8
8
|
const MembersStats = require('./stats/members-stats');
|
|
9
|
+
const memberJobs = require('./jobs');
|
|
9
10
|
const createMembersSettingsInstance = require('./settings');
|
|
10
11
|
const logging = require('@tryghost/logging');
|
|
11
12
|
const urlUtils = require('../../../shared/url-utils');
|
|
@@ -148,16 +149,21 @@ module.exports = {
|
|
|
148
149
|
}
|
|
149
150
|
})();
|
|
150
151
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
jobsService.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
152
|
+
if (!env?.startsWith('testing')) {
|
|
153
|
+
const membersMigrationJobName = 'members-migrations';
|
|
154
|
+
if (!(await jobsService.hasExecutedSuccessfully(membersMigrationJobName))) {
|
|
155
|
+
jobsService.addOneOffJob({
|
|
156
|
+
name: membersMigrationJobName,
|
|
157
|
+
offloaded: false,
|
|
158
|
+
job: stripeService.migrations.execute.bind(stripeService.migrations)
|
|
159
|
+
});
|
|
158
160
|
|
|
159
|
-
|
|
161
|
+
await jobsService.awaitCompletion(membersMigrationJobName);
|
|
162
|
+
}
|
|
160
163
|
}
|
|
164
|
+
|
|
165
|
+
// Schedule daily cron job to clean expired comp subs
|
|
166
|
+
memberJobs.scheduleExpiredCompCleanupJob();
|
|
161
167
|
},
|
|
162
168
|
contentGating: require('./content-gating'),
|
|
163
169
|
|
|
@@ -67,7 +67,7 @@ module.exports = {
|
|
|
67
67
|
* Initialize the cache, used in boot and in testing
|
|
68
68
|
*/
|
|
69
69
|
async init() {
|
|
70
|
-
const cacheStore = adapterManager.getAdapter('cache');
|
|
70
|
+
const cacheStore = adapterManager.getAdapter('cache:settings');
|
|
71
71
|
const settingsCollection = await models.Settings.populateDefaults();
|
|
72
72
|
SettingsCache.init(events, settingsCollection, this.getCalculatedFields(), cacheStore);
|
|
73
73
|
},
|
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
"active": "Default"
|
|
26
26
|
},
|
|
27
27
|
"cache": {
|
|
28
|
-
"active": "Memory"
|
|
28
|
+
"active": "Memory",
|
|
29
|
+
"settings": "SettingsCacheSyncInMemory",
|
|
30
|
+
"SettingsCacheSyncInMemory": {},
|
|
31
|
+
"imageSizes": "ImageSizesCacheSyncInMemory",
|
|
32
|
+
"ImageSizesCacheSyncInMemory": {}
|
|
29
33
|
}
|
|
30
34
|
},
|
|
31
35
|
"storage": {
|
|
@@ -137,7 +141,7 @@
|
|
|
137
141
|
},
|
|
138
142
|
"portal": {
|
|
139
143
|
"url": "https://cdn.jsdelivr.net/npm/@tryghost/portal@~{version}/umd/portal.min.js",
|
|
140
|
-
"version": "2.
|
|
144
|
+
"version": "2.9"
|
|
141
145
|
},
|
|
142
146
|
"sodoSearch": {
|
|
143
147
|
"url": "https://cdn.jsdelivr.net/npm/@tryghost/sodo-search@~{version}/umd/sodo-search.min.js",
|
package/core/shared/labs.js
CHANGED