ghost 5.115.0 → 5.116.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 (341) hide show
  1. package/components/{tryghost-api-framework-5.115.0.tgz → tryghost-api-framework-5.116.0.tgz} +0 -0
  2. package/components/tryghost-constants-5.116.0.tgz +0 -0
  3. package/components/tryghost-custom-fonts-5.116.0.tgz +0 -0
  4. package/components/{tryghost-custom-theme-settings-service-5.115.0.tgz → tryghost-custom-theme-settings-service-5.116.0.tgz} +0 -0
  5. package/components/tryghost-domain-events-5.116.0.tgz +0 -0
  6. package/components/{tryghost-donations-5.115.0.tgz → tryghost-donations-5.116.0.tgz} +0 -0
  7. package/components/tryghost-email-addresses-5.116.0.tgz +0 -0
  8. package/components/tryghost-email-service-5.116.0.tgz +0 -0
  9. package/components/tryghost-email-suppression-list-5.116.0.tgz +0 -0
  10. package/components/tryghost-html-to-plaintext-5.116.0.tgz +0 -0
  11. package/components/tryghost-i18n-5.116.0.tgz +0 -0
  12. package/components/tryghost-job-manager-5.116.0.tgz +0 -0
  13. package/components/tryghost-link-replacer-5.116.0.tgz +0 -0
  14. package/components/tryghost-magic-link-5.116.0.tgz +0 -0
  15. package/components/tryghost-member-attribution-5.116.0.tgz +0 -0
  16. package/components/tryghost-member-events-5.116.0.tgz +0 -0
  17. package/components/tryghost-members-api-5.116.0.tgz +0 -0
  18. package/components/tryghost-members-csv-5.116.0.tgz +0 -0
  19. package/components/{tryghost-members-offers-5.115.0.tgz → tryghost-members-offers-5.116.0.tgz} +0 -0
  20. package/components/{tryghost-milestones-5.115.0.tgz → tryghost-milestones-5.116.0.tgz} +0 -0
  21. package/components/{tryghost-mw-error-handler-5.115.0.tgz → tryghost-mw-error-handler-5.116.0.tgz} +0 -0
  22. package/components/tryghost-mw-vhost-5.116.0.tgz +0 -0
  23. package/components/{tryghost-post-events-5.115.0.tgz → tryghost-post-events-5.116.0.tgz} +0 -0
  24. package/components/{tryghost-post-revisions-5.115.0.tgz → tryghost-post-revisions-5.116.0.tgz} +0 -0
  25. package/components/tryghost-posts-service-5.116.0.tgz +0 -0
  26. package/components/{tryghost-prometheus-metrics-5.115.0.tgz → tryghost-prometheus-metrics-5.116.0.tgz} +0 -0
  27. package/components/tryghost-security-5.116.0.tgz +0 -0
  28. package/components/{tryghost-tiers-5.115.0.tgz → tryghost-tiers-5.116.0.tgz} +0 -0
  29. package/components/tryghost-webmentions-5.116.0.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 +11 -6
  37. package/content/themes/source/partials/feature-image.hbs +2 -2
  38. package/core/boot.js +3 -43
  39. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +30494 -29403
  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-0040480a.mjs → index-a9601514.mjs} +5 -4
  42. package/core/built/admin/assets/admin-x-demo/{modals-fb35c86c.mjs → modals-c1789d04.mjs} +67 -65
  43. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-806ef39c.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-376f847c.mjs → index-84580c3a.mjs} +2 -2
  46. package/core/built/admin/assets/admin-x-settings/{index-8fa19303.mjs → index-f744cab7.mjs} +3147 -3123
  47. package/core/built/admin/assets/admin-x-settings/{modals-36775d71.mjs → modals-d9ca60c5.mjs} +1198 -1192
  48. package/core/built/admin/assets/chunk.524.8371443ef8f60db429d0.js +35 -0
  49. package/core/built/admin/assets/chunk.582.f90151775f2e53dd21d9.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-938b3d9c29e3564a53a22f8c8f82d351.js → ghost-03b64c086f3c60cabc85fe7a7e2b640a.js} +272 -251
  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 +75373 -0
  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 +8 -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/frontend/src/cards/css/cta.css +1 -1
  71. package/core/server/adapters/cache/Redis.js +1 -1
  72. package/core/server/adapters/lib/redis/AdapterCacheRedis.js +287 -0
  73. package/core/server/adapters/lib/redis/redis-store-factory.js +22 -0
  74. package/core/server/api/endpoints/posts.js +9 -3
  75. package/core/server/api/endpoints/previews.js +35 -1
  76. package/core/server/api/endpoints/slugs.js +6 -2
  77. package/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js +6 -9
  78. package/core/server/api/endpoints/utils/validators/input/settings.js +1 -1
  79. package/core/server/data/db/connection.js +2 -0
  80. package/core/server/data/db/index.js +1 -0
  81. package/core/server/data/importer/handlers/ImporterContentFileHandler.js +90 -0
  82. package/core/server/data/importer/import-manager.js +3 -3
  83. package/core/server/data/importer/importers/importer-revue.js +128 -0
  84. package/core/server/data/importer/importers/json-to-html.js +107 -0
  85. package/core/server/data/migrations/utils/tables.js +2 -4
  86. package/core/server/data/seeders/DataGenerator.js +288 -0
  87. package/core/server/data/seeders/importers/BenefitsImporter.js +28 -0
  88. package/core/server/data/seeders/importers/CommentsImporter.js +73 -0
  89. package/core/server/data/seeders/importers/EmailBatchesImporter.js +38 -0
  90. package/core/server/data/seeders/importers/EmailRecipientFailuresImporter.js +67 -0
  91. package/core/server/data/seeders/importers/EmailRecipientsImporter.js +212 -0
  92. package/core/server/data/seeders/importers/EmailsImporter.js +99 -0
  93. package/core/server/data/seeders/importers/LabelsImporter.js +41 -0
  94. package/core/server/data/seeders/importers/MembersClickEventsImporter.js +69 -0
  95. package/core/server/data/seeders/importers/MembersCreatedEventsImporter.js +103 -0
  96. package/core/server/data/seeders/importers/MembersFeedbackImporter.js +45 -0
  97. package/core/server/data/seeders/importers/MembersImporter.js +111 -0
  98. package/core/server/data/seeders/importers/MembersLabelsImporter.js +39 -0
  99. package/core/server/data/seeders/importers/MembersLoginEventsImporter.js +69 -0
  100. package/core/server/data/seeders/importers/MembersNewslettersImporter.js +38 -0
  101. package/core/server/data/seeders/importers/MembersPaidSubscriptionEventsImporter.js +99 -0
  102. package/core/server/data/seeders/importers/MembersProductsImporter.js +42 -0
  103. package/core/server/data/seeders/importers/MembersStatusEventsImporter.js +58 -0
  104. package/core/server/data/seeders/importers/MembersStripeCustomersImporter.js +60 -0
  105. package/core/server/data/seeders/importers/MembersStripeCustomersSubscriptionsImporter.js +259 -0
  106. package/core/server/data/seeders/importers/MembersSubscribeEventsImporter.js +69 -0
  107. package/core/server/data/seeders/importers/MembersSubscriptionCreatedEventsImporter.js +95 -0
  108. package/core/server/data/seeders/importers/NewslettersImporter.js +40 -0
  109. package/core/server/data/seeders/importers/OffersImporter.js +70 -0
  110. package/core/server/data/seeders/importers/PostsAuthorsImporter.js +32 -0
  111. package/core/server/data/seeders/importers/PostsImporter.js +102 -0
  112. package/core/server/data/seeders/importers/PostsProductsImporter.js +35 -0
  113. package/core/server/data/seeders/importers/PostsTagsImporter.js +46 -0
  114. package/core/server/data/seeders/importers/ProductsBenefitsImporter.js +54 -0
  115. package/core/server/data/seeders/importers/ProductsImporter.js +90 -0
  116. package/core/server/data/seeders/importers/RecommendationClickEventsImporter.js +32 -0
  117. package/core/server/data/seeders/importers/RecommendationSubscribeEventsImporter.js +32 -0
  118. package/core/server/data/seeders/importers/RecommendationsImporter.js +34 -0
  119. package/core/server/data/seeders/importers/RedirectsImporter.js +49 -0
  120. package/core/server/data/seeders/importers/RolesUsersImporter.js +42 -0
  121. package/core/server/data/seeders/importers/StripePricesImporter.js +69 -0
  122. package/core/server/data/seeders/importers/StripeProductsImporter.js +34 -0
  123. package/core/server/data/seeders/importers/TableImporter.js +187 -0
  124. package/core/server/data/seeders/importers/TagsImporter.js +41 -0
  125. package/core/server/data/seeders/importers/UsersImporter.js +31 -0
  126. package/core/server/data/seeders/importers/WebMentionsImporter.js +42 -0
  127. package/core/server/data/seeders/importers/index.js +41 -0
  128. package/core/server/data/seeders/utils/JsonImporter.js +39 -0
  129. package/core/server/data/seeders/utils/blog-info.js +3 -0
  130. package/core/server/data/seeders/utils/database-date.js +7 -0
  131. package/core/server/data/seeders/utils/event-generator.js +48 -0
  132. package/core/server/data/seeders/utils/random.js +13 -0
  133. package/core/server/data/seeders/utils/topological-sort.js +33 -0
  134. package/core/server/lib/bootstrap-socket.js +87 -0
  135. package/core/server/lib/package-json/index.js +1 -0
  136. package/core/server/lib/package-json/package-json.js +160 -0
  137. package/core/server/lib/package-json/parse.js +57 -0
  138. package/core/server/models/base/plugins/actions.js +44 -31
  139. package/core/server/models/base/plugins/generate-slug.js +6 -0
  140. package/core/server/notify.js +1 -1
  141. package/core/server/services/activitypub/ActivityPubService.ts +1 -1
  142. package/core/server/services/adapter-manager/AdapterManager.js +161 -0
  143. package/core/server/services/adapter-manager/index.js +1 -1
  144. package/core/server/services/announcement-bar-service/AnnouncementBarSettings.js +54 -0
  145. package/core/server/services/announcement-bar-service/AnnouncementVisibilityValues.js +11 -0
  146. package/core/server/services/announcement-bar-service/index.js +1 -1
  147. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +99 -0
  148. package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +80 -0
  149. package/core/server/services/api-version-compatibility/extract-api-key.js +57 -0
  150. package/core/server/services/api-version-compatibility/index.js +2 -2
  151. package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +31 -0
  152. package/core/server/services/audience-feedback/AudienceFeedbackController.js +85 -0
  153. package/core/server/services/audience-feedback/AudienceFeedbackService.js +34 -0
  154. package/core/server/services/audience-feedback/Feedback.js +35 -0
  155. package/core/server/services/audience-feedback/index.js +4 -2
  156. package/core/server/services/auth/session/emails/signin.js +168 -0
  157. package/core/server/services/auth/session/index.js +2 -2
  158. package/core/server/services/auth/session/session-from-token.js +69 -0
  159. package/core/server/services/auth/session/session-service.js +374 -0
  160. package/core/server/services/custom-redirects/index.js +1 -1
  161. package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +62 -0
  162. package/core/server/services/email-analytics/EmailAnalyticsService.js +552 -0
  163. package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +3 -3
  164. package/core/server/services/email-analytics/EventProcessingResult.js +66 -0
  165. package/core/server/services/email-service/EmailServiceWrapper.js +4 -4
  166. package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +1 -1
  167. package/core/server/services/email-suppression-list/service.js +1 -1
  168. package/core/server/services/explore-ping/ExplorePingService.js +106 -0
  169. package/core/server/services/explore-ping/index.js +31 -0
  170. package/core/server/services/identity-tokens/IdentityTokenService.js +30 -0
  171. package/core/server/services/identity-tokens/IdentityTokenService.ts +28 -0
  172. package/core/server/services/identity-tokens/IdentityTokenServiceWrapper.js +1 -1
  173. package/core/server/services/invitations/accept.js +5 -2
  174. package/core/server/services/lib/DynamicRedirectManager.js +156 -0
  175. package/core/server/services/lib/EmailContentGenerator.js +54 -0
  176. package/core/server/services/lib/InMemoryRepository.js +62 -0
  177. package/core/server/services/lib/InMemoryRepository.ts +80 -0
  178. package/core/server/services/lib/MailgunClient.js +364 -0
  179. package/core/server/services/link-redirection/LinkRedirect.js +26 -0
  180. package/core/server/services/link-redirection/LinkRedirectRepository.js +7 -7
  181. package/core/server/services/link-redirection/LinkRedirectsService.js +123 -0
  182. package/core/server/services/link-redirection/README.md +151 -0
  183. package/core/server/services/link-redirection/RedirectEvent.js +24 -0
  184. package/core/server/services/link-redirection/index.js +1 -1
  185. package/core/server/services/link-tracking/LinkClickTrackingService.js +1 -1
  186. package/core/server/services/mail/index.js +1 -1
  187. package/core/server/services/mail-events/BookshelfMailEventRepository.js +2 -2
  188. package/core/server/services/mail-events/InMemoryMailEventRepository.js +10 -0
  189. package/core/server/services/mail-events/InMemoryMailEventRepository.ts +8 -0
  190. package/core/server/services/mail-events/MailEvent.js +20 -0
  191. package/core/server/services/mail-events/MailEvent.ts +10 -0
  192. package/core/server/services/mail-events/MailEventRepository.js +2 -0
  193. package/core/server/services/mail-events/MailEventRepository.ts +5 -0
  194. package/core/server/services/mail-events/MailEventService.js +124 -0
  195. package/core/server/services/mail-events/MailEventService.ts +169 -0
  196. package/core/server/services/mail-events/index.js +1 -1
  197. package/core/server/services/mail-events/libraries.d.ts +2 -0
  198. package/core/server/services/members/CaptchaService.js +80 -0
  199. package/core/server/services/members/api.js +1 -1
  200. package/core/server/services/members/importer/MembersCSVImporter.js +464 -0
  201. package/core/server/services/members/importer/MembersCSVImporterStripeUtils.js +194 -0
  202. package/core/server/services/members/importer/email-template.js +182 -0
  203. package/core/server/services/members/importer/index.js +30 -0
  204. package/core/server/services/members/members-ssr.js +333 -0
  205. package/core/server/services/members/service.js +2 -2
  206. package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
  207. package/core/server/services/offers/service.js +1 -1
  208. package/core/server/services/posts/stats/PostStats.js +13 -0
  209. package/core/server/services/recommendations/RecommendationServiceWrapper.js +8 -8
  210. package/core/server/services/recommendations/service/BookshelfClickEventRepository.js +48 -0
  211. package/core/server/services/recommendations/service/BookshelfClickEventRepository.ts +49 -0
  212. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.js +98 -0
  213. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.ts +117 -0
  214. package/core/server/services/recommendations/service/BookshelfRepository.js +134 -0
  215. package/core/server/services/recommendations/service/BookshelfRepository.ts +196 -0
  216. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.js +48 -0
  217. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.ts +49 -0
  218. package/core/server/services/recommendations/service/ClickEvent.js +33 -0
  219. package/core/server/services/recommendations/service/ClickEvent.ts +32 -0
  220. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.js +19 -0
  221. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.ts +20 -0
  222. package/core/server/services/recommendations/service/IncomingRecommendationController.js +34 -0
  223. package/core/server/services/recommendations/service/IncomingRecommendationController.ts +51 -0
  224. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.js +25 -0
  225. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.ts +37 -0
  226. package/core/server/services/recommendations/service/IncomingRecommendationService.js +93 -0
  227. package/core/server/services/recommendations/service/IncomingRecommendationService.ts +160 -0
  228. package/core/server/services/recommendations/service/Recommendation.js +140 -0
  229. package/core/server/services/recommendations/service/Recommendation.ts +201 -0
  230. package/core/server/services/recommendations/service/RecommendationController.js +208 -0
  231. package/core/server/services/recommendations/service/RecommendationController.ts +258 -0
  232. package/core/server/services/recommendations/service/RecommendationMetadataService.js +86 -0
  233. package/core/server/services/recommendations/service/RecommendationMetadataService.ts +128 -0
  234. package/core/server/services/recommendations/service/RecommendationRepository.js +2 -0
  235. package/core/server/services/recommendations/service/RecommendationRepository.ts +13 -0
  236. package/core/server/services/recommendations/service/RecommendationService.js +228 -0
  237. package/core/server/services/recommendations/service/RecommendationService.ts +281 -0
  238. package/core/server/services/recommendations/service/SubscribeEvent.js +33 -0
  239. package/core/server/services/recommendations/service/SubscribeEvent.ts +32 -0
  240. package/core/server/services/recommendations/service/UnsafeData.js +183 -0
  241. package/core/server/services/recommendations/service/UnsafeData.ts +217 -0
  242. package/core/server/services/recommendations/service/WellknownService.js +36 -0
  243. package/core/server/services/recommendations/service/WellknownService.ts +47 -0
  244. package/core/server/services/recommendations/service/index.js +31 -0
  245. package/core/server/services/recommendations/service/index.ts +15 -0
  246. package/core/server/services/recommendations/service/libraries.d.ts +5 -0
  247. package/core/server/services/route-settings/SettingsPathManager.js +47 -0
  248. package/core/server/services/route-settings/index.js +1 -1
  249. package/core/server/services/slack-notifications/SlackNotifications.js +211 -0
  250. package/core/server/services/slack-notifications/SlackNotificationsService.js +90 -0
  251. package/core/server/services/slack-notifications/service.js +4 -6
  252. package/core/server/services/stripe/README.md +63 -0
  253. package/core/server/services/stripe/StripeAPI.js +931 -0
  254. package/core/server/services/stripe/StripeMigrations.js +613 -0
  255. package/core/server/services/stripe/StripeService.js +175 -0
  256. package/core/server/services/stripe/WebhookController.js +100 -0
  257. package/core/server/services/stripe/WebhookManager.js +175 -0
  258. package/core/server/services/stripe/events/StripeLiveDisabledEvent.js +23 -0
  259. package/core/server/services/stripe/events/StripeLiveEnabledEvent.js +23 -0
  260. package/core/server/services/stripe/events/index.js +4 -0
  261. package/core/server/services/stripe/service.js +1 -1
  262. package/core/server/services/stripe/services/webhook/CheckoutSessionEventService.js +255 -0
  263. package/core/server/services/stripe/services/webhook/InvoiceEventService.js +70 -0
  264. package/core/server/services/stripe/services/webhook/SubscriptionEventService.js +54 -0
  265. package/core/server/services/themes/loader.js +1 -1
  266. package/core/server/services/themes/to-json.js +1 -1
  267. package/core/server/web/api/endpoints/admin/app.js +1 -21
  268. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  269. package/core/server/web/api/middleware/version-match.js +41 -0
  270. package/core/server/web/shared/middleware/cache-control.js +51 -0
  271. package/core/server/web/shared/middleware/index.js +1 -1
  272. package/core/server/web/well-known.js +1 -1
  273. package/core/shared/labs.js +5 -3
  274. package/core/shared/settings-cache/CacheManager.js +64 -6
  275. package/package.json +98 -146
  276. package/tsconfig.tsbuildinfo +1 -1
  277. package/yarn.lock +1478 -1634
  278. package/components/tryghost-adapter-cache-redis-5.115.0.tgz +0 -0
  279. package/components/tryghost-adapter-manager-5.115.0.tgz +0 -0
  280. package/components/tryghost-announcement-bar-settings-5.115.0.tgz +0 -0
  281. package/components/tryghost-api-version-compatibility-service-5.115.0.tgz +0 -0
  282. package/components/tryghost-audience-feedback-5.115.0.tgz +0 -0
  283. package/components/tryghost-bookshelf-repository-5.115.0.tgz +0 -0
  284. package/components/tryghost-bootstrap-socket-5.115.0.tgz +0 -0
  285. package/components/tryghost-captcha-service-5.115.0.tgz +0 -0
  286. package/components/tryghost-constants-5.115.0.tgz +0 -0
  287. package/components/tryghost-custom-fonts-5.115.0.tgz +0 -0
  288. package/components/tryghost-data-generator-5.115.0.tgz +0 -0
  289. package/components/tryghost-domain-events-5.115.0.tgz +0 -0
  290. package/components/tryghost-email-addresses-5.115.0.tgz +0 -0
  291. package/components/tryghost-email-analytics-provider-mailgun-5.115.0.tgz +0 -0
  292. package/components/tryghost-email-analytics-service-5.115.0.tgz +0 -0
  293. package/components/tryghost-email-content-generator-5.115.0.tgz +0 -0
  294. package/components/tryghost-email-events-5.115.0.tgz +0 -0
  295. package/components/tryghost-email-service-5.115.0.tgz +0 -0
  296. package/components/tryghost-email-suppression-list-5.115.0.tgz +0 -0
  297. package/components/tryghost-express-dynamic-redirects-5.115.0.tgz +0 -0
  298. package/components/tryghost-extract-api-key-5.115.0.tgz +0 -0
  299. package/components/tryghost-ghost-5.115.0.tgz +0 -0
  300. package/components/tryghost-html-to-plaintext-5.115.0.tgz +0 -0
  301. package/components/tryghost-i18n-5.115.0.tgz +0 -0
  302. package/components/tryghost-identity-token-service-5.115.0.tgz +0 -0
  303. package/components/tryghost-importer-handler-content-files-5.115.0.tgz +0 -0
  304. package/components/tryghost-importer-revue-5.115.0.tgz +0 -0
  305. package/components/tryghost-in-memory-repository-5.115.0.tgz +0 -0
  306. package/components/tryghost-job-manager-5.115.0.tgz +0 -0
  307. package/components/tryghost-link-redirects-5.115.0.tgz +0 -0
  308. package/components/tryghost-link-replacer-5.115.0.tgz +0 -0
  309. package/components/tryghost-magic-link-5.115.0.tgz +0 -0
  310. package/components/tryghost-mail-events-5.115.0.tgz +0 -0
  311. package/components/tryghost-mailgun-client-5.115.0.tgz +0 -0
  312. package/components/tryghost-member-attribution-5.115.0.tgz +0 -0
  313. package/components/tryghost-member-events-5.115.0.tgz +0 -0
  314. package/components/tryghost-members-api-5.115.0.tgz +0 -0
  315. package/components/tryghost-members-csv-5.115.0.tgz +0 -0
  316. package/components/tryghost-members-importer-5.115.0.tgz +0 -0
  317. package/components/tryghost-members-payments-5.115.0.tgz +0 -0
  318. package/components/tryghost-members-ssr-5.115.0.tgz +0 -0
  319. package/components/tryghost-members-stripe-service-5.115.0.tgz +0 -0
  320. package/components/tryghost-minifier-5.115.0.tgz +0 -0
  321. package/components/tryghost-mw-api-version-mismatch-5.115.0.tgz +0 -0
  322. package/components/tryghost-mw-cache-control-5.115.0.tgz +0 -0
  323. package/components/tryghost-mw-session-from-token-5.115.0.tgz +0 -0
  324. package/components/tryghost-mw-update-user-last-seen-5.115.0.tgz +0 -0
  325. package/components/tryghost-mw-version-match-5.115.0.tgz +0 -0
  326. package/components/tryghost-mw-vhost-5.115.0.tgz +0 -0
  327. package/components/tryghost-package-json-5.115.0.tgz +0 -0
  328. package/components/tryghost-posts-service-5.115.0.tgz +0 -0
  329. package/components/tryghost-recommendations-5.115.0.tgz +0 -0
  330. package/components/tryghost-referrers-5.115.0.tgz +0 -0
  331. package/components/tryghost-security-5.115.0.tgz +0 -0
  332. package/components/tryghost-session-service-5.115.0.tgz +0 -0
  333. package/components/tryghost-settings-path-manager-5.115.0.tgz +0 -0
  334. package/components/tryghost-slack-notifications-5.115.0.tgz +0 -0
  335. package/components/tryghost-version-notifications-data-service-5.115.0.tgz +0 -0
  336. package/components/tryghost-webmentions-5.115.0.tgz +0 -0
  337. package/core/built/admin/assets/chunk.524.31419fdf6fb3859ecc1e.js +0 -35
  338. package/core/built/admin/assets/chunk.582.08c816d5e4ab766486a7.js +0 -37
  339. package/core/built/admin/assets/ghost-c2a7c4a1b76550c4219adb2ed4124ce0.css +0 -1
  340. package/core/built/admin/assets/ghost-dark-f91e4a479c6d38d94d5d1b14727871dc.css +0 -1
  341. /package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js.LICENSE.txt → chunk.713.e9027c0cc3c56110f5da.js.LICENSE.txt} +0 -0
@@ -0,0 +1,140 @@
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.Recommendation = void 0;
8
+ const bson_objectid_1 = __importDefault(require("bson-objectid"));
9
+ const errors_1 = __importDefault(require("@tryghost/errors"));
10
+ const UnsafeData_1 = require("./UnsafeData");
11
+ class Recommendation {
12
+ id;
13
+ title;
14
+ description;
15
+ excerpt; // Fetched from the site meta data
16
+ featuredImage; // Fetched from the site meta data
17
+ favicon; // Fetched from the site meta data
18
+ url;
19
+ oneClickSubscribe;
20
+ createdAt;
21
+ updatedAt;
22
+ #clickCount;
23
+ #subscriberCount;
24
+ #deleted;
25
+ get deleted() {
26
+ return this.#deleted;
27
+ }
28
+ get clickCount() {
29
+ return this.#clickCount;
30
+ }
31
+ get subscriberCount() {
32
+ return this.#subscriberCount;
33
+ }
34
+ constructor(data) {
35
+ this.id = data.id;
36
+ this.title = data.title;
37
+ this.description = data.description;
38
+ this.excerpt = data.excerpt;
39
+ this.featuredImage = data.featuredImage;
40
+ this.favicon = data.favicon;
41
+ this.url = data.url;
42
+ this.oneClickSubscribe = data.oneClickSubscribe;
43
+ this.createdAt = data.createdAt;
44
+ this.updatedAt = data.updatedAt;
45
+ this.#clickCount = data.clickCount;
46
+ this.#subscriberCount = data.subscriberCount;
47
+ this.#deleted = false;
48
+ }
49
+ static validate(properties) {
50
+ if (properties.title.length === 0) {
51
+ throw new errors_1.default.ValidationError({
52
+ message: 'Title must not be empty'
53
+ });
54
+ }
55
+ if (properties.title.length > 2000) {
56
+ throw new errors_1.default.ValidationError({
57
+ message: 'Title must be less than 2000 characters'
58
+ });
59
+ }
60
+ if (properties.description && properties.description.length > 200) {
61
+ throw new errors_1.default.ValidationError({
62
+ message: 'Description must be less than 200 characters'
63
+ });
64
+ }
65
+ }
66
+ clean() {
67
+ if (this.description !== null && this.description.length === 0) {
68
+ this.description = null;
69
+ }
70
+ if (this.excerpt !== null && this.excerpt.length === 0) {
71
+ this.excerpt = null;
72
+ }
73
+ if (this.excerpt !== null && this.excerpt.length > 2000) {
74
+ this.excerpt = this.excerpt.slice(0, 1997) + '...';
75
+ }
76
+ this.createdAt.setMilliseconds(0);
77
+ this.updatedAt?.setMilliseconds(0);
78
+ }
79
+ static create(data) {
80
+ const id = data.id ?? (0, bson_objectid_1.default)().toString();
81
+ const d = {
82
+ id,
83
+ title: data.title,
84
+ description: data.description,
85
+ excerpt: data.excerpt,
86
+ featuredImage: new UnsafeData_1.UnsafeData(data.featuredImage, { field: ['featuredImage'] }).nullable.url,
87
+ favicon: new UnsafeData_1.UnsafeData(data.favicon, { field: ['favicon'] }).nullable.url,
88
+ url: new UnsafeData_1.UnsafeData(data.url, { field: ['url'] }).url,
89
+ oneClickSubscribe: data.oneClickSubscribe,
90
+ createdAt: data.createdAt ?? new Date(),
91
+ updatedAt: data.updatedAt ?? null,
92
+ clickCount: data.clickCount,
93
+ subscriberCount: data.subscriberCount
94
+ };
95
+ this.validate(d);
96
+ const recommendation = new Recommendation(d);
97
+ recommendation.clean();
98
+ return recommendation;
99
+ }
100
+ get plain() {
101
+ return {
102
+ id: this.id,
103
+ title: this.title,
104
+ description: this.description,
105
+ excerpt: this.excerpt,
106
+ featuredImage: this.featuredImage,
107
+ favicon: this.favicon,
108
+ url: this.url,
109
+ oneClickSubscribe: this.oneClickSubscribe,
110
+ createdAt: this.createdAt,
111
+ updatedAt: this.updatedAt,
112
+ clickCount: this.clickCount,
113
+ subscriberCount: this.subscriberCount
114
+ };
115
+ }
116
+ /**
117
+ * Change the specified properties. Properties that are set to undefined will not be changed
118
+ */
119
+ edit(properties) {
120
+ // Delete undefined properties
121
+ const newProperties = this.plain;
122
+ let didChange = false;
123
+ for (const key of Object.keys(properties)) {
124
+ if (Object.prototype.hasOwnProperty.call(properties, key) && properties[key] !== undefined && properties[key] !== newProperties[key]) {
125
+ newProperties[key] = properties[key];
126
+ didChange = true;
127
+ }
128
+ }
129
+ if (!didChange) {
130
+ return;
131
+ }
132
+ newProperties.updatedAt = new Date();
133
+ const created = Recommendation.create(newProperties);
134
+ Object.assign(this, created);
135
+ }
136
+ delete() {
137
+ this.#deleted = true;
138
+ }
139
+ }
140
+ exports.Recommendation = Recommendation;
@@ -0,0 +1,201 @@
1
+ /* eslint-disable ghost/filenames/match-exported-class */
2
+
3
+ import ObjectId from 'bson-objectid';
4
+ import errors from '@tryghost/errors';
5
+ import {UnsafeData} from './UnsafeData';
6
+
7
+ /**
8
+ * We never expose Entities outside of services. Because we should never expose the bussiness logic methods. The plain objects are used for that
9
+ */
10
+ export type RecommendationPlain = {
11
+ id: string,
12
+ title: string
13
+ description: string|null
14
+ excerpt: string|null // Fetched from the site meta data
15
+ featuredImage: URL|null // Fetched from the site meta data
16
+ favicon: URL|null // Fetched from the site meta data
17
+ url: URL
18
+ oneClickSubscribe: boolean,
19
+ createdAt: Date,
20
+ updatedAt: Date|null,
21
+
22
+ /**
23
+ * These are read only, you cannot change them
24
+ */
25
+ clickCount?: number
26
+ subscriberCount?: number
27
+ }
28
+
29
+ export type RecommendationCreateData = {
30
+ id?: string
31
+ title: string
32
+ description: string|null
33
+ excerpt: string|null // Fetched from the site meta data
34
+ featuredImage: URL|string|null // Fetched from the site meta data
35
+ favicon: URL|string|null // Fetched from the site meta data
36
+ url: URL|string
37
+ oneClickSubscribe: boolean
38
+ createdAt?: Date
39
+ updatedAt?: Date|null,
40
+
41
+ /**
42
+ * These are read only, you cannot change them
43
+ */
44
+ clickCount?: number
45
+ subscriberCount?: number
46
+ }
47
+
48
+ export type AddRecommendation = Omit<RecommendationCreateData, 'id'|'createdAt'|'updatedAt'>
49
+ export type EditRecommendation = Partial<AddRecommendation>
50
+
51
+ export class Recommendation {
52
+ id: string;
53
+ title: string;
54
+ description: string|null;
55
+ excerpt: string|null; // Fetched from the site meta data
56
+ featuredImage: URL|null; // Fetched from the site meta data
57
+ favicon: URL|null; // Fetched from the site meta data
58
+ url: URL;
59
+ oneClickSubscribe: boolean;
60
+ createdAt: Date;
61
+ updatedAt: Date|null;
62
+ #clickCount: number|undefined;
63
+ #subscriberCount: number|undefined;
64
+
65
+ #deleted: boolean;
66
+
67
+ get deleted() {
68
+ return this.#deleted;
69
+ }
70
+
71
+ get clickCount() {
72
+ return this.#clickCount;
73
+ }
74
+
75
+ get subscriberCount() {
76
+ return this.#subscriberCount;
77
+ }
78
+
79
+ private constructor(data: RecommendationPlain) {
80
+ this.id = data.id;
81
+ this.title = data.title;
82
+ this.description = data.description;
83
+ this.excerpt = data.excerpt;
84
+ this.featuredImage = data.featuredImage;
85
+ this.favicon = data.favicon;
86
+ this.url = data.url;
87
+ this.oneClickSubscribe = data.oneClickSubscribe;
88
+ this.createdAt = data.createdAt;
89
+ this.updatedAt = data.updatedAt;
90
+ this.#clickCount = data.clickCount;
91
+ this.#subscriberCount = data.subscriberCount;
92
+ this.#deleted = false;
93
+ }
94
+
95
+ static validate(properties: AddRecommendation) {
96
+ if (properties.title.length === 0) {
97
+ throw new errors.ValidationError({
98
+ message: 'Title must not be empty'
99
+ });
100
+ }
101
+
102
+ if (properties.title.length > 2000) {
103
+ throw new errors.ValidationError({
104
+ message: 'Title must be less than 2000 characters'
105
+ });
106
+ }
107
+
108
+ if (properties.description && properties.description.length > 200) {
109
+ throw new errors.ValidationError({
110
+ message: 'Description must be less than 200 characters'
111
+ });
112
+ }
113
+ }
114
+
115
+ clean() {
116
+ if (this.description !== null && this.description.length === 0) {
117
+ this.description = null;
118
+ }
119
+
120
+ if (this.excerpt !== null && this.excerpt.length === 0) {
121
+ this.excerpt = null;
122
+ }
123
+
124
+ if (this.excerpt !== null && this.excerpt.length > 2000) {
125
+ this.excerpt = this.excerpt.slice(0, 1997) + '...';
126
+ }
127
+
128
+ this.createdAt.setMilliseconds(0);
129
+ this.updatedAt?.setMilliseconds(0);
130
+ }
131
+
132
+ static create(data: RecommendationCreateData) {
133
+ const id = data.id ?? ObjectId().toString();
134
+
135
+ const d = {
136
+ id,
137
+ title: data.title,
138
+ description: data.description,
139
+ excerpt: data.excerpt,
140
+ featuredImage: new UnsafeData(data.featuredImage, {field: ['featuredImage']}).nullable.url,
141
+ favicon: new UnsafeData(data.favicon, {field: ['favicon']}).nullable.url,
142
+ url: new UnsafeData(data.url, {field: ['url']}).url,
143
+ oneClickSubscribe: data.oneClickSubscribe,
144
+ createdAt: data.createdAt ?? new Date(),
145
+ updatedAt: data.updatedAt ?? null,
146
+ clickCount: data.clickCount,
147
+ subscriberCount: data.subscriberCount
148
+ };
149
+
150
+ this.validate(d);
151
+ const recommendation = new Recommendation(d);
152
+ recommendation.clean();
153
+
154
+ return recommendation;
155
+ }
156
+
157
+ get plain(): RecommendationPlain {
158
+ return {
159
+ id: this.id,
160
+ title: this.title,
161
+ description: this.description,
162
+ excerpt: this.excerpt,
163
+ featuredImage: this.featuredImage,
164
+ favicon: this.favicon,
165
+ url: this.url,
166
+ oneClickSubscribe: this.oneClickSubscribe,
167
+ createdAt: this.createdAt,
168
+ updatedAt: this.updatedAt,
169
+ clickCount: this.clickCount,
170
+ subscriberCount: this.subscriberCount
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Change the specified properties. Properties that are set to undefined will not be changed
176
+ */
177
+ edit(properties: EditRecommendation) {
178
+ // Delete undefined properties
179
+ const newProperties = this.plain;
180
+ let didChange = false;
181
+
182
+ for (const key of Object.keys(properties) as (keyof EditRecommendation)[]) {
183
+ if (Object.prototype.hasOwnProperty.call(properties, key) && properties[key] !== undefined && properties[key] !== newProperties[key]) {
184
+ (newProperties as Record<string, unknown>)[key] = properties[key] as unknown;
185
+ didChange = true;
186
+ }
187
+ }
188
+
189
+ if (!didChange) {
190
+ return;
191
+ }
192
+ newProperties.updatedAt = new Date();
193
+
194
+ const created = Recommendation.create(newProperties);
195
+ Object.assign(this, created);
196
+ }
197
+
198
+ delete() {
199
+ this.#deleted = true;
200
+ }
201
+ }
@@ -0,0 +1,208 @@
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.RecommendationController = void 0;
7
+ /* eslint-disable @typescript-eslint/no-explicit-any */
8
+ const errors_1 = __importDefault(require("@tryghost/errors"));
9
+ const UnsafeData_1 = require("./UnsafeData");
10
+ const RecommendationIncludesMap = {
11
+ 'count.clicks': 'clickCount',
12
+ 'count.subscribers': 'subscriberCount'
13
+ };
14
+ const RecommendationOrderMap = {
15
+ title: 'title',
16
+ description: 'description',
17
+ excerpt: 'excerpt',
18
+ one_click_subscribe: 'oneClickSubscribe',
19
+ created_at: 'createdAt',
20
+ updated_at: 'updatedAt',
21
+ 'count.clicks': 'clickCount',
22
+ 'count.subscribers': 'subscriberCount'
23
+ };
24
+ class RecommendationController {
25
+ service;
26
+ constructor(deps) {
27
+ this.service = deps.service;
28
+ }
29
+ async read(frame) {
30
+ const options = new UnsafeData_1.UnsafeData(frame.options);
31
+ const id = options.key('id').string;
32
+ const recommendation = await this.service.readRecommendation(id);
33
+ return this.#serialize([recommendation]);
34
+ }
35
+ async add(frame) {
36
+ const data = new UnsafeData_1.UnsafeData(frame.data);
37
+ const recommendation = data.key('recommendations').index(0);
38
+ const plain = {
39
+ title: recommendation.key('title').string,
40
+ url: recommendation.key('url').url,
41
+ // Optional fields
42
+ oneClickSubscribe: recommendation.optionalKey('one_click_subscribe')?.boolean ?? false,
43
+ description: recommendation.optionalKey('description')?.nullable.string ?? null,
44
+ excerpt: recommendation.optionalKey('excerpt')?.nullable.string ?? null,
45
+ featuredImage: recommendation.optionalKey('featured_image')?.nullable.url ?? null,
46
+ favicon: recommendation.optionalKey('favicon')?.nullable.url ?? null
47
+ };
48
+ return this.#serialize([await this.service.addRecommendation(plain)]);
49
+ }
50
+ /**
51
+ * Given a recommendation URL, returns either an existing recommendation with that url and updated metadata,
52
+ * or the metadata from that URL as if it would create a new one (without creating a new one)
53
+ *
54
+ * This can be used in the frontend when creating a new recommendation (duplication checking + showing a preview before saving)
55
+ */
56
+ async check(frame) {
57
+ const data = new UnsafeData_1.UnsafeData(frame.data);
58
+ const recommendation = data.key('recommendations').index(0);
59
+ const url = recommendation.key('url').url;
60
+ return this.#serialize([await this.service.checkRecommendation(url)]);
61
+ }
62
+ async edit(frame) {
63
+ const options = new UnsafeData_1.UnsafeData(frame.options);
64
+ const data = new UnsafeData_1.UnsafeData(frame.data);
65
+ const recommendation = data.key('recommendations').index(0);
66
+ const id = options.key('id').string;
67
+ const plain = {
68
+ title: recommendation.optionalKey('title')?.string,
69
+ url: recommendation.optionalKey('url')?.url,
70
+ oneClickSubscribe: recommendation.optionalKey('one_click_subscribe')?.boolean,
71
+ description: recommendation.optionalKey('description')?.nullable.string,
72
+ excerpt: recommendation.optionalKey('excerpt')?.nullable.string,
73
+ featuredImage: recommendation.optionalKey('featured_image')?.nullable.url,
74
+ favicon: recommendation.optionalKey('favicon')?.nullable.url
75
+ };
76
+ return this.#serialize([await this.service.editRecommendation(id, plain)]);
77
+ }
78
+ async destroy(frame) {
79
+ const options = new UnsafeData_1.UnsafeData(frame.options);
80
+ const id = options.key('id').string;
81
+ await this.service.deleteRecommendation(id);
82
+ }
83
+ #stringToOrder(str) {
84
+ if (!str) {
85
+ // Default order
86
+ return [
87
+ {
88
+ field: 'createdAt',
89
+ direction: 'desc'
90
+ }
91
+ ];
92
+ }
93
+ const parts = str.split(',');
94
+ const order = [];
95
+ for (const [index, part] of parts.entries()) {
96
+ const trimmed = part.trim();
97
+ const fieldData = new UnsafeData_1.UnsafeData(trimmed.split(' ')[0].trim(), { field: ['order', index.toString(), 'field'] });
98
+ const directionData = new UnsafeData_1.UnsafeData(trimmed.split(' ')[1]?.trim() ?? 'desc', { field: ['order', index.toString(), 'direction'] });
99
+ const validatedField = fieldData.enum(Object.keys(RecommendationOrderMap));
100
+ const direction = directionData.enum(['asc', 'desc']);
101
+ // Convert 'count.' and camelCase to snake_case
102
+ const field = RecommendationOrderMap[validatedField];
103
+ order.push({
104
+ field,
105
+ direction
106
+ });
107
+ }
108
+ return order;
109
+ }
110
+ async browse(frame) {
111
+ const options = new UnsafeData_1.UnsafeData(frame.options);
112
+ const page = options.optionalKey('page')?.integer ?? 1;
113
+ const limit = options.optionalKey('limit')?.integer ?? 5;
114
+ const include = options.optionalKey('withRelated')?.array.map((item) => {
115
+ return RecommendationIncludesMap[item.enum(Object.keys(RecommendationIncludesMap))];
116
+ }) ?? [];
117
+ const filter = options.optionalKey('filter')?.string;
118
+ const orderOption = options.optionalKey('order')?.string;
119
+ const order = this.#stringToOrder(orderOption);
120
+ const count = await this.service.countRecommendations({});
121
+ const recommendations = (await this.service.listRecommendations({ page, limit, filter, include, order }));
122
+ return this.#serialize(recommendations, {
123
+ pagination: this.#serializePagination({ page, limit, count })
124
+ });
125
+ }
126
+ async trackClicked(frame) {
127
+ const member = this.#optionalAuthMember(frame);
128
+ const options = new UnsafeData_1.UnsafeData(frame.options);
129
+ const id = options.key('id').string;
130
+ await this.service.trackClicked({
131
+ id,
132
+ memberId: member?.id
133
+ });
134
+ }
135
+ async trackSubscribed(frame) {
136
+ const member = this.#authMember(frame);
137
+ const options = new UnsafeData_1.UnsafeData(frame.options);
138
+ const id = options.key('id').string;
139
+ await this.service.trackSubscribed({
140
+ id,
141
+ memberId: member.id
142
+ });
143
+ }
144
+ #authMember(frame) {
145
+ const options = new UnsafeData_1.UnsafeData(frame.options);
146
+ const memberId = options.key('context').optionalKey('member')?.nullable.key('id').string;
147
+ if (!memberId) {
148
+ // This is an internal server error because authentication should happen outside this service.
149
+ throw new errors_1.default.UnauthorizedError({
150
+ message: 'Member not found'
151
+ });
152
+ }
153
+ return {
154
+ id: memberId
155
+ };
156
+ }
157
+ #optionalAuthMember(frame) {
158
+ try {
159
+ const member = this.#authMember(frame);
160
+ return member;
161
+ }
162
+ catch (e) {
163
+ if (e instanceof errors_1.default.UnauthorizedError) {
164
+ // This is fine, this is not required
165
+ }
166
+ else {
167
+ throw e;
168
+ }
169
+ }
170
+ return null;
171
+ }
172
+ #serialize(recommendations, meta) {
173
+ return {
174
+ data: recommendations.map((entity) => {
175
+ const d = {
176
+ id: entity.id ?? null,
177
+ title: entity.title ?? null,
178
+ description: entity.description ?? null,
179
+ excerpt: entity.excerpt ?? null,
180
+ featured_image: entity.featuredImage?.toString() ?? null,
181
+ favicon: entity.favicon?.toString() ?? null,
182
+ url: entity.url?.toString() ?? null,
183
+ one_click_subscribe: entity.oneClickSubscribe ?? null,
184
+ created_at: entity.createdAt?.toISOString() ?? null,
185
+ updated_at: entity.updatedAt?.toISOString() ?? null,
186
+ count: entity.clickCount !== undefined || entity.subscriberCount !== undefined ? {
187
+ clicks: entity.clickCount,
188
+ subscribers: entity.subscriberCount
189
+ } : undefined
190
+ };
191
+ return d;
192
+ }),
193
+ meta
194
+ };
195
+ }
196
+ #serializePagination({ page, limit, count }) {
197
+ const pages = Math.ceil(count / limit);
198
+ return {
199
+ page,
200
+ limit,
201
+ total: count,
202
+ pages,
203
+ prev: page > 1 ? page - 1 : null,
204
+ next: page < pages ? page + 1 : null
205
+ };
206
+ }
207
+ }
208
+ exports.RecommendationController = RecommendationController;