ghost 5.116.2 → 5.118.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 (131) hide show
  1. package/components/{tryghost-api-framework-5.116.2.tgz → tryghost-api-framework-5.118.0.tgz} +0 -0
  2. package/components/tryghost-constants-5.118.0.tgz +0 -0
  3. package/components/tryghost-custom-fonts-5.118.0.tgz +0 -0
  4. package/components/tryghost-custom-theme-settings-service-5.118.0.tgz +0 -0
  5. package/components/tryghost-domain-events-5.118.0.tgz +0 -0
  6. package/components/tryghost-donations-5.118.0.tgz +0 -0
  7. package/components/tryghost-email-addresses-5.118.0.tgz +0 -0
  8. package/components/{tryghost-email-service-5.116.2.tgz → tryghost-email-service-5.118.0.tgz} +0 -0
  9. package/components/tryghost-email-suppression-list-5.118.0.tgz +0 -0
  10. package/components/tryghost-html-to-plaintext-5.118.0.tgz +0 -0
  11. package/components/tryghost-i18n-5.118.0.tgz +0 -0
  12. package/components/{tryghost-job-manager-5.116.2.tgz → tryghost-job-manager-5.118.0.tgz} +0 -0
  13. package/components/tryghost-link-replacer-5.118.0.tgz +0 -0
  14. package/components/{tryghost-magic-link-5.116.2.tgz → tryghost-magic-link-5.118.0.tgz} +0 -0
  15. package/components/{tryghost-member-attribution-5.116.2.tgz → tryghost-member-attribution-5.118.0.tgz} +0 -0
  16. package/components/tryghost-member-events-5.118.0.tgz +0 -0
  17. package/components/{tryghost-members-csv-5.116.2.tgz → tryghost-members-csv-5.118.0.tgz} +0 -0
  18. package/components/{tryghost-members-offers-5.116.2.tgz → tryghost-members-offers-5.118.0.tgz} +0 -0
  19. package/components/tryghost-mw-error-handler-5.118.0.tgz +0 -0
  20. package/components/tryghost-mw-vhost-5.118.0.tgz +0 -0
  21. package/components/{tryghost-post-events-5.116.2.tgz → tryghost-post-events-5.118.0.tgz} +0 -0
  22. package/components/tryghost-post-revisions-5.118.0.tgz +0 -0
  23. package/components/tryghost-posts-service-5.118.0.tgz +0 -0
  24. package/components/tryghost-prometheus-metrics-5.118.0.tgz +0 -0
  25. package/components/tryghost-security-5.118.0.tgz +0 -0
  26. package/components/tryghost-tiers-5.118.0.tgz +0 -0
  27. package/components/tryghost-webmentions-5.118.0.tgz +0 -0
  28. package/content/themes/casper/LICENSE +1 -1
  29. package/content/themes/casper/README.md +1 -1
  30. package/content/themes/casper/assets/built/screen.css +1 -1
  31. package/content/themes/casper/assets/built/screen.css.map +1 -1
  32. package/content/themes/casper/assets/css/screen.css +1 -1
  33. package/content/themes/casper/author.hbs +23 -2
  34. package/content/themes/casper/package.json +2 -2
  35. package/content/themes/casper/partials/icons/bluesky.hbs +3 -0
  36. package/content/themes/casper/partials/icons/instagram.hbs +5 -0
  37. package/content/themes/casper/partials/icons/linkedin.hbs +3 -0
  38. package/content/themes/casper/partials/icons/mastodon.hbs +3 -0
  39. package/content/themes/casper/partials/icons/threads.hbs +3 -0
  40. package/content/themes/casper/partials/icons/tiktok.hbs +3 -0
  41. package/content/themes/casper/partials/icons/twitter.hbs +3 -1
  42. package/content/themes/casper/partials/icons/youtube.hbs +3 -0
  43. package/content/themes/source/LICENSE +1 -1
  44. package/content/themes/source/README.md +1 -1
  45. package/content/themes/source/assets/built/screen.css +1 -1
  46. package/content/themes/source/assets/built/screen.css.map +1 -1
  47. package/content/themes/source/assets/css/screen.css +7 -12
  48. package/content/themes/source/author.hbs +24 -3
  49. package/content/themes/source/package.json +2 -2
  50. package/content/themes/source/partials/feature-image.hbs +2 -2
  51. package/content/themes/source/partials/icons/bluesky.hbs +3 -0
  52. package/content/themes/source/partials/icons/instagram.hbs +5 -0
  53. package/content/themes/source/partials/icons/linkedin.hbs +3 -0
  54. package/content/themes/source/partials/icons/mastodon.hbs +3 -0
  55. package/content/themes/source/partials/icons/threads.hbs +3 -0
  56. package/content/themes/source/partials/icons/tiktok.hbs +3 -0
  57. package/content/themes/source/partials/icons/youtube.hbs +3 -0
  58. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +31793 -26588
  59. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-550846e0.mjs → CodeEditorView-1143c509.mjs} +2 -2
  60. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  61. package/core/built/admin/assets/admin-x-settings/{index-f3cb3f4d.mjs → index-19ebc8ad.mjs} +2 -2
  62. package/core/built/admin/assets/admin-x-settings/{index-4ce2fcd1.mjs → index-ac104f42.mjs} +2635 -2607
  63. package/core/built/admin/assets/admin-x-settings/{modals-6bc20529.mjs → modals-994901ee.mjs} +6680 -6165
  64. package/core/built/admin/assets/{chunk.524.578de86e5014b911b05a.js → chunk.524.5710919eb507b9a81166.js} +8 -8
  65. package/core/built/admin/assets/{chunk.582.21bf3e37b5d84ac4b58a.js → chunk.582.c8cb99b85cfa13fc7df1.js} +10 -10
  66. package/core/built/admin/assets/{chunk.713.761d11035fe0bf3e557c.js → chunk.713.48f120c377bcaffdfddf.js} +6 -9
  67. package/core/built/admin/assets/{ghost-868c537d5c02ca65323d0122596a67ec.js → ghost-cd90a28b214ee800a007bb62cd45e6e6.js} +780 -775
  68. package/core/built/admin/assets/posts/posts.js +11561 -11302
  69. package/core/built/admin/assets/stats/stats.js +76076 -59355
  70. package/core/built/admin/index.html +4 -4
  71. package/core/frontend/helpers/social_url.js +31 -0
  72. package/core/server/api/endpoints/users.js +7 -0
  73. package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
  74. package/core/server/data/migrations/versions/5.117/2025-04-14-02-36-30-add-additional-social-accounts-columns-to-user-table.js +38 -0
  75. package/core/server/data/schema/schema.js +7 -0
  76. package/core/server/services/auth/session/index.js +5 -2
  77. package/core/server/services/auth/session/middleware.js +2 -1
  78. package/core/server/services/auth/session/session-service.js +7 -6
  79. package/core/server/services/members/api.js +2 -2
  80. package/core/server/services/members/members-api/controllers/MemberController.js +214 -0
  81. package/core/server/services/members/members-api/controllers/RouterController.js +667 -0
  82. package/core/server/services/members/members-api/controllers/WellKnownController.js +46 -0
  83. package/core/server/services/members/members-api/members-api.js +404 -0
  84. package/core/server/services/members/members-api/repositories/EventRepository.js +984 -0
  85. package/core/server/services/members/members-api/repositories/MemberRepository.js +1739 -0
  86. package/core/server/services/members/members-api/repositories/ProductRepository.js +662 -0
  87. package/core/server/services/members/members-api/services/GeolocationService.js +23 -0
  88. package/core/server/services/members/members-api/services/MemberBREADService.js +444 -0
  89. package/core/server/services/members/members-api/services/PaymentsService.js +522 -0
  90. package/core/server/services/members/members-api/services/TokenService.js +54 -0
  91. package/core/server/services/milestones/BookshelfMilestoneRepository.js +8 -9
  92. package/core/server/services/milestones/InMemoryMilestoneRepository.js +119 -0
  93. package/core/server/services/milestones/Milestone.js +231 -0
  94. package/core/server/services/milestones/MilestoneCreatedEvent.js +22 -0
  95. package/core/server/services/milestones/MilestonesService.js +327 -0
  96. package/core/server/services/milestones/service.js +2 -2
  97. package/core/server/services/newsletters/index.js +1 -1
  98. package/core/server/services/public-config/config.js +2 -1
  99. package/core/server/services/settings/settings-service.js +1 -1
  100. package/core/server/services/slack-notifications/SlackNotifications.js +1 -1
  101. package/core/server/services/slack-notifications/SlackNotificationsService.js +2 -2
  102. package/core/server/services/staff/StaffService.js +1 -1
  103. package/core/shared/config/defaults.json +3 -0
  104. package/core/shared/config/env/config.testing-mysql.json +3 -0
  105. package/core/shared/config/env/config.testing.json +3 -0
  106. package/core/shared/labs.js +2 -2
  107. package/package.json +63 -63
  108. package/tsconfig.tsbuildinfo +1 -1
  109. package/yarn.lock +306 -70
  110. package/components/tryghost-constants-5.116.2.tgz +0 -0
  111. package/components/tryghost-custom-fonts-5.116.2.tgz +0 -0
  112. package/components/tryghost-custom-theme-settings-service-5.116.2.tgz +0 -0
  113. package/components/tryghost-domain-events-5.116.2.tgz +0 -0
  114. package/components/tryghost-donations-5.116.2.tgz +0 -0
  115. package/components/tryghost-email-addresses-5.116.2.tgz +0 -0
  116. package/components/tryghost-email-suppression-list-5.116.2.tgz +0 -0
  117. package/components/tryghost-html-to-plaintext-5.116.2.tgz +0 -0
  118. package/components/tryghost-i18n-5.116.2.tgz +0 -0
  119. package/components/tryghost-link-replacer-5.116.2.tgz +0 -0
  120. package/components/tryghost-member-events-5.116.2.tgz +0 -0
  121. package/components/tryghost-members-api-5.116.2.tgz +0 -0
  122. package/components/tryghost-milestones-5.116.2.tgz +0 -0
  123. package/components/tryghost-mw-error-handler-5.116.2.tgz +0 -0
  124. package/components/tryghost-mw-vhost-5.116.2.tgz +0 -0
  125. package/components/tryghost-post-revisions-5.116.2.tgz +0 -0
  126. package/components/tryghost-posts-service-5.116.2.tgz +0 -0
  127. package/components/tryghost-prometheus-metrics-5.116.2.tgz +0 -0
  128. package/components/tryghost-security-5.116.2.tgz +0 -0
  129. package/components/tryghost-tiers-5.116.2.tgz +0 -0
  130. package/components/tryghost-webmentions-5.116.2.tgz +0 -0
  131. /package/core/built/admin/assets/{chunk.713.761d11035fe0bf3e557c.js.LICENSE.txt → chunk.713.48f120c377bcaffdfddf.js.LICENSE.txt} +0 -0
@@ -0,0 +1,444 @@
1
+ const errors = require('@tryghost/errors');
2
+ const logging = require('@tryghost/logging');
3
+ const tpl = require('@tryghost/tpl');
4
+ const moment = require('moment');
5
+
6
+ const messages = {
7
+ stripeNotConnected: 'Missing Stripe connection.',
8
+ memberAlreadyExists: 'Member already exists.'
9
+ };
10
+
11
+ /**
12
+ * @typedef {object} ILabsService
13
+ * @prop {(key: string) => boolean} isSet
14
+ */
15
+
16
+ /**
17
+ * @typedef {object} IEmailService
18
+ * @prop {(data: {email: string, requestedType: string}) => Promise<any>} sendEmailWithMagicLink
19
+ */
20
+
21
+ /**
22
+ * @typedef {object} IStripeService
23
+ * @prop {boolean} configured
24
+ */
25
+
26
+ /**
27
+ * @typedef {import('@tryghost/members-offers/lib/application/OfferMapper').OfferDTO} OfferDTO
28
+ */
29
+
30
+ module.exports = class MemberBREADService {
31
+ /**
32
+ * @param {object} deps
33
+ * @param {import('../repositories/MemberRepository')} deps.memberRepository
34
+ * @param {import('@tryghost/members-offers/lib/application/OffersAPI')} deps.offersAPI
35
+ * @param {ILabsService} deps.labsService
36
+ * @param {IEmailService} deps.emailService
37
+ * @param {IStripeService} deps.stripeService
38
+ * @param {import('@tryghost/member-attribution/lib/service')} deps.memberAttributionService
39
+ * @param {import('@tryghost/email-suppression-list/lib/email-suppression-list').IEmailSuppressionList} deps.emailSuppressionList
40
+ * @param {import('@tryghost/settings-helpers')} deps.settingsHelpers
41
+ */
42
+ constructor({memberRepository, labsService, emailService, stripeService, offersAPI, memberAttributionService, emailSuppressionList, settingsHelpers}) {
43
+ this.offersAPI = offersAPI;
44
+ /** @private */
45
+ this.memberRepository = memberRepository;
46
+ /** @private */
47
+ this.labsService = labsService;
48
+ /** @private */
49
+ this.emailService = emailService;
50
+ /** @private */
51
+ this.stripeService = stripeService;
52
+ /** @private */
53
+ this.memberAttributionService = memberAttributionService;
54
+ /** @private */
55
+ this.emailSuppressionList = emailSuppressionList;
56
+ /** @private */
57
+ this.settingsHelpers = settingsHelpers;
58
+ }
59
+
60
+ /**
61
+ * @private
62
+ * Adds missing complimentary subscriptions to a member and makes sure the tier of all subscriptions is set correctly.
63
+ */
64
+ attachSubscriptionsToMember(member) {
65
+ if (!member.products || !Array.isArray(member.products)) {
66
+ return member;
67
+ }
68
+
69
+ const subscriptionProducts = (member.subscriptions || [])
70
+ .filter(sub => this.memberRepository.isActiveSubscriptionStatus(sub.status))
71
+ .map(sub => sub.price.product.product_id);
72
+
73
+ // Remove incomplete subscriptions from the API
74
+ member.subscriptions = member.subscriptions.filter(sub => sub.status !== 'incomplete' && sub.status !== 'incomplete_expired');
75
+
76
+ for (const product of member.products) {
77
+ if (!subscriptionProducts.includes(product.id)) {
78
+ const productAddEvent = member.productEvents.find(event => event.product_id === product.id);
79
+ let startDate;
80
+ if (!productAddEvent || productAddEvent.action !== 'added') {
81
+ startDate = moment();
82
+ } else {
83
+ startDate = moment(productAddEvent.created_at);
84
+ }
85
+ member.subscriptions.push({
86
+ id: '',
87
+ tier: product,
88
+ customer: {
89
+ id: '',
90
+ name: member.name,
91
+ email: member.email
92
+ },
93
+ plan: {
94
+ id: '',
95
+ nickname: 'Complimentary',
96
+ interval: 'year',
97
+ currency: 'USD',
98
+ amount: 0
99
+ },
100
+ status: 'active',
101
+ start_date: startDate,
102
+ default_payment_card_last4: '****',
103
+ cancel_at_period_end: false,
104
+ cancellation_reason: null,
105
+ current_period_end: moment(product.expiry_at),
106
+ price: {
107
+ id: '',
108
+ price_id: '',
109
+ nickname: 'Complimentary',
110
+ amount: 0,
111
+ interval: 'year',
112
+ type: 'recurring',
113
+ currency: 'USD',
114
+ product: {
115
+ id: '',
116
+ product_id: product.id
117
+ }
118
+ }
119
+ });
120
+ }
121
+ }
122
+
123
+ for (const subscription of member.subscriptions) {
124
+ if (!subscription.tier) {
125
+ subscription.tier = member.products.find(product => product.id === subscription.price.product.product_id);
126
+ }
127
+ }
128
+ }
129
+
130
+ /**
131
+ * @private Builds a map between subscriptions and their offer representation (from OfferMapper)
132
+ * @returns {Promise<Map<string, OfferDTO>>}
133
+ */
134
+ async fetchSubscriptionOffers(subscriptions) {
135
+ const fetchedOffers = new Map();
136
+ const subscriptionOffers = new Map();
137
+
138
+ try {
139
+ for (const subscriptionModel of subscriptions) {
140
+ const offerId = subscriptionModel.get('offer_id');
141
+
142
+ if (!offerId) {
143
+ continue;
144
+ }
145
+
146
+ let offer = fetchedOffers.get(offerId);
147
+ if (!offer) {
148
+ offer = await this.offersAPI.getOffer({id: offerId});
149
+ fetchedOffers.set(offerId, offer);
150
+ }
151
+
152
+ subscriptionOffers.set(subscriptionModel.get('subscription_id'), offer);
153
+ }
154
+ } catch (e) {
155
+ logging.error(`Failed to load offers for subscriptions - ${subscriptions.map(s => s.id).join(', ')}.`);
156
+ logging.error(e);
157
+ }
158
+
159
+ return subscriptionOffers;
160
+ }
161
+
162
+ /**
163
+ * @private
164
+ * @param {Object} member JSON serialized member
165
+ * @param {Map<string, OfferDTO>} subscriptionOffers result from fetchSubscriptionOffers
166
+ */
167
+ attachOffersToSubscriptions(member, subscriptionOffers) {
168
+ member.subscriptions = member.subscriptions.map((subscription) => {
169
+ const offer = subscriptionOffers.get(subscription.id);
170
+ if (offer) {
171
+ subscription.offer = offer;
172
+ } else {
173
+ subscription.offer = null;
174
+ }
175
+ return subscription;
176
+ });
177
+ }
178
+
179
+ /**
180
+ * @private
181
+ * Adds missing complimentary subscriptions to a member and makes sure the tier of all subscriptions is set correctly.
182
+ */
183
+ async attachAttributionsToMember(member, subscriptionIdMap) {
184
+ // Created attribution
185
+ member.attribution = await this.memberAttributionService.getMemberCreatedAttribution(member.id);
186
+
187
+ // Subscriptions attributions
188
+ for (const subscription of member.subscriptions) {
189
+ if (!subscription.id) {
190
+ continue;
191
+ }
192
+
193
+ // Convert stripe ID to database id
194
+ const id = subscriptionIdMap.get(subscription.id);
195
+ if (!id) {
196
+ continue;
197
+ }
198
+ subscription.attribution = await this.memberAttributionService.getSubscriptionCreatedAttribution(id);
199
+ }
200
+ }
201
+
202
+ async read(data, options = {}) {
203
+ const defaultWithRelated = [
204
+ 'labels',
205
+ 'stripeSubscriptions',
206
+ 'stripeSubscriptions.customer',
207
+ 'stripeSubscriptions.stripePrice',
208
+ 'stripeSubscriptions.stripePrice.stripeProduct',
209
+ 'stripeSubscriptions.stripePrice.stripeProduct.product',
210
+ 'products',
211
+ 'newsletters'
212
+ ];
213
+
214
+ const withRelated = new Set((options.withRelated || []).concat(defaultWithRelated));
215
+
216
+ if (!withRelated.has('productEvents')) {
217
+ withRelated.add('productEvents');
218
+ }
219
+
220
+ if (withRelated.has('email_recipients')) {
221
+ withRelated.add('email_recipients.email');
222
+ }
223
+
224
+ const model = await this.memberRepository.get(data, {
225
+ ...options,
226
+ withRelated: Array.from(withRelated)
227
+ });
228
+
229
+ if (!model) {
230
+ return null;
231
+ }
232
+
233
+ // We need to know the real IDs for each subscription to fetch the member attribution
234
+ const subscriptionIdMap = new Map();
235
+ for (const subscription of model.related('stripeSubscriptions')) {
236
+ subscriptionIdMap.set(subscription.get('subscription_id'), subscription.id);
237
+ }
238
+
239
+ const member = model.toJSON(options);
240
+
241
+ member.subscriptions = member.subscriptions.filter(sub => !!sub.price);
242
+ this.attachSubscriptionsToMember(member);
243
+ this.attachOffersToSubscriptions(member, await this.fetchSubscriptionOffers(model.related('stripeSubscriptions')));
244
+ await this.attachAttributionsToMember(member, subscriptionIdMap);
245
+
246
+ const suppressionData = await this.emailSuppressionList.getSuppressionData(member.email);
247
+ member.email_suppression = {
248
+ suppressed: suppressionData.suppressed || !!model.get('email_disabled'),
249
+ info: suppressionData.info
250
+ };
251
+
252
+ const unsubscribeUrl = this.settingsHelpers.createUnsubscribeUrl(member.uuid);
253
+ member.unsubscribe_url = unsubscribeUrl;
254
+
255
+ return member;
256
+ }
257
+
258
+ async add(data, options) {
259
+ if (!this.stripeService.configured && (data.comped || data.stripe_customer_id)) {
260
+ const property = data.comped ? 'comped' : 'stripe_customer_id';
261
+ throw new errors.ValidationError({
262
+ message: tpl(messages.stripeNotConnected),
263
+ context: 'Attempting to import members with Stripe data when there is no Stripe account connected.',
264
+ help: 'You need to connect to Stripe to import Stripe customers. ',
265
+ property
266
+ });
267
+ }
268
+
269
+ let model;
270
+
271
+ try {
272
+ const attribution = await this.memberAttributionService.getAttributionFromContext(options?.context);
273
+ if (attribution) {
274
+ data.attribution = attribution;
275
+ }
276
+ model = await this.memberRepository.create(data, options);
277
+ } catch (error) {
278
+ if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
279
+ throw new errors.ValidationError({
280
+ message: tpl(messages.memberAlreadyExists),
281
+ context: 'Attempting to add member with existing email address',
282
+ property: 'email'
283
+ });
284
+ }
285
+ throw error;
286
+ }
287
+
288
+ const sharedOptions = options.transacting ? {
289
+ transacting: options.transacting
290
+ } : {};
291
+
292
+ try {
293
+ if (data.stripe_customer_id) {
294
+ await this.memberRepository.linkStripeCustomer({
295
+ customer_id: data.stripe_customer_id,
296
+ member_id: model.id
297
+ }, sharedOptions);
298
+ }
299
+ } catch (error) {
300
+ const isStripeLinkingError = error.message && (error.message.match(/customer|plan|subscription/g));
301
+ if (isStripeLinkingError) {
302
+ if (error.message.indexOf('customer') && error.code === 'resource_missing') {
303
+ error.message = `Member not imported. ${error.message}`;
304
+ error.context = 'Missing Stripe Customer';
305
+ error.help = 'Make sure you\'re connected to the correct Stripe Account';
306
+ }
307
+
308
+ await this.memberRepository.destroy({
309
+ id: model.id
310
+ }, options);
311
+ }
312
+ throw error;
313
+ }
314
+
315
+ if (options.send_email) {
316
+ await this.emailService.sendEmailWithMagicLink({
317
+ email: model.get('email'), requestedType: options.email_type
318
+ });
319
+ }
320
+
321
+ if (data.comped) {
322
+ await this.memberRepository.setComplimentarySubscription(model, options);
323
+ }
324
+
325
+ return this.read({id: model.id}, options);
326
+ }
327
+
328
+ async edit(data, options) {
329
+ delete data.last_seen_at;
330
+
331
+ let model;
332
+
333
+ try {
334
+ // Update email_disabled based on whether the new email is suppressed
335
+ if (data.email) {
336
+ const isSuppressed = (await this.emailSuppressionList.getSuppressionData(data.email))?.suppressed;
337
+ data.email_disabled = !!isSuppressed;
338
+ }
339
+
340
+ model = await this.memberRepository.update(data, options);
341
+ } catch (error) {
342
+ if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
343
+ throw new errors.ValidationError({
344
+ message: tpl(messages.memberAlreadyExists),
345
+ context: 'Attempting to edit member with existing email address',
346
+ property: 'email'
347
+ });
348
+ }
349
+
350
+ throw error;
351
+ }
352
+
353
+ if (this.stripeService.configured) {
354
+ const hasCompedSubscription = !!model.related('stripeSubscriptions').find(sub => sub.get('plan_nickname') === 'Complimentary' && sub.get('status') === 'active');
355
+
356
+ if (typeof data.comped === 'boolean') {
357
+ if (data.comped && !hasCompedSubscription) {
358
+ await this.memberRepository.setComplimentarySubscription(model, {
359
+ context: options.context,
360
+ transacting: options.transacting
361
+ });
362
+ } else if (!(data.comped) && hasCompedSubscription) {
363
+ await this.memberRepository.cancelComplimentarySubscription(model, {
364
+ context: options.context,
365
+ transacting: options.transacting
366
+ });
367
+ }
368
+ }
369
+ }
370
+
371
+ return this.read({id: model.id}, options);
372
+ }
373
+
374
+ async logout(options) {
375
+ await this.memberRepository.cycleTransientId(options);
376
+ }
377
+
378
+ async browse(options) {
379
+ const defaultWithRelated = [
380
+ 'labels',
381
+ 'stripeSubscriptions',
382
+ 'stripeSubscriptions.customer',
383
+ 'stripeSubscriptions.stripePrice',
384
+ 'stripeSubscriptions.stripePrice.stripeProduct',
385
+ 'stripeSubscriptions.stripePrice.stripeProduct.product',
386
+ 'products',
387
+ 'newsletters'
388
+ ];
389
+
390
+ if (options.limit === 'all' || options.limit > 100) {
391
+ options.limit = 100;
392
+ }
393
+
394
+ const originalWithRelated = options.withRelated || [];
395
+
396
+ const withRelated = new Set((originalWithRelated).concat(defaultWithRelated));
397
+
398
+ if (!withRelated.has('productEvents')) {
399
+ withRelated.add('productEvents');
400
+ }
401
+
402
+ if (withRelated.has('email_recipients')) {
403
+ withRelated.add('email_recipients.email');
404
+ }
405
+
406
+ //option param to skip distinct from count query, distinct adds a lot of latency and in this case the result set will always be unique.
407
+ options.useBasicCount = true;
408
+
409
+ const page = await this.memberRepository.list({
410
+ ...options,
411
+ withRelated: Array.from(withRelated)
412
+ });
413
+
414
+ if (!page) {
415
+ return null;
416
+ }
417
+
418
+ const subscriptions = page.data.flatMap(model => model.related('stripeSubscriptions').slice());
419
+ const offerMap = await this.fetchSubscriptionOffers(subscriptions);
420
+
421
+ const bulkSuppressionData = await this.emailSuppressionList.getBulkSuppressionData(page.data.map(member => member.get('email')));
422
+
423
+ const data = page.data.map((model, index) => {
424
+ const member = model.toJSON(options);
425
+ member.subscriptions = member.subscriptions.filter(sub => !!sub.price);
426
+ this.attachSubscriptionsToMember(member);
427
+ this.attachOffersToSubscriptions(member, offerMap);
428
+ if (!originalWithRelated.includes('products')) {
429
+ delete member.products;
430
+ }
431
+ member.email_suppression = {
432
+ suppressed: bulkSuppressionData[index].suppressed || !!model.get('email_disabled'),
433
+ info: bulkSuppressionData[index].info
434
+ };
435
+ member.unsubscribe_url = this.settingsHelpers.createUnsubscribeUrl(member.uuid);
436
+ return member;
437
+ });
438
+
439
+ return {
440
+ data,
441
+ meta: page.meta
442
+ };
443
+ }
444
+ };