ghost 5.110.4 → 5.112.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 (202) hide show
  1. package/components/tryghost-adapter-cache-memory-ttl-5.112.0.tgz +0 -0
  2. package/components/tryghost-adapter-cache-redis-5.112.0.tgz +0 -0
  3. package/components/{tryghost-adapter-manager-5.110.4.tgz → tryghost-adapter-manager-5.112.0.tgz} +0 -0
  4. package/components/tryghost-announcement-bar-settings-5.112.0.tgz +0 -0
  5. package/components/{tryghost-api-framework-5.110.4.tgz → tryghost-api-framework-5.112.0.tgz} +0 -0
  6. package/components/tryghost-api-version-compatibility-service-5.112.0.tgz +0 -0
  7. package/components/{tryghost-audience-feedback-5.110.4.tgz → tryghost-audience-feedback-5.112.0.tgz} +0 -0
  8. package/components/tryghost-bookshelf-repository-5.112.0.tgz +0 -0
  9. package/components/tryghost-bootstrap-socket-5.112.0.tgz +0 -0
  10. package/components/tryghost-captcha-service-5.112.0.tgz +0 -0
  11. package/components/tryghost-constants-5.112.0.tgz +0 -0
  12. package/components/tryghost-custom-fonts-5.112.0.tgz +0 -0
  13. package/components/{tryghost-custom-theme-settings-service-5.110.4.tgz → tryghost-custom-theme-settings-service-5.112.0.tgz} +0 -0
  14. package/components/{tryghost-data-generator-5.110.4.tgz → tryghost-data-generator-5.112.0.tgz} +0 -0
  15. package/components/{tryghost-domain-events-5.110.4.tgz → tryghost-domain-events-5.112.0.tgz} +0 -0
  16. package/components/tryghost-donations-5.112.0.tgz +0 -0
  17. package/components/tryghost-email-addresses-5.112.0.tgz +0 -0
  18. package/components/tryghost-email-analytics-provider-mailgun-5.112.0.tgz +0 -0
  19. package/components/{tryghost-email-analytics-service-5.110.4.tgz → tryghost-email-analytics-service-5.112.0.tgz} +0 -0
  20. package/components/tryghost-email-content-generator-5.112.0.tgz +0 -0
  21. package/components/tryghost-email-events-5.112.0.tgz +0 -0
  22. package/components/tryghost-email-service-5.112.0.tgz +0 -0
  23. package/components/tryghost-email-suppression-list-5.112.0.tgz +0 -0
  24. package/components/{tryghost-express-dynamic-redirects-5.110.4.tgz → tryghost-express-dynamic-redirects-5.112.0.tgz} +0 -0
  25. package/components/{tryghost-external-media-inliner-5.110.4.tgz → tryghost-external-media-inliner-5.112.0.tgz} +0 -0
  26. package/components/tryghost-extract-api-key-5.112.0.tgz +0 -0
  27. package/components/tryghost-ghost-5.112.0.tgz +0 -0
  28. package/components/{tryghost-html-to-plaintext-5.110.4.tgz → tryghost-html-to-plaintext-5.112.0.tgz} +0 -0
  29. package/components/tryghost-i18n-5.112.0.tgz +0 -0
  30. package/components/tryghost-identity-token-service-5.112.0.tgz +0 -0
  31. package/components/tryghost-importer-handler-content-files-5.112.0.tgz +0 -0
  32. package/components/{tryghost-importer-revue-5.110.4.tgz → tryghost-importer-revue-5.112.0.tgz} +0 -0
  33. package/components/tryghost-in-memory-repository-5.112.0.tgz +0 -0
  34. package/components/{tryghost-job-manager-5.110.4.tgz → tryghost-job-manager-5.112.0.tgz} +0 -0
  35. package/components/{tryghost-link-redirects-5.110.4.tgz → tryghost-link-redirects-5.112.0.tgz} +0 -0
  36. package/components/tryghost-link-replacer-5.112.0.tgz +0 -0
  37. package/components/{tryghost-magic-link-5.110.4.tgz → tryghost-magic-link-5.112.0.tgz} +0 -0
  38. package/components/tryghost-mail-events-5.112.0.tgz +0 -0
  39. package/components/tryghost-mailgun-client-5.112.0.tgz +0 -0
  40. package/components/{tryghost-member-attribution-5.110.4.tgz → tryghost-member-attribution-5.112.0.tgz} +0 -0
  41. package/components/{tryghost-member-events-5.110.4.tgz → tryghost-member-events-5.112.0.tgz} +0 -0
  42. package/components/{tryghost-members-api-5.110.4.tgz → tryghost-members-api-5.112.0.tgz} +0 -0
  43. package/components/{tryghost-members-csv-5.110.4.tgz → tryghost-members-csv-5.112.0.tgz} +0 -0
  44. package/components/{tryghost-members-importer-5.110.4.tgz → tryghost-members-importer-5.112.0.tgz} +0 -0
  45. package/components/{tryghost-members-offers-5.110.4.tgz → tryghost-members-offers-5.112.0.tgz} +0 -0
  46. package/components/{tryghost-members-payments-5.110.4.tgz → tryghost-members-payments-5.112.0.tgz} +0 -0
  47. package/components/{tryghost-members-ssr-5.110.4.tgz → tryghost-members-ssr-5.112.0.tgz} +0 -0
  48. package/components/{tryghost-members-stripe-service-5.110.4.tgz → tryghost-members-stripe-service-5.112.0.tgz} +0 -0
  49. package/components/{tryghost-milestones-5.110.4.tgz → tryghost-milestones-5.112.0.tgz} +0 -0
  50. package/components/tryghost-minifier-5.112.0.tgz +0 -0
  51. package/components/tryghost-mw-api-version-mismatch-5.112.0.tgz +0 -0
  52. package/components/{tryghost-mw-cache-control-5.110.4.tgz → tryghost-mw-cache-control-5.112.0.tgz} +0 -0
  53. package/components/tryghost-mw-error-handler-5.112.0.tgz +0 -0
  54. package/components/{tryghost-mw-session-from-token-5.110.4.tgz → tryghost-mw-session-from-token-5.112.0.tgz} +0 -0
  55. package/components/{tryghost-mw-update-user-last-seen-5.110.4.tgz → tryghost-mw-update-user-last-seen-5.112.0.tgz} +0 -0
  56. package/components/tryghost-mw-version-match-5.112.0.tgz +0 -0
  57. package/components/tryghost-mw-vhost-5.112.0.tgz +0 -0
  58. package/components/tryghost-package-json-5.112.0.tgz +0 -0
  59. package/components/{tryghost-post-events-5.110.4.tgz → tryghost-post-events-5.112.0.tgz} +0 -0
  60. package/components/{tryghost-post-revisions-5.110.4.tgz → tryghost-post-revisions-5.112.0.tgz} +0 -0
  61. package/components/{tryghost-posts-service-5.110.4.tgz → tryghost-posts-service-5.112.0.tgz} +0 -0
  62. package/components/tryghost-prometheus-metrics-5.112.0.tgz +0 -0
  63. package/components/tryghost-recommendations-5.112.0.tgz +0 -0
  64. package/components/tryghost-referrers-5.112.0.tgz +0 -0
  65. package/components/{tryghost-security-5.110.4.tgz → tryghost-security-5.112.0.tgz} +0 -0
  66. package/components/tryghost-session-service-5.112.0.tgz +0 -0
  67. package/components/tryghost-settings-path-manager-5.112.0.tgz +0 -0
  68. package/components/{tryghost-slack-notifications-5.110.4.tgz → tryghost-slack-notifications-5.112.0.tgz} +0 -0
  69. package/components/tryghost-tiers-5.112.0.tgz +0 -0
  70. package/components/tryghost-version-notifications-data-service-5.112.0.tgz +0 -0
  71. package/components/tryghost-webmentions-5.112.0.tgz +0 -0
  72. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +12799 -10270
  73. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +2 -2
  74. package/core/built/admin/assets/admin-x-demo/{index-82e381fb.mjs → index-0040480a.mjs} +3252 -2891
  75. package/core/built/admin/assets/admin-x-demo/{modals-b20a9ede.mjs → modals-fb35c86c.mjs} +2 -2
  76. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-ea62c29b.mjs → CodeEditorView-ad8698fe.mjs} +624 -618
  77. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +3 -3
  78. package/core/built/admin/assets/admin-x-settings/{index-af8cf9cf.mjs → index-2713e469.mjs} +6892 -6469
  79. package/core/built/admin/assets/admin-x-settings/{index-4b25c788.mjs → index-463cec50.mjs} +2 -2
  80. package/core/built/admin/assets/admin-x-settings/{modals-cb2dc7b7.mjs → modals-033e8fc4.mjs} +7888 -7669
  81. package/core/built/admin/assets/{chunk.524.3096e68df5b51dacf872.js → chunk.524.db49da6fd8ae155205a4.js} +6 -6
  82. package/core/built/admin/assets/{chunk.582.e225422f90639ff30544.js → chunk.582.0bf715eb6807f7641706.js} +8 -8
  83. package/core/built/admin/assets/{ghost-98d002d50a5e01d2100b2c387a849249.js → ghost-62bd4d4c837d453e1038808dc1cd1e4c.js} +43 -42
  84. package/core/built/admin/assets/img/ap-nodes-01ee317529e6353a1c34a062c388f1e7.png +0 -0
  85. package/core/built/admin/assets/koenig-lexical/index.css +1 -1
  86. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +18314 -17680
  87. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +229 -200
  88. package/core/built/admin/assets/posts/posts.js +24137 -24156
  89. package/core/built/admin/index.html +3 -3
  90. package/core/frontend/helpers/get.js +2 -3
  91. package/core/frontend/services/sitemap/SiteMapManager.js +1 -1
  92. package/core/frontend/src/cards/css/cta.css +40 -30
  93. package/core/frontend/src/cards/css/video.css +1 -0
  94. package/core/server/api/endpoints/settings-public.js +3 -2
  95. package/core/server/api/endpoints/utils/serializers/input/settings.js +3 -1
  96. package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-group-mapper.js +2 -1
  97. package/core/server/api/endpoints/utils/serializers/input/utils/settings-key-type-mapper.js +2 -1
  98. package/core/server/data/migrations/versions/5.111/2025-03-05-16-36-39-add-captcha-setting.js +8 -0
  99. package/core/server/data/migrations/versions/5.112/2025-03-10-10-01-01-add-require-mfa-setting.js +8 -0
  100. package/core/server/data/schema/default-settings/default-settings.json +14 -0
  101. package/core/server/models/invite.js +4 -5
  102. package/core/server/models/post.js +3 -9
  103. package/core/server/models/relations/authors.js +2 -4
  104. package/core/server/models/role-utils.js +38 -0
  105. package/core/server/models/role.js +5 -3
  106. package/core/server/models/user.js +5 -3
  107. package/core/server/services/activitypub/ActivityPubService.js +116 -0
  108. package/core/server/services/activitypub/ActivityPubService.ts +139 -0
  109. package/core/server/services/activitypub/ActivityPubServiceWrapper.js +1 -1
  110. package/core/server/services/link-tracking/ClickEvent.js +25 -0
  111. package/core/server/services/link-tracking/FullPostLink.js +36 -0
  112. package/core/server/services/link-tracking/LinkClickRepository.js +1 -1
  113. package/core/server/services/link-tracking/LinkClickTrackingService.js +237 -0
  114. package/core/server/services/link-tracking/PostLink.js +29 -0
  115. package/core/server/services/link-tracking/PostLinkRepository.js +2 -2
  116. package/core/server/services/link-tracking/index.js +1 -1
  117. package/core/server/services/members-events/EventStorage.js +61 -0
  118. package/core/server/services/members-events/LastSeenAtCache.js +96 -0
  119. package/core/server/services/members-events/LastSeenAtUpdater.js +192 -0
  120. package/core/server/services/members-events/index.js +3 -1
  121. package/core/server/services/mentions-email-report/MentionEmailReportJob.js +117 -0
  122. package/core/server/services/mentions-email-report/service.js +3 -3
  123. package/core/server/services/staff/StaffService.js +179 -0
  124. package/core/server/services/staff/StaffServiceEmails.js +527 -0
  125. package/core/server/services/staff/email-templates/donation.hbs +119 -0
  126. package/core/server/services/staff/email-templates/donation.txt.js +15 -0
  127. package/core/server/services/staff/email-templates/mention-report.hbs +136 -0
  128. package/core/server/services/staff/email-templates/mention-report.txt.js +19 -0
  129. package/core/server/services/staff/email-templates/new-free-signup.hbs +118 -0
  130. package/core/server/services/staff/email-templates/new-free-signup.txt.js +13 -0
  131. package/core/server/services/staff/email-templates/new-milestone-received.hbs +142 -0
  132. package/core/server/services/staff/email-templates/new-milestone-received.txt.js +13 -0
  133. package/core/server/services/staff/email-templates/new-paid-cancellation.hbs +125 -0
  134. package/core/server/services/staff/email-templates/new-paid-cancellation.txt.js +13 -0
  135. package/core/server/services/staff/email-templates/new-paid-started.hbs +124 -0
  136. package/core/server/services/staff/email-templates/new-paid-started.txt.js +13 -0
  137. package/core/server/services/staff/email-templates/partials/preview.hbs +6 -0
  138. package/core/server/services/staff/email-templates/partials/styles.hbs +114 -0
  139. package/core/server/services/staff/email-templates/recommendation-received.hbs +154 -0
  140. package/core/server/services/staff/email-templates/recommendation-received.txt.js +13 -0
  141. package/core/server/services/staff/index.js +1 -1
  142. package/core/server/services/staff/milestone-email-config.js +207 -0
  143. package/core/server/services/stats/MembersStatsService.js +167 -0
  144. package/core/server/services/stats/MrrStatsService.js +161 -0
  145. package/core/server/services/stats/ReferrersStatsService.js +164 -0
  146. package/core/server/services/stats/StatsService.js +63 -0
  147. package/core/server/services/stats/SubscriptionStatsService.js +180 -0
  148. package/core/server/services/stats/service.js +1 -1
  149. package/core/server/services/url/Resources.js +2 -2
  150. package/core/shared/config/defaults.json +2 -1
  151. package/core/shared/events/URLResourceUpdatedEvent.js +33 -0
  152. package/core/shared/settings-cache/public.js +1 -0
  153. package/package.json +155 -158
  154. package/tsconfig.json +105 -0
  155. package/tsconfig.tsbuildinfo +1 -0
  156. package/yarn.lock +347 -136
  157. package/components/tryghost-activitypub-5.110.4.tgz +0 -0
  158. package/components/tryghost-adapter-cache-memory-ttl-5.110.4.tgz +0 -0
  159. package/components/tryghost-adapter-cache-redis-5.110.4.tgz +0 -0
  160. package/components/tryghost-announcement-bar-settings-5.110.4.tgz +0 -0
  161. package/components/tryghost-api-version-compatibility-service-5.110.4.tgz +0 -0
  162. package/components/tryghost-bookshelf-repository-5.110.4.tgz +0 -0
  163. package/components/tryghost-bootstrap-socket-5.110.4.tgz +0 -0
  164. package/components/tryghost-captcha-service-5.110.4.tgz +0 -0
  165. package/components/tryghost-constants-5.110.4.tgz +0 -0
  166. package/components/tryghost-custom-fonts-5.110.4.tgz +0 -0
  167. package/components/tryghost-donations-5.110.4.tgz +0 -0
  168. package/components/tryghost-dynamic-routing-events-5.110.4.tgz +0 -0
  169. package/components/tryghost-email-addresses-5.110.4.tgz +0 -0
  170. package/components/tryghost-email-analytics-provider-mailgun-5.110.4.tgz +0 -0
  171. package/components/tryghost-email-content-generator-5.110.4.tgz +0 -0
  172. package/components/tryghost-email-events-5.110.4.tgz +0 -0
  173. package/components/tryghost-email-service-5.110.4.tgz +0 -0
  174. package/components/tryghost-email-suppression-list-5.110.4.tgz +0 -0
  175. package/components/tryghost-extract-api-key-5.110.4.tgz +0 -0
  176. package/components/tryghost-ghost-5.110.4.tgz +0 -0
  177. package/components/tryghost-i18n-5.110.4.tgz +0 -0
  178. package/components/tryghost-identity-token-service-5.110.4.tgz +0 -0
  179. package/components/tryghost-importer-handler-content-files-5.110.4.tgz +0 -0
  180. package/components/tryghost-in-memory-repository-5.110.4.tgz +0 -0
  181. package/components/tryghost-link-replacer-5.110.4.tgz +0 -0
  182. package/components/tryghost-link-tracking-5.110.4.tgz +0 -0
  183. package/components/tryghost-mail-events-5.110.4.tgz +0 -0
  184. package/components/tryghost-mailgun-client-5.110.4.tgz +0 -0
  185. package/components/tryghost-members-events-service-5.110.4.tgz +0 -0
  186. package/components/tryghost-mentions-email-report-5.110.4.tgz +0 -0
  187. package/components/tryghost-minifier-5.110.4.tgz +0 -0
  188. package/components/tryghost-mw-api-version-mismatch-5.110.4.tgz +0 -0
  189. package/components/tryghost-mw-error-handler-5.110.4.tgz +0 -0
  190. package/components/tryghost-mw-version-match-5.110.4.tgz +0 -0
  191. package/components/tryghost-mw-vhost-5.110.4.tgz +0 -0
  192. package/components/tryghost-package-json-5.110.4.tgz +0 -0
  193. package/components/tryghost-prometheus-metrics-5.110.4.tgz +0 -0
  194. package/components/tryghost-recommendations-5.110.4.tgz +0 -0
  195. package/components/tryghost-referrers-5.110.4.tgz +0 -0
  196. package/components/tryghost-session-service-5.110.4.tgz +0 -0
  197. package/components/tryghost-settings-path-manager-5.110.4.tgz +0 -0
  198. package/components/tryghost-staff-service-5.110.4.tgz +0 -0
  199. package/components/tryghost-stats-service-5.110.4.tgz +0 -0
  200. package/components/tryghost-tiers-5.110.4.tgz +0 -0
  201. package/components/tryghost-version-notifications-data-service-5.110.4.tgz +0 -0
  202. package/components/tryghost-webmentions-5.110.4.tgz +0 -0
@@ -0,0 +1,164 @@
1
+ const moment = require('moment');
2
+
3
+ class ReferrersStatsService {
4
+ /**
5
+ * @param {object} deps
6
+ * @param {import('knex').Knex} deps.knex
7
+ **/
8
+ constructor({knex}) {
9
+ this.knex = knex;
10
+ }
11
+
12
+ /**
13
+ * Return a list of all the attribution sources for a given post, with their signup and conversion counts
14
+ * @param {string} postId
15
+ * @returns {Promise<AttributionCountStat[]>}
16
+ */
17
+ async getForPost(postId) {
18
+ const knex = this.knex;
19
+ const signupRows = await knex('members_created_events')
20
+ .select('referrer_source')
21
+ .select(knex.raw('COUNT(id) AS total'))
22
+ .where('attribution_id', postId)
23
+ .where('attribution_type', 'post')
24
+ .groupBy('referrer_source');
25
+
26
+ const conversionRows = await knex('members_subscription_created_events')
27
+ .select('referrer_source')
28
+ .select(knex.raw('COUNT(id) AS total'))
29
+ .where('attribution_id', postId)
30
+ .where('attribution_type', 'post')
31
+ .groupBy('referrer_source');
32
+
33
+ // Stitch them toghether, grouping them by source
34
+
35
+ const map = new Map();
36
+ for (const row of signupRows) {
37
+ map.set(row.referrer_source, {
38
+ source: row.referrer_source,
39
+ signups: row.total,
40
+ paid_conversions: 0
41
+ });
42
+ }
43
+
44
+ for (const row of conversionRows) {
45
+ const existing = map.get(row.referrer_source) ?? {
46
+ source: row.referrer_source,
47
+ signups: 0,
48
+ paid_conversions: 0
49
+ };
50
+ existing.paid_conversions = row.total;
51
+ map.set(row.referrer_source, existing);
52
+ }
53
+
54
+ return [...map.values()].sort((a, b) => b.paid_conversions - a.paid_conversions);
55
+ }
56
+
57
+ /**
58
+ * Return a list of all the attribution sources, with their signup and conversion counts on each date
59
+ * @returns {Promise<{data: AttributionCountStat[], meta: {}}>}
60
+ */
61
+ async getReferrersHistory() {
62
+ const paidConversionEntries = await this.fetchAllPaidConversionSources();
63
+ const signupEntries = await this.fetchAllSignupSources();
64
+
65
+ const allEntries = signupEntries.map((entry) => {
66
+ return {
67
+ ...entry,
68
+ paid_conversions: 0,
69
+ date: moment(entry.date).format('YYYY-MM-DD')
70
+ };
71
+ });
72
+
73
+ paidConversionEntries.forEach((entry) => {
74
+ const entryDate = moment(entry.date).format('YYYY-MM-DD');
75
+ const existingEntry = allEntries.find(e => e.source === entry.source && e.date === entryDate);
76
+
77
+ if (existingEntry) {
78
+ existingEntry.paid_conversions = entry.paid_conversions;
79
+ } else {
80
+ allEntries.push({
81
+ ...entry,
82
+ signups: 0,
83
+ date: entryDate
84
+ });
85
+ }
86
+ });
87
+
88
+ // sort allEntries in date ascending format
89
+ allEntries.sort((a, b) => {
90
+ return moment(a.date).diff(moment(b.date));
91
+ });
92
+
93
+ return {
94
+ data: allEntries,
95
+ meta: {}
96
+ };
97
+ }
98
+
99
+ /**
100
+ * @returns {Promise<PaidConversionsCountStatDate[]>}
101
+ **/
102
+ async fetchAllPaidConversionSources() {
103
+ const knex = this.knex;
104
+ const ninetyDaysAgo = moment.utc().subtract(90, 'days').startOf('day').utc().format('YYYY-MM-DD HH:mm:ss');
105
+ const rows = await knex('members_subscription_created_events')
106
+ .select(knex.raw(`DATE(created_at) as date`))
107
+ .select(knex.raw(`COUNT(*) as paid_conversions`))
108
+ .select(knex.raw(`referrer_source as source`))
109
+ .where('created_at', '>=', ninetyDaysAgo)
110
+ .groupBy('date', 'referrer_source')
111
+ .orderBy('date');
112
+
113
+ return rows;
114
+ }
115
+
116
+ /**
117
+ * @returns {Promise<SignupCountStatDate[]>}
118
+ **/
119
+ async fetchAllSignupSources() {
120
+ const knex = this.knex;
121
+ const ninetyDaysAgo = moment.utc().subtract(90, 'days').startOf('day').utc().format('YYYY-MM-DD HH:mm:ss');
122
+ const rows = await knex('members_created_events')
123
+ .select(knex.raw(`DATE(created_at) as date`))
124
+ .select(knex.raw(`COUNT(*) as signups`))
125
+ .select(knex.raw(`referrer_source as source`))
126
+ .where('created_at', '>=', ninetyDaysAgo)
127
+ .groupBy('date', 'referrer_source')
128
+ .orderBy('date');
129
+
130
+ return rows;
131
+ }
132
+ }
133
+
134
+ module.exports = ReferrersStatsService;
135
+
136
+ /**
137
+ * @typedef AttributionCountStat
138
+ * @type {Object}
139
+ * @property {string} source Attribution Source
140
+ * @property {number} signups Total free members signed up for this source
141
+ * @property {number} paid_conversions Total paid conversions for this source
142
+ */
143
+
144
+ /**
145
+ * @typedef AttributionCountStatDate
146
+ * @type {AttributionCountStat}
147
+ * @property {string} date The date (YYYY-MM-DD) on which these counts were recorded
148
+ */
149
+
150
+ /**
151
+ * @typedef {object} SignupCountStatDate
152
+ * @type {Object}
153
+ * @property {string} source Attribution Source
154
+ * @property {number} signups Total free members signed up for this source
155
+ * @property {string} date The date (YYYY-MM-DD) on which these counts were recorded
156
+ **/
157
+
158
+ /**
159
+ * @typedef {object} PaidConversionsCountStatDate
160
+ * @type {Object}
161
+ * @property {string} source Attribution Source
162
+ * @property {number} paid_conversions Total paid conversions for this source
163
+ * @property {string} date The date (YYYY-MM-DD) on which these counts were recorded
164
+ **/
@@ -0,0 +1,63 @@
1
+ const MRRService = require('./MrrStatsService');
2
+ const MembersService = require('./MembersStatsService');
3
+ const SubscriptionStatsService = require('./SubscriptionStatsService');
4
+ const ReferrersStatsService = require('./ReferrersStatsService');
5
+
6
+ class StatsService {
7
+ /**
8
+ * @param {object} deps
9
+ * @param {MRRService} deps.mrr
10
+ * @param {MembersService} deps.members
11
+ * @param {SubscriptionStatsService} deps.subscriptions
12
+ * @param {ReferrersStatsService} deps.referrers
13
+ **/
14
+ constructor(deps) {
15
+ this.mrr = deps.mrr;
16
+ this.members = deps.members;
17
+ this.subscriptions = deps.subscriptions;
18
+ this.referrers = deps.referrers;
19
+ }
20
+
21
+ async getMRRHistory() {
22
+ return this.mrr.getHistory();
23
+ }
24
+
25
+ async getMemberCountHistory() {
26
+ return this.members.getCountHistory();
27
+ }
28
+
29
+ async getSubscriptionCountHistory() {
30
+ return this.subscriptions.getSubscriptionHistory();
31
+ }
32
+
33
+ async getReferrersHistory() {
34
+ return this.referrers.getReferrersHistory();
35
+ }
36
+
37
+ /**
38
+ * @param {string} postId
39
+ */
40
+ async getPostReferrers(postId) {
41
+ return {
42
+ data: await this.referrers.getForPost(postId),
43
+ meta: {}
44
+ };
45
+ }
46
+
47
+ /**
48
+ * @param {object} deps
49
+ * @param {import('knex').Knex} deps.knex
50
+ *
51
+ * @returns {StatsService}
52
+ **/
53
+ static create(deps) {
54
+ return new StatsService({
55
+ mrr: new MRRService(deps),
56
+ members: new MembersService(deps),
57
+ subscriptions: new SubscriptionStatsService(deps),
58
+ referrers: new ReferrersStatsService(deps)
59
+ });
60
+ }
61
+ }
62
+
63
+ module.exports = StatsService;
@@ -0,0 +1,180 @@
1
+ const moment = require('moment');
2
+
3
+ class SubscriptionStatsService {
4
+ /**
5
+ * @param {object} deps
6
+ * @param {import('knex').Knex} deps.knex*/
7
+ constructor({knex}) {
8
+ this.knex = knex;
9
+ }
10
+
11
+ /**
12
+ * @returns {Promise<{data: SubscriptionHistoryEntry[]}>}
13
+ **/
14
+ async getSubscriptionHistory() {
15
+ const subscriptionDeltaEntries = await this.fetchAllSubscriptionDeltas();
16
+ const counts = await this.fetchSubscriptionCounts();
17
+
18
+ /** @type {Object.<string, Object.<string, number>>} */
19
+ const countData = {};
20
+ counts.forEach((count) => {
21
+ if (!countData[count.tier]) {
22
+ countData[count.tier] = {};
23
+ }
24
+ countData[count.tier][count.cadence] = count.count;
25
+ });
26
+
27
+ /** @type {SubscriptionHistoryEntry[]} */
28
+ let subscriptionHistoryEntries = [];
29
+
30
+ /** @type {string[]} */
31
+ let cadences = [];
32
+ /** @type {string[]} */
33
+ let tiers = [];
34
+
35
+ for (let index = subscriptionDeltaEntries.length - 1; index >= 0; index -= 1) {
36
+ const entry = subscriptionDeltaEntries[index];
37
+ if (!countData[entry.tier]) {
38
+ countData[entry.tier] = {};
39
+ }
40
+ if (!countData[entry.tier][entry.cadence]) {
41
+ countData[entry.tier][entry.cadence] = 0;
42
+ }
43
+
44
+ subscriptionHistoryEntries.unshift({
45
+ ...entry,
46
+ date: moment(entry.date).format('YYYY-MM-DD'),
47
+ count: countData[entry.tier][entry.cadence]
48
+ });
49
+
50
+ countData[entry.tier][entry.cadence] += entry.negative_delta;
51
+ countData[entry.tier][entry.cadence] -= entry.positive_delta;
52
+
53
+ if (!cadences.includes(entry.cadence)) {
54
+ cadences.push(entry.cadence);
55
+ }
56
+ if (!tiers.includes(entry.tier)) {
57
+ tiers.push(entry.tier);
58
+ }
59
+ }
60
+
61
+ return {
62
+ data: subscriptionHistoryEntries,
63
+ meta: {
64
+ cadences,
65
+ tiers,
66
+ totals: counts
67
+ }
68
+ };
69
+ }
70
+
71
+ /**
72
+ * @returns {Promise<SubscriptionDelta[]>}
73
+ **/
74
+ async fetchAllSubscriptionDeltas() {
75
+ const knex = this.knex;
76
+ const rows = await knex('members_paid_subscription_events')
77
+ .join('stripe_prices AS price', function () {
78
+ this.on('price.stripe_price_id', '=', 'members_paid_subscription_events.from_plan')
79
+ .orOn('price.stripe_price_id', '=', 'members_paid_subscription_events.to_plan');
80
+ })
81
+ .join('stripe_products AS product', 'product.stripe_product_id', '=', 'price.stripe_product_id')
82
+ .join('products AS tier', 'tier.id', '=', 'product.product_id')
83
+ .leftJoin('stripe_prices AS from_price', 'from_price.stripe_price_id', '=', 'members_paid_subscription_events.from_plan')
84
+ .leftJoin('stripe_prices AS to_price', 'to_price.stripe_price_id', '=', 'members_paid_subscription_events.to_plan')
85
+ .select(knex.raw(`
86
+ DATE(members_paid_subscription_events.created_at) as date
87
+ `))
88
+ .select(knex.raw(`
89
+ tier.id as tier
90
+ `))
91
+ .select(knex.raw(`
92
+ price.interval as cadence
93
+ `))
94
+ .select(knex.raw(`SUM(
95
+ CASE
96
+ WHEN members_paid_subscription_events.type IN ('created','reactivated','active') AND members_paid_subscription_events.mrr_delta != 0 THEN 1
97
+ WHEN members_paid_subscription_events.type='updated' AND price.id = to_price.id THEN 1
98
+ WHEN members_paid_subscription_events.type='updated' AND members_paid_subscription_events.from_plan = members_paid_subscription_events.to_plan AND members_paid_subscription_events.mrr_delta > 0 THEN 1
99
+ ELSE 0
100
+ END
101
+ ) as positive_delta`))
102
+ .select(knex.raw(`SUM(
103
+ CASE
104
+ WHEN members_paid_subscription_events.type IN ('canceled', 'expired','inactive') AND members_paid_subscription_events.mrr_delta != 0 THEN 1
105
+ WHEN members_paid_subscription_events.type='updated' AND price.id = from_price.id THEN 1
106
+ ELSE 0
107
+ END
108
+ ) as negative_delta`))
109
+ .select(knex.raw(`SUM(
110
+ CASE
111
+ WHEN members_paid_subscription_events.type IN ('created','reactivated','active') AND members_paid_subscription_events.mrr_delta != 0 THEN 1
112
+ WHEN members_paid_subscription_events.type='updated' AND members_paid_subscription_events.from_plan = members_paid_subscription_events.to_plan AND members_paid_subscription_events.mrr_delta > 0 THEN 1
113
+ ELSE 0
114
+ END
115
+ ) as signups`))
116
+ .select(knex.raw(`SUM(
117
+ CASE
118
+ WHEN members_paid_subscription_events.type IN ('canceled', 'expired','inactive') AND members_paid_subscription_events.mrr_delta != 0 THEN 1
119
+ ELSE 0
120
+ END
121
+ ) as cancellations`))
122
+ .groupBy('date', 'tier', 'cadence')
123
+ .orderBy('date');
124
+
125
+ return rows;
126
+ }
127
+
128
+ /**
129
+ * Get the current total subscriptions grouped by Cadence and Tier
130
+ * @returns {Promise<SubscriptionCount[]>}
131
+ **/
132
+ async fetchSubscriptionCounts() {
133
+ const knex = this.knex;
134
+
135
+ const data = await knex('members_stripe_customers_subscriptions')
136
+ .select(knex.raw(`
137
+ COUNT(members_stripe_customers_subscriptions.id) AS count,
138
+ products.id AS tier,
139
+ stripe_prices.interval AS cadence
140
+ `))
141
+ .join('stripe_prices', 'stripe_prices.stripe_price_id', '=', 'members_stripe_customers_subscriptions.stripe_price_id')
142
+ .join('stripe_products', 'stripe_products.stripe_product_id', '=', 'stripe_prices.stripe_product_id')
143
+ .join('products', 'products.id', '=', 'stripe_products.product_id')
144
+ .whereNot('members_stripe_customers_subscriptions.mrr', 0)
145
+ .groupBy('tier', 'cadence');
146
+
147
+ return data;
148
+ }
149
+ }
150
+
151
+ /** @typedef {object} SubscriptionCount
152
+ * @prop {string} tier
153
+ * @prop {string} cadence
154
+ * @prop {number} count
155
+ **/
156
+
157
+ /**
158
+ * @typedef {object} SubscriptionDelta
159
+ * @prop {string} tier
160
+ * @prop {string} cadence
161
+ * @prop {string} date
162
+ * @prop {number} positive_delta
163
+ * @prop {number} negative_delta
164
+ * @prop {number} signups
165
+ * @prop {number} cancellations
166
+ **/
167
+
168
+ /**
169
+ * @typedef {object} SubscriptionHistoryEntry
170
+ * @prop {string} tier
171
+ * @prop {string} cadence
172
+ * @prop {string} date
173
+ * @prop {number} positive_delta
174
+ * @prop {number} negative_delta
175
+ * @prop {number} signups
176
+ * @prop {number} cancellations
177
+ * @prop {number} count
178
+ **/
179
+
180
+ module.exports = SubscriptionStatsService;
@@ -10,7 +10,7 @@ class StatsServiceWrapper {
10
10
  return;
11
11
  }
12
12
 
13
- const StatsService = require('@tryghost/stats-service');
13
+ const StatsService = require('./StatsService');
14
14
  const db = require('../../data/db');
15
15
 
16
16
  this.api = StatsService.create({knex: db.knex});
@@ -1,7 +1,7 @@
1
1
  const _ = require('lodash');
2
2
  const debug = require('@tryghost/debug')('services:url:resources');
3
3
  const DomainEvents = require('@tryghost/domain-events');
4
- const {URLResourceUpdatedEvent} = require('@tryghost/dynamic-routing-events');
4
+ const URLResourceUpdatedEvent = require('../../../shared/events/URLResourceUpdatedEvent');
5
5
  const Resource = require('./Resource');
6
6
  const config = require('../../../shared/config');
7
7
  const models = require('../../models');
@@ -446,7 +446,7 @@ class Resources {
446
446
  * @returns {Object}
447
447
  */
448
448
  getByIdAndType(type, id) {
449
- return _.find(this.data[type], {data: {id: id}});
449
+ return this.data[type]?.find(r => r.data.id === id);
450
450
  }
451
451
 
452
452
  /**
@@ -127,7 +127,8 @@
127
127
  "lifetime": 3600,
128
128
  "freeRetries": 10
129
129
  },
130
- "blocked_email_domains": []
130
+ "blocked_email_domains": [],
131
+ "captcha_enabled": false
131
132
  },
132
133
  "caching": {
133
134
  "301": {
@@ -0,0 +1,33 @@
1
+ module.exports = class URLResourceUpdatedEvent {
2
+ /**
3
+ * @readonly
4
+ * @type {Object}
5
+ */
6
+ data;
7
+
8
+ /**
9
+ * @readonly
10
+ * @type {Date}
11
+ */
12
+ timestamp;
13
+
14
+ /**
15
+ * @private
16
+ */
17
+ constructor({timestamp, ...data}) {
18
+ this.data = data;
19
+ this.timestamp = timestamp;
20
+ }
21
+
22
+ /**
23
+ *
24
+ * @param {Object} data URL Resource
25
+ * @returns
26
+ */
27
+ static create(data) {
28
+ return new URLResourceUpdatedEvent({
29
+ ...data,
30
+ timestamp: data.timestamp || new Date
31
+ });
32
+ }
33
+ };
@@ -48,5 +48,6 @@ module.exports = {
48
48
  default_email_address: 'default_email_address',
49
49
  support_email_address: 'support_email_address',
50
50
  editor_default_email_recipients: 'editor_default_email_recipients',
51
+ captcha_enabled: 'captcha_enabled',
51
52
  labs: 'labs'
52
53
  };