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.
- package/components/{tryghost-api-framework-5.115.1.tgz → tryghost-api-framework-5.116.1.tgz} +0 -0
- package/components/tryghost-constants-5.116.1.tgz +0 -0
- package/components/tryghost-custom-fonts-5.116.1.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.115.1.tgz → tryghost-custom-theme-settings-service-5.116.1.tgz} +0 -0
- package/components/{tryghost-domain-events-5.115.1.tgz → tryghost-domain-events-5.116.1.tgz} +0 -0
- package/components/{tryghost-donations-5.115.1.tgz → tryghost-donations-5.116.1.tgz} +0 -0
- package/components/tryghost-email-addresses-5.116.1.tgz +0 -0
- package/components/tryghost-email-service-5.116.1.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.116.1.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.116.1.tgz +0 -0
- package/components/tryghost-i18n-5.116.1.tgz +0 -0
- package/components/tryghost-job-manager-5.116.1.tgz +0 -0
- package/components/tryghost-link-replacer-5.116.1.tgz +0 -0
- package/components/tryghost-magic-link-5.116.1.tgz +0 -0
- package/components/{tryghost-member-attribution-5.115.1.tgz → tryghost-member-attribution-5.116.1.tgz} +0 -0
- package/components/tryghost-member-events-5.116.1.tgz +0 -0
- package/components/tryghost-members-api-5.116.1.tgz +0 -0
- package/components/tryghost-members-csv-5.116.1.tgz +0 -0
- package/components/{tryghost-members-offers-5.115.1.tgz → tryghost-members-offers-5.116.1.tgz} +0 -0
- package/components/{tryghost-milestones-5.115.1.tgz → tryghost-milestones-5.116.1.tgz} +0 -0
- package/components/{tryghost-mw-error-handler-5.115.1.tgz → tryghost-mw-error-handler-5.116.1.tgz} +0 -0
- package/components/tryghost-mw-vhost-5.116.1.tgz +0 -0
- package/components/{tryghost-post-events-5.115.1.tgz → tryghost-post-events-5.116.1.tgz} +0 -0
- package/components/{tryghost-post-revisions-5.115.1.tgz → tryghost-post-revisions-5.116.1.tgz} +0 -0
- package/components/tryghost-posts-service-5.116.1.tgz +0 -0
- package/components/{tryghost-prometheus-metrics-5.115.1.tgz → tryghost-prometheus-metrics-5.116.1.tgz} +0 -0
- package/components/tryghost-security-5.116.1.tgz +0 -0
- package/components/{tryghost-tiers-5.115.1.tgz → tryghost-tiers-5.116.1.tgz} +0 -0
- package/components/tryghost-webmentions-5.116.1.tgz +0 -0
- package/content/themes/casper/LICENSE +1 -1
- package/content/themes/casper/README.md +1 -1
- package/content/themes/source/LICENSE +1 -1
- package/content/themes/source/README.md +1 -1
- package/content/themes/source/assets/built/screen.css +1 -1
- package/content/themes/source/assets/built/screen.css.map +1 -1
- package/content/themes/source/assets/css/screen.css +6 -11
- package/content/themes/source/partials/feature-image.hbs +2 -2
- package/core/boot.js +0 -42
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +24764 -24129
- package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
- package/core/built/admin/assets/admin-x-demo/{index-15df2af5.mjs → index-a9601514.mjs} +3 -3
- package/core/built/admin/assets/admin-x-demo/{modals-8ca61d78.mjs → modals-c1789d04.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-d2e6872f.mjs → CodeEditorView-e9c9deb8.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-8e8821e5.mjs → index-84580c3a.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-f5cb3db3.mjs → index-f744cab7.mjs} +49 -35
- package/core/built/admin/assets/admin-x-settings/{modals-e8ae4d46.mjs → modals-d9ca60c5.mjs} +1198 -1192
- package/core/built/admin/assets/chunk.524.cb72a86e19c9ffd6172e.js +35 -0
- package/core/built/admin/assets/chunk.582.4f4d38ffe79fbdbd26f7.js +37 -0
- package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js → chunk.713.e9027c0cc3c56110f5da.js} +125 -98
- package/core/built/admin/assets/{ghost-df7b9558260aa27d18b195ee895b487d.js → ghost-03b64c086f3c60cabc85fe7a7e2b640a.js} +144 -145
- package/core/built/admin/assets/ghost-ba58e9822f7384461e926c7e23f04a75.css +1 -0
- package/core/built/admin/assets/ghost-dark-f1f29683b14ffa11615b3bba8b6ab92c.css +1 -0
- package/core/built/admin/assets/koenig-lexical/index.css +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +20563 -20891
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +139 -139
- package/core/built/admin/assets/posts/posts.js +5732 -5667
- package/core/built/admin/assets/stats/stats.js +71082 -7533
- package/core/built/admin/assets/{vendor-68a4aa424a179a90f5bbc2b750def576.js → vendor-72026232b36d97babc6320917c16c321.js} +36 -34
- package/core/built/admin/index.html +6 -6
- package/core/cli/generate-data.js +1 -1
- package/core/frontend/helpers/ghost_head.js +6 -1
- package/core/frontend/public/ghost-stats.js +55 -2
- package/core/frontend/services/assets-minification/AdminAuthAssets.js +2 -1
- package/core/frontend/services/assets-minification/CardAssets.js +1 -1
- package/core/frontend/services/assets-minification/CommentCountsAssets.js +1 -1
- package/core/frontend/services/assets-minification/MemberAttributionAssets.js +1 -1
- package/core/frontend/services/assets-minification/Minifier.js +191 -0
- package/core/frontend/services/routing/controllers/previews.js +2 -1
- package/core/server/adapters/cache/Redis.js +1 -1
- package/core/server/adapters/lib/redis/AdapterCacheRedis.js +287 -0
- package/core/server/adapters/lib/redis/redis-store-factory.js +22 -0
- package/core/server/api/endpoints/posts.js +9 -3
- package/core/server/api/endpoints/previews.js +35 -1
- package/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js +6 -9
- package/core/server/api/endpoints/utils/validators/input/settings.js +1 -1
- package/core/server/data/db/connection.js +2 -0
- package/core/server/data/db/index.js +1 -0
- package/core/server/data/importer/handlers/ImporterContentFileHandler.js +90 -0
- package/core/server/data/importer/import-manager.js +1 -1
- package/core/server/data/seeders/DataGenerator.js +288 -0
- package/core/server/data/seeders/importers/BenefitsImporter.js +28 -0
- package/core/server/data/seeders/importers/CommentsImporter.js +73 -0
- package/core/server/data/seeders/importers/EmailBatchesImporter.js +38 -0
- package/core/server/data/seeders/importers/EmailRecipientFailuresImporter.js +67 -0
- package/core/server/data/seeders/importers/EmailRecipientsImporter.js +212 -0
- package/core/server/data/seeders/importers/EmailsImporter.js +99 -0
- package/core/server/data/seeders/importers/LabelsImporter.js +41 -0
- package/core/server/data/seeders/importers/MembersClickEventsImporter.js +69 -0
- package/core/server/data/seeders/importers/MembersCreatedEventsImporter.js +103 -0
- package/core/server/data/seeders/importers/MembersFeedbackImporter.js +45 -0
- package/core/server/data/seeders/importers/MembersImporter.js +111 -0
- package/core/server/data/seeders/importers/MembersLabelsImporter.js +39 -0
- package/core/server/data/seeders/importers/MembersLoginEventsImporter.js +69 -0
- package/core/server/data/seeders/importers/MembersNewslettersImporter.js +38 -0
- package/core/server/data/seeders/importers/MembersPaidSubscriptionEventsImporter.js +99 -0
- package/core/server/data/seeders/importers/MembersProductsImporter.js +42 -0
- package/core/server/data/seeders/importers/MembersStatusEventsImporter.js +58 -0
- package/core/server/data/seeders/importers/MembersStripeCustomersImporter.js +60 -0
- package/core/server/data/seeders/importers/MembersStripeCustomersSubscriptionsImporter.js +259 -0
- package/core/server/data/seeders/importers/MembersSubscribeEventsImporter.js +69 -0
- package/core/server/data/seeders/importers/MembersSubscriptionCreatedEventsImporter.js +95 -0
- package/core/server/data/seeders/importers/NewslettersImporter.js +40 -0
- package/core/server/data/seeders/importers/OffersImporter.js +70 -0
- package/core/server/data/seeders/importers/PostsAuthorsImporter.js +32 -0
- package/core/server/data/seeders/importers/PostsImporter.js +102 -0
- package/core/server/data/seeders/importers/PostsProductsImporter.js +35 -0
- package/core/server/data/seeders/importers/PostsTagsImporter.js +46 -0
- package/core/server/data/seeders/importers/ProductsBenefitsImporter.js +54 -0
- package/core/server/data/seeders/importers/ProductsImporter.js +90 -0
- package/core/server/data/seeders/importers/RecommendationClickEventsImporter.js +32 -0
- package/core/server/data/seeders/importers/RecommendationSubscribeEventsImporter.js +32 -0
- package/core/server/data/seeders/importers/RecommendationsImporter.js +34 -0
- package/core/server/data/seeders/importers/RedirectsImporter.js +49 -0
- package/core/server/data/seeders/importers/RolesUsersImporter.js +42 -0
- package/core/server/data/seeders/importers/StripePricesImporter.js +69 -0
- package/core/server/data/seeders/importers/StripeProductsImporter.js +34 -0
- package/core/server/data/seeders/importers/TableImporter.js +187 -0
- package/core/server/data/seeders/importers/TagsImporter.js +41 -0
- package/core/server/data/seeders/importers/UsersImporter.js +31 -0
- package/core/server/data/seeders/importers/WebMentionsImporter.js +42 -0
- package/core/server/data/seeders/importers/index.js +41 -0
- package/core/server/data/seeders/utils/JsonImporter.js +39 -0
- package/core/server/data/seeders/utils/blog-info.js +3 -0
- package/core/server/data/seeders/utils/database-date.js +7 -0
- package/core/server/data/seeders/utils/event-generator.js +48 -0
- package/core/server/data/seeders/utils/random.js +13 -0
- package/core/server/data/seeders/utils/topological-sort.js +33 -0
- package/core/server/services/adapter-manager/AdapterManager.js +161 -0
- package/core/server/services/adapter-manager/index.js +1 -1
- package/core/server/services/announcement-bar-service/AnnouncementBarSettings.js +54 -0
- package/core/server/services/announcement-bar-service/AnnouncementVisibilityValues.js +11 -0
- package/core/server/services/announcement-bar-service/index.js +1 -1
- package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +1 -1
- package/core/server/services/auth/session/session-service.js +15 -5
- package/core/server/services/custom-redirects/index.js +1 -1
- package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +1 -1
- package/core/server/services/email-service/EmailServiceWrapper.js +4 -4
- package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +1 -1
- package/core/server/services/email-suppression-list/service.js +1 -1
- package/core/server/services/lib/DynamicRedirectManager.js +156 -0
- package/core/server/services/lib/EmailContentGenerator.js +54 -0
- package/core/server/services/lib/InMemoryRepository.js +62 -0
- package/core/server/services/lib/InMemoryRepository.ts +80 -0
- package/core/server/services/lib/MailgunClient.js +364 -0
- package/core/server/services/link-redirection/LinkRedirect.js +26 -0
- package/core/server/services/link-redirection/LinkRedirectRepository.js +7 -7
- package/core/server/services/link-redirection/LinkRedirectsService.js +123 -0
- package/core/server/services/link-redirection/README.md +151 -0
- package/core/server/services/link-redirection/RedirectEvent.js +24 -0
- package/core/server/services/link-redirection/index.js +1 -1
- package/core/server/services/link-tracking/LinkClickTrackingService.js +1 -1
- package/core/server/services/mail/index.js +1 -1
- package/core/server/services/mail-events/InMemoryMailEventRepository.js +2 -2
- package/core/server/services/mail-events/InMemoryMailEventRepository.ts +1 -1
- package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
- package/core/server/services/offers/service.js +1 -1
- package/core/server/services/recommendations/RecommendationServiceWrapper.js +8 -8
- package/core/server/services/recommendations/service/BookshelfClickEventRepository.js +48 -0
- package/core/server/services/recommendations/service/BookshelfClickEventRepository.ts +49 -0
- package/core/server/services/recommendations/service/BookshelfRecommendationRepository.js +98 -0
- package/core/server/services/recommendations/service/BookshelfRecommendationRepository.ts +117 -0
- package/core/server/services/recommendations/service/BookshelfRepository.js +134 -0
- package/core/server/services/recommendations/service/BookshelfRepository.ts +196 -0
- package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.js +48 -0
- package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.ts +49 -0
- package/core/server/services/recommendations/service/ClickEvent.js +33 -0
- package/core/server/services/recommendations/service/ClickEvent.ts +32 -0
- package/core/server/services/recommendations/service/InMemoryRecommendationRepository.js +19 -0
- package/core/server/services/recommendations/service/InMemoryRecommendationRepository.ts +20 -0
- package/core/server/services/recommendations/service/IncomingRecommendationController.js +34 -0
- package/core/server/services/recommendations/service/IncomingRecommendationController.ts +51 -0
- package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.js +25 -0
- package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.ts +37 -0
- package/core/server/services/recommendations/service/IncomingRecommendationService.js +93 -0
- package/core/server/services/recommendations/service/IncomingRecommendationService.ts +160 -0
- package/core/server/services/recommendations/service/Recommendation.js +140 -0
- package/core/server/services/recommendations/service/Recommendation.ts +201 -0
- package/core/server/services/recommendations/service/RecommendationController.js +208 -0
- package/core/server/services/recommendations/service/RecommendationController.ts +258 -0
- package/core/server/services/recommendations/service/RecommendationMetadataService.js +86 -0
- package/core/server/services/recommendations/service/RecommendationMetadataService.ts +128 -0
- package/core/server/services/recommendations/service/RecommendationRepository.js +2 -0
- package/core/server/services/recommendations/service/RecommendationRepository.ts +13 -0
- package/core/server/services/recommendations/service/RecommendationService.js +228 -0
- package/core/server/services/recommendations/service/RecommendationService.ts +281 -0
- package/core/server/services/recommendations/service/SubscribeEvent.js +33 -0
- package/core/server/services/recommendations/service/SubscribeEvent.ts +32 -0
- package/core/server/services/recommendations/service/UnsafeData.js +183 -0
- package/core/server/services/recommendations/service/UnsafeData.ts +217 -0
- package/core/server/services/recommendations/service/WellknownService.js +36 -0
- package/core/server/services/recommendations/service/WellknownService.ts +47 -0
- package/core/server/services/recommendations/service/index.js +31 -0
- package/core/server/services/recommendations/service/index.ts +15 -0
- package/core/server/services/recommendations/service/libraries.d.ts +5 -0
- package/core/server/services/slack-notifications/SlackNotifications.js +211 -0
- package/core/server/services/slack-notifications/SlackNotificationsService.js +90 -0
- package/core/server/services/slack-notifications/service.js +4 -6
- package/core/server/web/api/endpoints/admin/app.js +1 -21
- package/core/server/web/api/middleware/version-match.js +41 -0
- package/core/shared/labs.js +2 -2
- package/package.json +87 -104
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +1470 -1540
- package/components/tryghost-adapter-cache-redis-5.115.1.tgz +0 -0
- package/components/tryghost-adapter-manager-5.115.1.tgz +0 -0
- package/components/tryghost-announcement-bar-settings-5.115.1.tgz +0 -0
- package/components/tryghost-constants-5.115.1.tgz +0 -0
- package/components/tryghost-custom-fonts-5.115.1.tgz +0 -0
- package/components/tryghost-data-generator-5.115.1.tgz +0 -0
- package/components/tryghost-email-addresses-5.115.1.tgz +0 -0
- package/components/tryghost-email-content-generator-5.115.1.tgz +0 -0
- package/components/tryghost-email-events-5.115.1.tgz +0 -0
- package/components/tryghost-email-service-5.115.1.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.115.1.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-5.115.1.tgz +0 -0
- package/components/tryghost-ghost-5.115.1.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.115.1.tgz +0 -0
- package/components/tryghost-i18n-5.115.1.tgz +0 -0
- package/components/tryghost-importer-handler-content-files-5.115.1.tgz +0 -0
- package/components/tryghost-in-memory-repository-5.115.1.tgz +0 -0
- package/components/tryghost-job-manager-5.115.1.tgz +0 -0
- package/components/tryghost-link-redirects-5.115.1.tgz +0 -0
- package/components/tryghost-link-replacer-5.115.1.tgz +0 -0
- package/components/tryghost-magic-link-5.115.1.tgz +0 -0
- package/components/tryghost-mailgun-client-5.115.1.tgz +0 -0
- package/components/tryghost-member-events-5.115.1.tgz +0 -0
- package/components/tryghost-members-api-5.115.1.tgz +0 -0
- package/components/tryghost-members-csv-5.115.1.tgz +0 -0
- package/components/tryghost-members-payments-5.115.1.tgz +0 -0
- package/components/tryghost-minifier-5.115.1.tgz +0 -0
- package/components/tryghost-mw-version-match-5.115.1.tgz +0 -0
- package/components/tryghost-mw-vhost-5.115.1.tgz +0 -0
- package/components/tryghost-posts-service-5.115.1.tgz +0 -0
- package/components/tryghost-recommendations-5.115.1.tgz +0 -0
- package/components/tryghost-security-5.115.1.tgz +0 -0
- package/components/tryghost-slack-notifications-5.115.1.tgz +0 -0
- package/components/tryghost-webmentions-5.115.1.tgz +0 -0
- package/core/built/admin/assets/chunk.524.2439684964c164c598ab.js +0 -35
- package/core/built/admin/assets/chunk.582.bf5a2bbb2c4eb69ef1e7.js +0 -37
- package/core/built/admin/assets/ghost-327b17ea23cb8c89bd7e6a51e18e8506.css +0 -1
- package/core/built/admin/assets/ghost-dark-f30a597ac19632a118939492591c531b.css +0 -1
- /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;
|