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.
Files changed (85) hide show
  1. package/components/tryghost-api-framework-0.0.0.tgz +0 -0
  2. package/components/tryghost-domain-events-0.0.0.tgz +0 -0
  3. package/components/tryghost-email-analytics-provider-mailgun-0.0.0.tgz +0 -0
  4. package/components/tryghost-email-content-generator-0.0.0.tgz +0 -0
  5. package/components/tryghost-express-dynamic-redirects-0.0.0.tgz +0 -0
  6. package/components/tryghost-job-manager-0.0.0.tgz +0 -0
  7. package/components/tryghost-magic-link-0.0.0.tgz +0 -0
  8. package/components/tryghost-mailgun-client-0.0.0.tgz +0 -0
  9. package/components/tryghost-member-attribution-0.0.0.tgz +0 -0
  10. package/components/tryghost-member-events-0.0.0.tgz +0 -0
  11. package/components/tryghost-members-api-0.0.0.tgz +0 -0
  12. package/components/tryghost-members-events-service-0.0.0.tgz +0 -0
  13. package/components/tryghost-members-importer-0.0.0.tgz +0 -0
  14. package/components/tryghost-members-offers-0.0.0.tgz +0 -0
  15. package/components/tryghost-members-ssr-0.0.0.tgz +0 -0
  16. package/components/tryghost-members-stripe-service-0.0.0.tgz +0 -0
  17. package/components/tryghost-mw-api-version-mismatch-0.0.0.tgz +0 -0
  18. package/components/tryghost-oembed-service-0.0.0.tgz +0 -0
  19. package/components/tryghost-security-0.0.0.tgz +0 -0
  20. package/components/tryghost-settings-path-manager-0.0.0.tgz +0 -0
  21. package/components/tryghost-update-check-service-0.0.0.tgz +0 -0
  22. package/components/tryghost-verification-trigger-0.0.0.tgz +0 -0
  23. package/components/tryghost-version-notifications-data-service-0.0.0.tgz +0 -0
  24. package/content/themes/casper/assets/built/screen.css +1 -1
  25. package/content/themes/casper/assets/built/screen.css.map +1 -1
  26. package/content/themes/casper/assets/css/screen.css +8 -5
  27. package/content/themes/casper/package.json +1 -1
  28. package/core/boot.js +2 -0
  29. package/core/bridge.js +2 -0
  30. package/core/built/admin/assets/{chunk.143.1c158e8ef19f10e5439c.js → chunk.143.6a3c46a89c731b86a730.js} +6 -6
  31. package/core/built/admin/assets/{chunk.174.eec7f6398cef4c3e2485.js → chunk.174.0364e8abdae8210d8e6d.js} +31 -29
  32. package/core/built/admin/assets/{chunk.178.506264293194a4922091.js → chunk.178.8a19c35ce1a7cf4249ce.js} +4 -4
  33. package/core/built/admin/assets/{chunk.351.73f27952f867334a8228.js → chunk.351.ea4a4ff4b40d5f2ad141.js} +22 -19
  34. package/core/built/admin/assets/{chunk.351.73f27952f867334a8228.js.LICENSE.txt → chunk.351.ea4a4ff4b40d5f2ad141.js.LICENSE.txt} +0 -0
  35. package/core/built/admin/assets/{ghost-facfdf4a7d9759c5b681340805f21fd8.css → ghost-13baab17b3f54b21f341fb8f36f83110.css} +1 -1
  36. package/core/built/admin/assets/{ghost-b441c9cfa2e31453e86460e50ae7e378.js → ghost-ced03a7ac75c3148e0ea7d1bf51e39fc.js} +319 -282
  37. package/core/built/admin/assets/{ghost-dark-4080c8f100997d4b8947f5da0e7946a1.css → ghost-dark-b0500577a42e2770994e6aef0e70f182.css} +1 -1
  38. package/core/built/admin/assets/icons/ghost-orb-pink.svg +10 -0
  39. package/core/built/admin/assets/img/logos/orb-pink-3-a2c52eb9fda9f2401ea706c3f24976ff.png +0 -0
  40. package/core/built/admin/assets/{vendor-516c9e43b4aeb92079dc1ab92c9ce492.js → vendor-a1ae7a38d5c38fcba5609eed4e37f02a.js} +73 -70
  41. package/core/built/admin/index.html +6 -6
  42. package/core/frontend/helpers/ghost_head.js +4 -0
  43. package/core/frontend/helpers/search.js +42 -0
  44. package/core/frontend/services/member-attribution-assets/index.js +4 -0
  45. package/core/frontend/services/member-attribution-assets/service.js +83 -0
  46. package/core/frontend/src/member-attribution/.eslintrc +10 -0
  47. package/core/frontend/src/member-attribution/member-attribution.js +90 -0
  48. package/core/frontend/web/site.js +3 -0
  49. package/core/server/adapters/cache/ImageSizesCacheSyncInMemory.js +7 -0
  50. package/core/server/adapters/cache/SettingsCacheSyncInMemory.js +7 -0
  51. package/core/server/api/endpoints/posts.js +10 -1
  52. package/core/server/api/endpoints/utils/serializers/input/posts.js +1 -1
  53. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +5 -0
  54. package/core/server/data/exporter/table-lists.js +2 -0
  55. package/core/server/data/migrations/versions/5.10/2022-08-15-05-34-add-expiry-at-column-to-members-products.js +6 -0
  56. package/core/server/data/migrations/versions/5.10/2022-08-16-14-25-add-member-created-events-table.js +11 -0
  57. package/core/server/data/migrations/versions/5.10/2022-08-16-14-25-add-subscription-created-events-table.js +11 -0
  58. package/core/server/data/migrations/versions/5.10/2022-08-19-14-15-fix-comments-deletion-strategy.js +45 -0
  59. package/core/server/data/schema/schema.js +21 -2
  60. package/core/server/lib/image/cached-image-size-from-url.js +52 -28
  61. package/core/server/lib/image/image-utils.js +5 -2
  62. package/core/server/lib/image/index.js +14 -1
  63. package/core/server/models/api-key.js +3 -18
  64. package/core/server/models/base/plugins/actions.js +32 -0
  65. package/core/server/models/integration.js +3 -0
  66. package/core/server/models/label.js +3 -18
  67. package/core/server/models/member-created-event.js +26 -0
  68. package/core/server/models/member.js +36 -4
  69. package/core/server/models/post.js +26 -18
  70. package/core/server/models/subscription-created-event.js +30 -0
  71. package/core/server/models/tag.js +3 -18
  72. package/core/server/models/user.js +3 -18
  73. package/core/server/models/webhook.js +3 -0
  74. package/core/server/services/comments/emails.js +3 -3
  75. package/core/server/services/explore/service.js +3 -2
  76. package/core/server/services/member-attribution/index.js +24 -0
  77. package/core/server/services/members/api.js +3 -1
  78. package/core/server/services/members/jobs/clean-expired-comped.js +105 -0
  79. package/core/server/services/members/jobs/index.js +27 -0
  80. package/core/server/services/members/service.js +14 -8
  81. package/core/server/services/settings/settings-service.js +1 -1
  82. package/core/shared/config/defaults.json +6 -2
  83. package/core/shared/labs.js +4 -1
  84. package/package.json +6 -5
  85. 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
- qb.columns('products.*');
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
- return ghostBookshelf.Model.add.call(this, data, unfilteredOptions);
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
- return ghostBookshelf.Model.edit.call(this, data, unfilteredOptions);
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() {
@@ -7,6 +7,9 @@ let Webhooks;
7
7
  Webhook = ghostBookshelf.Model.extend({
8
8
  tableName: 'webhooks',
9
9
 
10
+ actionsCollectCRUD: true,
11
+ actionsResourceType: 'webhook',
12
+
10
13
  defaults() {
11
14
  return {
12
15
  api_version: `v${ghostVersion.safe}`,
@@ -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: `${this.urlUtils.getAdminUrl()}ghost/#/settings/staff/${author.get('slug')}`
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: `${this.urlUtils.getAdminUrl()}ghost/#/settings/staff/${owner.get('slug')}`
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
- const membersMigrationJobName = 'members-migrations';
152
- if (!(await jobsService.hasExecutedSuccessfully(membersMigrationJobName))) {
153
- jobsService.addOneOffJob({
154
- name: membersMigrationJobName,
155
- offloaded: false,
156
- job: stripeService.migrations.execute.bind(stripeService.migrations)
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
- await jobsService.awaitCompletion(membersMigrationJobName);
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.8"
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",
@@ -28,7 +28,10 @@ const ALPHA_FEATURES = [
28
28
  'auditLog',
29
29
  'urlCache',
30
30
  'beforeAfterCard',
31
- 'freeTrial'
31
+ 'freeTrial',
32
+ 'memberAttribution',
33
+ 'searchHelper',
34
+ 'compExpiring'
32
35
  ];
33
36
 
34
37
  module.exports.GA_KEYS = [...GA_FEATURES];