ghost 5.115.1 → 5.116.1

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 (243) hide show
  1. package/components/{tryghost-api-framework-5.115.1.tgz → tryghost-api-framework-5.116.1.tgz} +0 -0
  2. package/components/tryghost-constants-5.116.1.tgz +0 -0
  3. package/components/tryghost-custom-fonts-5.116.1.tgz +0 -0
  4. package/components/{tryghost-custom-theme-settings-service-5.115.1.tgz → tryghost-custom-theme-settings-service-5.116.1.tgz} +0 -0
  5. package/components/{tryghost-domain-events-5.115.1.tgz → tryghost-domain-events-5.116.1.tgz} +0 -0
  6. package/components/{tryghost-donations-5.115.1.tgz → tryghost-donations-5.116.1.tgz} +0 -0
  7. package/components/tryghost-email-addresses-5.116.1.tgz +0 -0
  8. package/components/tryghost-email-service-5.116.1.tgz +0 -0
  9. package/components/tryghost-email-suppression-list-5.116.1.tgz +0 -0
  10. package/components/tryghost-html-to-plaintext-5.116.1.tgz +0 -0
  11. package/components/tryghost-i18n-5.116.1.tgz +0 -0
  12. package/components/tryghost-job-manager-5.116.1.tgz +0 -0
  13. package/components/tryghost-link-replacer-5.116.1.tgz +0 -0
  14. package/components/tryghost-magic-link-5.116.1.tgz +0 -0
  15. package/components/{tryghost-member-attribution-5.115.1.tgz → tryghost-member-attribution-5.116.1.tgz} +0 -0
  16. package/components/tryghost-member-events-5.116.1.tgz +0 -0
  17. package/components/tryghost-members-api-5.116.1.tgz +0 -0
  18. package/components/tryghost-members-csv-5.116.1.tgz +0 -0
  19. package/components/{tryghost-members-offers-5.115.1.tgz → tryghost-members-offers-5.116.1.tgz} +0 -0
  20. package/components/{tryghost-milestones-5.115.1.tgz → tryghost-milestones-5.116.1.tgz} +0 -0
  21. package/components/{tryghost-mw-error-handler-5.115.1.tgz → tryghost-mw-error-handler-5.116.1.tgz} +0 -0
  22. package/components/tryghost-mw-vhost-5.116.1.tgz +0 -0
  23. package/components/{tryghost-post-events-5.115.1.tgz → tryghost-post-events-5.116.1.tgz} +0 -0
  24. package/components/{tryghost-post-revisions-5.115.1.tgz → tryghost-post-revisions-5.116.1.tgz} +0 -0
  25. package/components/tryghost-posts-service-5.116.1.tgz +0 -0
  26. package/components/{tryghost-prometheus-metrics-5.115.1.tgz → tryghost-prometheus-metrics-5.116.1.tgz} +0 -0
  27. package/components/tryghost-security-5.116.1.tgz +0 -0
  28. package/components/{tryghost-tiers-5.115.1.tgz → tryghost-tiers-5.116.1.tgz} +0 -0
  29. package/components/tryghost-webmentions-5.116.1.tgz +0 -0
  30. package/content/themes/casper/LICENSE +1 -1
  31. package/content/themes/casper/README.md +1 -1
  32. package/content/themes/source/LICENSE +1 -1
  33. package/content/themes/source/README.md +1 -1
  34. package/content/themes/source/assets/built/screen.css +1 -1
  35. package/content/themes/source/assets/built/screen.css.map +1 -1
  36. package/content/themes/source/assets/css/screen.css +6 -11
  37. package/content/themes/source/partials/feature-image.hbs +2 -2
  38. package/core/boot.js +0 -42
  39. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +24764 -24129
  40. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
  41. package/core/built/admin/assets/admin-x-demo/{index-15df2af5.mjs → index-a9601514.mjs} +3 -3
  42. package/core/built/admin/assets/admin-x-demo/{modals-8ca61d78.mjs → modals-c1789d04.mjs} +2 -2
  43. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-d2e6872f.mjs → CodeEditorView-e9c9deb8.mjs} +2 -2
  44. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  45. package/core/built/admin/assets/admin-x-settings/{index-8e8821e5.mjs → index-84580c3a.mjs} +2 -2
  46. package/core/built/admin/assets/admin-x-settings/{index-f5cb3db3.mjs → index-f744cab7.mjs} +49 -35
  47. package/core/built/admin/assets/admin-x-settings/{modals-e8ae4d46.mjs → modals-d9ca60c5.mjs} +1198 -1192
  48. package/core/built/admin/assets/chunk.524.cb72a86e19c9ffd6172e.js +35 -0
  49. package/core/built/admin/assets/chunk.582.4f4d38ffe79fbdbd26f7.js +37 -0
  50. package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js → chunk.713.e9027c0cc3c56110f5da.js} +125 -98
  51. package/core/built/admin/assets/{ghost-df7b9558260aa27d18b195ee895b487d.js → ghost-03b64c086f3c60cabc85fe7a7e2b640a.js} +144 -145
  52. package/core/built/admin/assets/ghost-ba58e9822f7384461e926c7e23f04a75.css +1 -0
  53. package/core/built/admin/assets/ghost-dark-f1f29683b14ffa11615b3bba8b6ab92c.css +1 -0
  54. package/core/built/admin/assets/koenig-lexical/index.css +1 -1
  55. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +20563 -20891
  56. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +139 -139
  57. package/core/built/admin/assets/posts/posts.js +5732 -5667
  58. package/core/built/admin/assets/stats/stats.js +71082 -7533
  59. package/core/built/admin/assets/{vendor-68a4aa424a179a90f5bbc2b750def576.js → vendor-72026232b36d97babc6320917c16c321.js} +36 -34
  60. package/core/built/admin/index.html +6 -6
  61. package/core/cli/generate-data.js +1 -1
  62. package/core/frontend/helpers/ghost_head.js +6 -1
  63. package/core/frontend/public/ghost-stats.js +55 -2
  64. package/core/frontend/services/assets-minification/AdminAuthAssets.js +2 -1
  65. package/core/frontend/services/assets-minification/CardAssets.js +1 -1
  66. package/core/frontend/services/assets-minification/CommentCountsAssets.js +1 -1
  67. package/core/frontend/services/assets-minification/MemberAttributionAssets.js +1 -1
  68. package/core/frontend/services/assets-minification/Minifier.js +191 -0
  69. package/core/frontend/services/routing/controllers/previews.js +2 -1
  70. package/core/server/adapters/cache/Redis.js +1 -1
  71. package/core/server/adapters/lib/redis/AdapterCacheRedis.js +287 -0
  72. package/core/server/adapters/lib/redis/redis-store-factory.js +22 -0
  73. package/core/server/api/endpoints/posts.js +9 -3
  74. package/core/server/api/endpoints/previews.js +35 -1
  75. package/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js +6 -9
  76. package/core/server/api/endpoints/utils/validators/input/settings.js +1 -1
  77. package/core/server/data/db/connection.js +2 -0
  78. package/core/server/data/db/index.js +1 -0
  79. package/core/server/data/importer/handlers/ImporterContentFileHandler.js +90 -0
  80. package/core/server/data/importer/import-manager.js +1 -1
  81. package/core/server/data/seeders/DataGenerator.js +288 -0
  82. package/core/server/data/seeders/importers/BenefitsImporter.js +28 -0
  83. package/core/server/data/seeders/importers/CommentsImporter.js +73 -0
  84. package/core/server/data/seeders/importers/EmailBatchesImporter.js +38 -0
  85. package/core/server/data/seeders/importers/EmailRecipientFailuresImporter.js +67 -0
  86. package/core/server/data/seeders/importers/EmailRecipientsImporter.js +212 -0
  87. package/core/server/data/seeders/importers/EmailsImporter.js +99 -0
  88. package/core/server/data/seeders/importers/LabelsImporter.js +41 -0
  89. package/core/server/data/seeders/importers/MembersClickEventsImporter.js +69 -0
  90. package/core/server/data/seeders/importers/MembersCreatedEventsImporter.js +103 -0
  91. package/core/server/data/seeders/importers/MembersFeedbackImporter.js +45 -0
  92. package/core/server/data/seeders/importers/MembersImporter.js +111 -0
  93. package/core/server/data/seeders/importers/MembersLabelsImporter.js +39 -0
  94. package/core/server/data/seeders/importers/MembersLoginEventsImporter.js +69 -0
  95. package/core/server/data/seeders/importers/MembersNewslettersImporter.js +38 -0
  96. package/core/server/data/seeders/importers/MembersPaidSubscriptionEventsImporter.js +99 -0
  97. package/core/server/data/seeders/importers/MembersProductsImporter.js +42 -0
  98. package/core/server/data/seeders/importers/MembersStatusEventsImporter.js +58 -0
  99. package/core/server/data/seeders/importers/MembersStripeCustomersImporter.js +60 -0
  100. package/core/server/data/seeders/importers/MembersStripeCustomersSubscriptionsImporter.js +259 -0
  101. package/core/server/data/seeders/importers/MembersSubscribeEventsImporter.js +69 -0
  102. package/core/server/data/seeders/importers/MembersSubscriptionCreatedEventsImporter.js +95 -0
  103. package/core/server/data/seeders/importers/NewslettersImporter.js +40 -0
  104. package/core/server/data/seeders/importers/OffersImporter.js +70 -0
  105. package/core/server/data/seeders/importers/PostsAuthorsImporter.js +32 -0
  106. package/core/server/data/seeders/importers/PostsImporter.js +102 -0
  107. package/core/server/data/seeders/importers/PostsProductsImporter.js +35 -0
  108. package/core/server/data/seeders/importers/PostsTagsImporter.js +46 -0
  109. package/core/server/data/seeders/importers/ProductsBenefitsImporter.js +54 -0
  110. package/core/server/data/seeders/importers/ProductsImporter.js +90 -0
  111. package/core/server/data/seeders/importers/RecommendationClickEventsImporter.js +32 -0
  112. package/core/server/data/seeders/importers/RecommendationSubscribeEventsImporter.js +32 -0
  113. package/core/server/data/seeders/importers/RecommendationsImporter.js +34 -0
  114. package/core/server/data/seeders/importers/RedirectsImporter.js +49 -0
  115. package/core/server/data/seeders/importers/RolesUsersImporter.js +42 -0
  116. package/core/server/data/seeders/importers/StripePricesImporter.js +69 -0
  117. package/core/server/data/seeders/importers/StripeProductsImporter.js +34 -0
  118. package/core/server/data/seeders/importers/TableImporter.js +187 -0
  119. package/core/server/data/seeders/importers/TagsImporter.js +41 -0
  120. package/core/server/data/seeders/importers/UsersImporter.js +31 -0
  121. package/core/server/data/seeders/importers/WebMentionsImporter.js +42 -0
  122. package/core/server/data/seeders/importers/index.js +41 -0
  123. package/core/server/data/seeders/utils/JsonImporter.js +39 -0
  124. package/core/server/data/seeders/utils/blog-info.js +3 -0
  125. package/core/server/data/seeders/utils/database-date.js +7 -0
  126. package/core/server/data/seeders/utils/event-generator.js +48 -0
  127. package/core/server/data/seeders/utils/random.js +13 -0
  128. package/core/server/data/seeders/utils/topological-sort.js +33 -0
  129. package/core/server/services/adapter-manager/AdapterManager.js +161 -0
  130. package/core/server/services/adapter-manager/index.js +1 -1
  131. package/core/server/services/announcement-bar-service/AnnouncementBarSettings.js +54 -0
  132. package/core/server/services/announcement-bar-service/AnnouncementVisibilityValues.js +11 -0
  133. package/core/server/services/announcement-bar-service/index.js +1 -1
  134. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +1 -1
  135. package/core/server/services/auth/session/session-service.js +15 -5
  136. package/core/server/services/custom-redirects/index.js +1 -1
  137. package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +1 -1
  138. package/core/server/services/email-service/EmailServiceWrapper.js +4 -4
  139. package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +1 -1
  140. package/core/server/services/email-suppression-list/service.js +1 -1
  141. package/core/server/services/lib/DynamicRedirectManager.js +156 -0
  142. package/core/server/services/lib/EmailContentGenerator.js +54 -0
  143. package/core/server/services/lib/InMemoryRepository.js +62 -0
  144. package/core/server/services/lib/InMemoryRepository.ts +80 -0
  145. package/core/server/services/lib/MailgunClient.js +364 -0
  146. package/core/server/services/link-redirection/LinkRedirect.js +26 -0
  147. package/core/server/services/link-redirection/LinkRedirectRepository.js +7 -7
  148. package/core/server/services/link-redirection/LinkRedirectsService.js +123 -0
  149. package/core/server/services/link-redirection/README.md +151 -0
  150. package/core/server/services/link-redirection/RedirectEvent.js +24 -0
  151. package/core/server/services/link-redirection/index.js +1 -1
  152. package/core/server/services/link-tracking/LinkClickTrackingService.js +1 -1
  153. package/core/server/services/mail/index.js +1 -1
  154. package/core/server/services/mail-events/InMemoryMailEventRepository.js +2 -2
  155. package/core/server/services/mail-events/InMemoryMailEventRepository.ts +1 -1
  156. package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
  157. package/core/server/services/offers/service.js +1 -1
  158. package/core/server/services/recommendations/RecommendationServiceWrapper.js +8 -8
  159. package/core/server/services/recommendations/service/BookshelfClickEventRepository.js +48 -0
  160. package/core/server/services/recommendations/service/BookshelfClickEventRepository.ts +49 -0
  161. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.js +98 -0
  162. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.ts +117 -0
  163. package/core/server/services/recommendations/service/BookshelfRepository.js +134 -0
  164. package/core/server/services/recommendations/service/BookshelfRepository.ts +196 -0
  165. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.js +48 -0
  166. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.ts +49 -0
  167. package/core/server/services/recommendations/service/ClickEvent.js +33 -0
  168. package/core/server/services/recommendations/service/ClickEvent.ts +32 -0
  169. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.js +19 -0
  170. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.ts +20 -0
  171. package/core/server/services/recommendations/service/IncomingRecommendationController.js +34 -0
  172. package/core/server/services/recommendations/service/IncomingRecommendationController.ts +51 -0
  173. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.js +25 -0
  174. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.ts +37 -0
  175. package/core/server/services/recommendations/service/IncomingRecommendationService.js +93 -0
  176. package/core/server/services/recommendations/service/IncomingRecommendationService.ts +160 -0
  177. package/core/server/services/recommendations/service/Recommendation.js +140 -0
  178. package/core/server/services/recommendations/service/Recommendation.ts +201 -0
  179. package/core/server/services/recommendations/service/RecommendationController.js +208 -0
  180. package/core/server/services/recommendations/service/RecommendationController.ts +258 -0
  181. package/core/server/services/recommendations/service/RecommendationMetadataService.js +86 -0
  182. package/core/server/services/recommendations/service/RecommendationMetadataService.ts +128 -0
  183. package/core/server/services/recommendations/service/RecommendationRepository.js +2 -0
  184. package/core/server/services/recommendations/service/RecommendationRepository.ts +13 -0
  185. package/core/server/services/recommendations/service/RecommendationService.js +228 -0
  186. package/core/server/services/recommendations/service/RecommendationService.ts +281 -0
  187. package/core/server/services/recommendations/service/SubscribeEvent.js +33 -0
  188. package/core/server/services/recommendations/service/SubscribeEvent.ts +32 -0
  189. package/core/server/services/recommendations/service/UnsafeData.js +183 -0
  190. package/core/server/services/recommendations/service/UnsafeData.ts +217 -0
  191. package/core/server/services/recommendations/service/WellknownService.js +36 -0
  192. package/core/server/services/recommendations/service/WellknownService.ts +47 -0
  193. package/core/server/services/recommendations/service/index.js +31 -0
  194. package/core/server/services/recommendations/service/index.ts +15 -0
  195. package/core/server/services/recommendations/service/libraries.d.ts +5 -0
  196. package/core/server/services/slack-notifications/SlackNotifications.js +211 -0
  197. package/core/server/services/slack-notifications/SlackNotificationsService.js +90 -0
  198. package/core/server/services/slack-notifications/service.js +4 -6
  199. package/core/server/web/api/endpoints/admin/app.js +1 -21
  200. package/core/server/web/api/middleware/version-match.js +41 -0
  201. package/core/shared/labs.js +2 -2
  202. package/package.json +87 -104
  203. package/tsconfig.tsbuildinfo +1 -1
  204. package/yarn.lock +1470 -1540
  205. package/components/tryghost-adapter-cache-redis-5.115.1.tgz +0 -0
  206. package/components/tryghost-adapter-manager-5.115.1.tgz +0 -0
  207. package/components/tryghost-announcement-bar-settings-5.115.1.tgz +0 -0
  208. package/components/tryghost-constants-5.115.1.tgz +0 -0
  209. package/components/tryghost-custom-fonts-5.115.1.tgz +0 -0
  210. package/components/tryghost-data-generator-5.115.1.tgz +0 -0
  211. package/components/tryghost-email-addresses-5.115.1.tgz +0 -0
  212. package/components/tryghost-email-content-generator-5.115.1.tgz +0 -0
  213. package/components/tryghost-email-events-5.115.1.tgz +0 -0
  214. package/components/tryghost-email-service-5.115.1.tgz +0 -0
  215. package/components/tryghost-email-suppression-list-5.115.1.tgz +0 -0
  216. package/components/tryghost-express-dynamic-redirects-5.115.1.tgz +0 -0
  217. package/components/tryghost-ghost-5.115.1.tgz +0 -0
  218. package/components/tryghost-html-to-plaintext-5.115.1.tgz +0 -0
  219. package/components/tryghost-i18n-5.115.1.tgz +0 -0
  220. package/components/tryghost-importer-handler-content-files-5.115.1.tgz +0 -0
  221. package/components/tryghost-in-memory-repository-5.115.1.tgz +0 -0
  222. package/components/tryghost-job-manager-5.115.1.tgz +0 -0
  223. package/components/tryghost-link-redirects-5.115.1.tgz +0 -0
  224. package/components/tryghost-link-replacer-5.115.1.tgz +0 -0
  225. package/components/tryghost-magic-link-5.115.1.tgz +0 -0
  226. package/components/tryghost-mailgun-client-5.115.1.tgz +0 -0
  227. package/components/tryghost-member-events-5.115.1.tgz +0 -0
  228. package/components/tryghost-members-api-5.115.1.tgz +0 -0
  229. package/components/tryghost-members-csv-5.115.1.tgz +0 -0
  230. package/components/tryghost-members-payments-5.115.1.tgz +0 -0
  231. package/components/tryghost-minifier-5.115.1.tgz +0 -0
  232. package/components/tryghost-mw-version-match-5.115.1.tgz +0 -0
  233. package/components/tryghost-mw-vhost-5.115.1.tgz +0 -0
  234. package/components/tryghost-posts-service-5.115.1.tgz +0 -0
  235. package/components/tryghost-recommendations-5.115.1.tgz +0 -0
  236. package/components/tryghost-security-5.115.1.tgz +0 -0
  237. package/components/tryghost-slack-notifications-5.115.1.tgz +0 -0
  238. package/components/tryghost-webmentions-5.115.1.tgz +0 -0
  239. package/core/built/admin/assets/chunk.524.2439684964c164c598ab.js +0 -35
  240. package/core/built/admin/assets/chunk.582.bf5a2bbb2c4eb69ef1e7.js +0 -37
  241. package/core/built/admin/assets/ghost-327b17ea23cb8c89bd7e6a51e18e8506.css +0 -1
  242. package/core/built/admin/assets/ghost-dark-f30a597ac19632a118939492591c531b.css +0 -1
  243. /package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js.LICENSE.txt → chunk.713.e9027c0cc3c56110f5da.js.LICENSE.txt} +0 -0
@@ -0,0 +1,117 @@
1
+ import {AllOptions, BookshelfRepository, ModelClass, ModelInstance} from './BookshelfRepository';
2
+ import logger from '@tryghost/logging';
3
+ import {Knex} from 'knex';
4
+ import {Recommendation} from './Recommendation';
5
+ import {RecommendationRepository} from './RecommendationRepository';
6
+
7
+ type Sentry = {
8
+ captureException(err: unknown): void;
9
+ }
10
+
11
+ type RecommendationFindOneData<T> = {
12
+ id?: T;
13
+ url?: string;
14
+ };
15
+
16
+ type RecommendationModelClass<T> = ModelClass<T> & {
17
+ findOne: (data: RecommendationFindOneData<T>, options?: { require?: boolean }) => Promise<ModelInstance<T> | null>;
18
+ };
19
+
20
+ export class BookshelfRecommendationRepository extends BookshelfRepository<string, Recommendation> implements RecommendationRepository {
21
+ sentry?: Sentry;
22
+
23
+ constructor(Model: RecommendationModelClass<string>, deps: {sentry?: Sentry} = {}) {
24
+ super(Model);
25
+ this.sentry = deps.sentry;
26
+ }
27
+
28
+ applyCustomQuery(query: Knex.QueryBuilder, options: AllOptions<Recommendation>) {
29
+ query.select('recommendations.*');
30
+
31
+ if (options.include?.includes('clickCount') || options.order?.find(o => o.field === 'clickCount')) {
32
+ query.select((knex: Knex.QueryBuilder) => {
33
+ knex.count('*').from('recommendation_click_events').where('recommendation_click_events.recommendation_id', knex.client.raw('recommendations.id')).as('count__clicks');
34
+ });
35
+ }
36
+
37
+ if (options.include?.includes('subscriberCount') || options.order?.find(o => o.field === 'subscriberCount')) {
38
+ query.select((knex: Knex.QueryBuilder) => {
39
+ knex.count('*').from('recommendation_subscribe_events').where('recommendation_subscribe_events.recommendation_id', knex.client.raw('recommendations.id')).as('count__subscribers');
40
+ });
41
+ }
42
+ }
43
+
44
+ toPrimitive(entity: Recommendation): object {
45
+ return {
46
+ id: entity.id,
47
+ title: entity.title,
48
+ description: entity.description,
49
+ excerpt: entity.excerpt,
50
+ featured_image: entity.featuredImage?.toString(),
51
+ favicon: entity.favicon?.toString(),
52
+ url: entity.url.toString(),
53
+ one_click_subscribe: entity.oneClickSubscribe,
54
+ created_at: entity.createdAt,
55
+ updated_at: entity.updatedAt
56
+ // Count relations are not saveable: so don't set them here
57
+ };
58
+ }
59
+
60
+ modelToEntity(model: ModelInstance<string>): Recommendation | null {
61
+ try {
62
+ return Recommendation.create({
63
+ id: model.id,
64
+ title: model.get('title') as string,
65
+ description: model.get('description') as string | null,
66
+ excerpt: model.get('excerpt') as string | null,
67
+ featuredImage: model.get('featured_image') as string | null,
68
+ favicon: model.get('favicon') as string | null,
69
+ url: model.get('url') as string,
70
+ oneClickSubscribe: model.get('one_click_subscribe') as boolean,
71
+ createdAt: model.get('created_at') as Date,
72
+ updatedAt: model.get('updated_at') as Date | null,
73
+ clickCount: (model.get('count__clicks') ?? undefined) as number | undefined,
74
+ subscriberCount: (model.get('count__subscribers') ?? undefined) as number | undefined
75
+ });
76
+ } catch (err) {
77
+ logger.error(err);
78
+ this.sentry?.captureException(err);
79
+ return null;
80
+ }
81
+ }
82
+
83
+ getFieldToColumnMap() {
84
+ return {
85
+ id: 'id',
86
+ title: 'title',
87
+ description: 'description',
88
+ excerpt: 'excerpt',
89
+ featuredImage: 'featured_image',
90
+ favicon: 'favicon',
91
+ url: 'url',
92
+ oneClickSubscribe: 'one_click_subscribe',
93
+ createdAt: 'created_at',
94
+ updatedAt: 'updated_at',
95
+ clickCount: 'count__clicks',
96
+ subscriberCount: 'count__subscribers'
97
+ } as Record<keyof Recommendation, string>;
98
+ }
99
+
100
+ async getByUrl(url: URL): Promise<Recommendation | null> {
101
+ const urlFilter = `url:~'${url.host.replace('www.', '')}${url.pathname.replace(/\/$/, '')}'`;
102
+ const recommendations = await this.getAll({filter: urlFilter});
103
+
104
+ if (!recommendations || recommendations.length === 0) {
105
+ return null;
106
+ }
107
+
108
+ // Find URL based on the hostname and pathname.
109
+ // Query params, hash fragements, protocol and www are ignored.
110
+ const existing = recommendations.find((r) => {
111
+ return r.url.hostname.replace('www.', '') === url.hostname.replace('www.', '') &&
112
+ r.url.pathname.replace(/\/$/, '') === url.pathname.replace(/\/$/, '');
113
+ }) || null;
114
+
115
+ return existing;
116
+ }
117
+ }
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ /* eslint-disable ghost/filenames/match-exported-class */
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.BookshelfRepository = void 0;
8
+ const mongo_utils_1 = require("@tryghost/mongo-utils");
9
+ const errors_1 = __importDefault(require("@tryghost/errors"));
10
+ class BookshelfRepository {
11
+ Model;
12
+ constructor(Model) {
13
+ this.Model = Model;
14
+ }
15
+ /**
16
+ * override this method to add custom query logic to knex queries
17
+ */
18
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
19
+ applyCustomQuery(query, options) {
20
+ return;
21
+ }
22
+ #entityFieldToColumn(field) {
23
+ const mapping = this.getFieldToColumnMap();
24
+ return mapping[field];
25
+ }
26
+ #orderToString(order) {
27
+ if (!order || order.length === 0) {
28
+ return;
29
+ }
30
+ return order.map(({ field, direction }) => `${this.#entityFieldToColumn(field)} ${direction}`).join(',');
31
+ }
32
+ /**
33
+ * Map all the fields in an NQL filter to the names of the model
34
+ */
35
+ #getNQLKeyTransformer() {
36
+ return (0, mongo_utils_1.chainTransformers)(...(0, mongo_utils_1.mapKeys)(this.getFieldToColumnMap()));
37
+ }
38
+ async save(entity) {
39
+ if (entity.deleted) {
40
+ await this.Model.destroy({ id: entity.id });
41
+ return;
42
+ }
43
+ const existing = await this.Model.findOne({ id: entity.id }, { require: false });
44
+ if (existing) {
45
+ existing.set(this.toPrimitive(entity));
46
+ await existing.save({}, { autoRefresh: false, method: 'update' });
47
+ }
48
+ else {
49
+ await this.Model.add(this.toPrimitive(entity));
50
+ }
51
+ }
52
+ async getById(id) {
53
+ const models = await this.#fetchAll({
54
+ filter: `id:'${id}'`,
55
+ limit: 1
56
+ });
57
+ if (models.length === 1) {
58
+ return models[0];
59
+ }
60
+ return null;
61
+ }
62
+ async #fetchAll(options = {}) {
63
+ const { filter, order, page, limit } = options;
64
+ if (page !== undefined) {
65
+ if (page < 1) {
66
+ throw new errors_1.default.BadRequestError({ message: 'page must be greater or equal to 1' });
67
+ }
68
+ if (limit !== undefined && limit < 1) {
69
+ throw new errors_1.default.BadRequestError({ message: 'limit must be greater or equal to 1' });
70
+ }
71
+ }
72
+ const collection = this.Model.getFilteredCollection({
73
+ filter,
74
+ mongoTransformer: this.#getNQLKeyTransformer()
75
+ });
76
+ const orderString = this.#orderToString(order);
77
+ collection
78
+ .query((q) => {
79
+ this.applyCustomQuery(q, options);
80
+ if (limit) {
81
+ q.limit(limit);
82
+ }
83
+ if (limit && page) {
84
+ q.limit(limit);
85
+ q.offset(limit * (page - 1));
86
+ }
87
+ if (orderString) {
88
+ q.orderByRaw(orderString);
89
+ }
90
+ });
91
+ const models = await collection.fetchAll();
92
+ return (await Promise.all(models.map(model => this.modelToEntity(model)))).filter(entity => !!entity);
93
+ }
94
+ async getAll({ filter, order, include } = {}) {
95
+ return this.#fetchAll({
96
+ filter,
97
+ order,
98
+ include
99
+ });
100
+ }
101
+ async getPage({ filter, order, page, limit, include }) {
102
+ return this.#fetchAll({
103
+ filter,
104
+ order,
105
+ page,
106
+ limit,
107
+ include
108
+ });
109
+ }
110
+ async getCount({ filter } = {}) {
111
+ const collection = this.Model.getFilteredCollection({
112
+ filter,
113
+ mongoTransformer: this.#getNQLKeyTransformer()
114
+ });
115
+ return await collection.count();
116
+ }
117
+ async getGroupedCount({ filter, groupBy }) {
118
+ const columnName = this.#entityFieldToColumn(groupBy);
119
+ const data = (await this.Model.getFilteredCollection({
120
+ filter,
121
+ mongoTransformer: this.#getNQLKeyTransformer()
122
+ }).query()
123
+ .select(columnName)
124
+ .count('* as count')
125
+ .groupBy(columnName));
126
+ return data.map((row) => {
127
+ return {
128
+ count: row.count,
129
+ [groupBy]: row[columnName]
130
+ };
131
+ });
132
+ }
133
+ }
134
+ exports.BookshelfRepository = BookshelfRepository;
@@ -0,0 +1,196 @@
1
+ /* eslint-disable ghost/filenames/match-exported-class */
2
+
3
+ import {Knex} from 'knex';
4
+ import {mapKeys, chainTransformers} from '@tryghost/mongo-utils';
5
+ import errors from '@tryghost/errors';
6
+
7
+ type Entity<T> = {
8
+ id: T;
9
+ deleted: boolean
10
+ }
11
+
12
+ type Order<T> = {
13
+ field: keyof T;
14
+ direction: 'asc' | 'desc';
15
+ }
16
+
17
+ export type ModelClass<T> = {
18
+ destroy: (data: {id: T}) => Promise<void>;
19
+ findOne: (data: {id: T}, options?: {require?: boolean}) => Promise<ModelInstance<T> | null>;
20
+ add: (data: object) => Promise<ModelInstance<T>>;
21
+ getFilteredCollection: (options: {filter?: string, mongoTransformer?: unknown}) => {
22
+ count(): Promise<number>,
23
+ query: (f?: (q: Knex.QueryBuilder) => void) => Knex.QueryBuilder,
24
+ fetchAll: () => Promise<ModelInstance<T>[]>
25
+ };
26
+ }
27
+
28
+ export type ModelInstance<T> = {
29
+ id: T;
30
+ get(field: string): unknown;
31
+ set(data: object|string, value?: unknown): void;
32
+ save(properties: object, options?: {autoRefresh?: boolean; method?: 'update' | 'insert'}): Promise<ModelInstance<T>>;
33
+ }
34
+
35
+ type OptionalPropertyOf<T extends object> = Exclude<{
36
+ [K in keyof T]: T extends Record<K, Exclude<T[K], undefined>>
37
+ ? never
38
+ : K
39
+ }[keyof T], undefined>
40
+
41
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
+ export type OrderOption<T extends Entity<any> = any> = Order<T>[];
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ export type IncludeOption<T extends Entity<any> = any> = OptionalPropertyOf<T>[];
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ export type AllOptions<T extends Entity<any> = any> = { filter?: string; order?: OrderOption<T>; page?: number; limit?: number, include?: IncludeOption<T> }
47
+
48
+ export abstract class BookshelfRepository<IDType, T extends Entity<IDType>> {
49
+ protected Model: ModelClass<IDType>;
50
+
51
+ constructor(Model: ModelClass<IDType>) {
52
+ this.Model = Model;
53
+ }
54
+
55
+ protected abstract toPrimitive(entity: T): object;
56
+ protected abstract modelToEntity (model: ModelInstance<IDType>): Promise<T|null> | T | null
57
+ protected abstract getFieldToColumnMap(): Record<keyof T, string>;
58
+
59
+ /**
60
+ * override this method to add custom query logic to knex queries
61
+ */
62
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
63
+ applyCustomQuery(query: Knex.QueryBuilder, options: AllOptions<T>) {
64
+ return;
65
+ }
66
+
67
+ #entityFieldToColumn(field: keyof T): string {
68
+ const mapping = this.getFieldToColumnMap();
69
+ return mapping[field];
70
+ }
71
+
72
+ #orderToString(order?: OrderOption<T>) {
73
+ if (!order || order.length === 0) {
74
+ return;
75
+ }
76
+ return order.map(({field, direction}) => `${this.#entityFieldToColumn(field)} ${direction}`).join(',');
77
+ }
78
+
79
+ /**
80
+ * Map all the fields in an NQL filter to the names of the model
81
+ */
82
+ #getNQLKeyTransformer() {
83
+ return chainTransformers(...mapKeys(this.getFieldToColumnMap()));
84
+ }
85
+
86
+ async save(entity: T): Promise<void> {
87
+ if (entity.deleted) {
88
+ await this.Model.destroy({id: entity.id});
89
+ return;
90
+ }
91
+
92
+ const existing = await this.Model.findOne({id: entity.id}, {require: false});
93
+ if (existing) {
94
+ existing.set(this.toPrimitive(entity));
95
+ await existing.save({}, {autoRefresh: false, method: 'update'});
96
+ } else {
97
+ await this.Model.add(this.toPrimitive(entity));
98
+ }
99
+ }
100
+
101
+ async getById(id: IDType): Promise<T | null> {
102
+ const models = await this.#fetchAll({
103
+ filter: `id:'${id}'`,
104
+ limit: 1
105
+ });
106
+ if (models.length === 1) {
107
+ return models[0];
108
+ }
109
+ return null;
110
+ }
111
+
112
+ async #fetchAll(options: AllOptions<T> = {}): Promise<T[]> {
113
+ const {filter, order, page, limit} = options;
114
+ if (page !== undefined) {
115
+ if (page < 1) {
116
+ throw new errors.BadRequestError({message: 'page must be greater or equal to 1'});
117
+ }
118
+ if (limit !== undefined && limit < 1) {
119
+ throw new errors.BadRequestError({message: 'limit must be greater or equal to 1'});
120
+ }
121
+ }
122
+
123
+ const collection = this.Model.getFilteredCollection({
124
+ filter,
125
+ mongoTransformer: this.#getNQLKeyTransformer()
126
+ });
127
+ const orderString = this.#orderToString(order);
128
+
129
+ collection
130
+ .query((q) => {
131
+ this.applyCustomQuery(q, options);
132
+
133
+ if (limit) {
134
+ q.limit(limit);
135
+ }
136
+ if (limit && page) {
137
+ q.limit(limit);
138
+ q.offset(limit * (page - 1));
139
+ }
140
+
141
+ if (orderString) {
142
+ q.orderByRaw(
143
+ orderString
144
+ );
145
+ }
146
+ });
147
+
148
+ const models = await collection.fetchAll();
149
+ return (await Promise.all(models.map(model => this.modelToEntity(model)))).filter(entity => !!entity) as T[];
150
+ }
151
+
152
+ async getAll({filter, order, include}: Omit<AllOptions<T>, 'page'|'limit'> = {}): Promise<T[]> {
153
+ return this.#fetchAll({
154
+ filter,
155
+ order,
156
+ include
157
+ });
158
+ }
159
+
160
+ async getPage({filter, order, page, limit, include}: AllOptions<T> & Required<Pick<AllOptions<T>, 'page'|'limit'>>): Promise<T[]> {
161
+ return this.#fetchAll({
162
+ filter,
163
+ order,
164
+ page,
165
+ limit,
166
+ include
167
+ });
168
+ }
169
+
170
+ async getCount({filter}: { filter?: string } = {}): Promise<number> {
171
+ const collection = this.Model.getFilteredCollection({
172
+ filter,
173
+ mongoTransformer: this.#getNQLKeyTransformer()
174
+ });
175
+ return await collection.count();
176
+ }
177
+
178
+ async getGroupedCount<K extends keyof T>({filter, groupBy}: { filter?: string, groupBy: K }): Promise<({count: number} & Record<K, T[K]>)[]> {
179
+ const columnName = this.#entityFieldToColumn(groupBy);
180
+
181
+ const data = (await this.Model.getFilteredCollection({
182
+ filter,
183
+ mongoTransformer: this.#getNQLKeyTransformer()
184
+ }).query()
185
+ .select(columnName)
186
+ .count('* as count')
187
+ .groupBy(columnName)) as ({count: number} & Record<string, T[K]>)[];
188
+
189
+ return data.map((row) => {
190
+ return {
191
+ count: row.count,
192
+ [groupBy]: row[columnName]
193
+ };
194
+ }) as ({count: number} & Record<K, T[K]>)[];
195
+ }
196
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BookshelfSubscribeEventRepository = void 0;
7
+ const BookshelfRepository_1 = require("./BookshelfRepository");
8
+ const logging_1 = __importDefault(require("@tryghost/logging"));
9
+ const SubscribeEvent_1 = require("./SubscribeEvent");
10
+ class BookshelfSubscribeEventRepository extends BookshelfRepository_1.BookshelfRepository {
11
+ sentry;
12
+ constructor(Model, deps = {}) {
13
+ super(Model);
14
+ this.sentry = deps.sentry;
15
+ }
16
+ toPrimitive(entity) {
17
+ return {
18
+ id: entity.id,
19
+ recommendation_id: entity.recommendationId,
20
+ member_id: entity.memberId,
21
+ created_at: entity.createdAt
22
+ };
23
+ }
24
+ modelToEntity(model) {
25
+ try {
26
+ return SubscribeEvent_1.SubscribeEvent.create({
27
+ id: model.id,
28
+ recommendationId: model.get('recommendation_id'),
29
+ memberId: model.get('member_id'),
30
+ createdAt: model.get('created_at')
31
+ });
32
+ }
33
+ catch (err) {
34
+ logging_1.default.error(err);
35
+ this.sentry?.captureException(err);
36
+ return null;
37
+ }
38
+ }
39
+ getFieldToColumnMap() {
40
+ return {
41
+ id: 'id',
42
+ recommendationId: 'recommendation_id',
43
+ memberId: 'member_id',
44
+ createdAt: 'created_at'
45
+ };
46
+ }
47
+ }
48
+ exports.BookshelfSubscribeEventRepository = BookshelfSubscribeEventRepository;
@@ -0,0 +1,49 @@
1
+ import {BookshelfRepository, ModelClass, ModelInstance} from './BookshelfRepository';
2
+ import logger from '@tryghost/logging';
3
+ import {SubscribeEvent} from './SubscribeEvent';
4
+
5
+ type Sentry = {
6
+ captureException(err: unknown): void;
7
+ }
8
+
9
+ export class BookshelfSubscribeEventRepository extends BookshelfRepository<string, SubscribeEvent> {
10
+ sentry?: Sentry;
11
+
12
+ constructor(Model: ModelClass<string>, deps: {sentry?: Sentry} = {}) {
13
+ super(Model);
14
+ this.sentry = deps.sentry;
15
+ }
16
+
17
+ toPrimitive(entity: SubscribeEvent): object {
18
+ return {
19
+ id: entity.id,
20
+ recommendation_id: entity.recommendationId,
21
+ member_id: entity.memberId,
22
+ created_at: entity.createdAt
23
+ };
24
+ }
25
+
26
+ modelToEntity(model: ModelInstance<string>): SubscribeEvent | null {
27
+ try {
28
+ return SubscribeEvent.create({
29
+ id: model.id,
30
+ recommendationId: model.get('recommendation_id') as string,
31
+ memberId: model.get('member_id') as string,
32
+ createdAt: model.get('created_at') as Date
33
+ });
34
+ } catch (err) {
35
+ logger.error(err);
36
+ this.sentry?.captureException(err);
37
+ return null;
38
+ }
39
+ }
40
+
41
+ getFieldToColumnMap() {
42
+ return {
43
+ id: 'id',
44
+ recommendationId: 'recommendation_id',
45
+ memberId: 'member_id',
46
+ createdAt: 'created_at'
47
+ } as Record<keyof SubscribeEvent, string>;
48
+ }
49
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ClickEvent = void 0;
7
+ const bson_objectid_1 = __importDefault(require("bson-objectid"));
8
+ class ClickEvent {
9
+ id;
10
+ recommendationId;
11
+ memberId;
12
+ createdAt;
13
+ get deleted() {
14
+ return false;
15
+ }
16
+ constructor(data) {
17
+ this.id = data.id;
18
+ this.recommendationId = data.recommendationId;
19
+ this.memberId = data.memberId;
20
+ this.createdAt = data.createdAt;
21
+ }
22
+ static create(data) {
23
+ const id = data.id ?? (0, bson_objectid_1.default)().toString();
24
+ const d = {
25
+ id,
26
+ recommendationId: data.recommendationId,
27
+ memberId: data.memberId ?? null,
28
+ createdAt: data.createdAt ?? new Date()
29
+ };
30
+ return new ClickEvent(d);
31
+ }
32
+ }
33
+ exports.ClickEvent = ClickEvent;
@@ -0,0 +1,32 @@
1
+ import ObjectId from 'bson-objectid';
2
+
3
+ export class ClickEvent {
4
+ id: string;
5
+ recommendationId: string;
6
+ memberId: string|null;
7
+ createdAt: Date;
8
+
9
+ get deleted() {
10
+ return false;
11
+ }
12
+
13
+ private constructor(data: {id: string, recommendationId: string, memberId: string|null, createdAt: Date}) {
14
+ this.id = data.id;
15
+ this.recommendationId = data.recommendationId;
16
+ this.memberId = data.memberId;
17
+ this.createdAt = data.createdAt;
18
+ }
19
+
20
+ static create(data: {id?: string, recommendationId: string, memberId?: string|null, createdAt?: Date}) {
21
+ const id = data.id ?? ObjectId().toString();
22
+
23
+ const d = {
24
+ id,
25
+ recommendationId: data.recommendationId,
26
+ memberId: data.memberId ?? null,
27
+ createdAt: data.createdAt ?? new Date()
28
+ };
29
+
30
+ return new ClickEvent(d);
31
+ }
32
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InMemoryRecommendationRepository = void 0;
4
+ const InMemoryRepository_1 = require("../../lib/InMemoryRepository");
5
+ class InMemoryRecommendationRepository extends InMemoryRepository_1.InMemoryRepository {
6
+ toPrimitive(entity) {
7
+ return entity;
8
+ }
9
+ async getByUrl(url) {
10
+ // Find URL based on the hostname and pathname.
11
+ // Query params, hash fragements, protocol and www are ignored.
12
+ const existing = this.store.find((r) => {
13
+ return r.url.hostname.replace('www.', '') === url.hostname.replace('www.', '') &&
14
+ r.url.pathname.replace(/\/$/, '') === url.pathname.replace(/\/$/, '');
15
+ }) || null;
16
+ return existing;
17
+ }
18
+ }
19
+ exports.InMemoryRecommendationRepository = InMemoryRecommendationRepository;
@@ -0,0 +1,20 @@
1
+ import {Recommendation} from './Recommendation';
2
+ import {RecommendationRepository} from './RecommendationRepository';
3
+ import {InMemoryRepository} from '../../lib/InMemoryRepository';
4
+
5
+ export class InMemoryRecommendationRepository extends InMemoryRepository<string, Recommendation> implements RecommendationRepository {
6
+ toPrimitive(entity: Recommendation): object {
7
+ return entity;
8
+ }
9
+
10
+ async getByUrl(url: URL): Promise<Recommendation | null > {
11
+ // Find URL based on the hostname and pathname.
12
+ // Query params, hash fragements, protocol and www are ignored.
13
+ const existing = this.store.find((r) => {
14
+ return r.url.hostname.replace('www.', '') === url.hostname.replace('www.', '') &&
15
+ r.url.pathname.replace(/\/$/, '') === url.pathname.replace(/\/$/, '');
16
+ }) || null;
17
+
18
+ return existing;
19
+ }
20
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IncomingRecommendationController = void 0;
4
+ const UnsafeData_1 = require("./UnsafeData");
5
+ class IncomingRecommendationController {
6
+ service;
7
+ constructor(deps) {
8
+ this.service = deps.service;
9
+ }
10
+ async browse(frame) {
11
+ const options = new UnsafeData_1.UnsafeData(frame.options);
12
+ const page = options.optionalKey('page')?.integer ?? 1;
13
+ const limit = options.optionalKey('limit')?.integer ?? 5;
14
+ const { incomingRecommendations, meta } = await this.service.listIncomingRecommendations({ page, limit });
15
+ return this.#serialize(incomingRecommendations, meta);
16
+ }
17
+ #serialize(recommendations, meta) {
18
+ return {
19
+ data: recommendations.map((entity) => {
20
+ return {
21
+ id: entity.id,
22
+ title: entity.title,
23
+ excerpt: entity.excerpt,
24
+ featured_image: entity.featuredImage?.toString() ?? null,
25
+ favicon: entity.favicon?.toString() ?? null,
26
+ url: entity.url.toString(),
27
+ recommending_back: !!entity.recommendingBack
28
+ };
29
+ }),
30
+ meta
31
+ };
32
+ }
33
+ }
34
+ exports.IncomingRecommendationController = IncomingRecommendationController;