ghost 5.115.1 → 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.
- package/components/{tryghost-api-framework-5.115.1.tgz → tryghost-api-framework-5.116.0.tgz} +0 -0
- package/components/tryghost-constants-5.116.0.tgz +0 -0
- package/components/tryghost-custom-fonts-5.116.0.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.115.1.tgz → tryghost-custom-theme-settings-service-5.116.0.tgz} +0 -0
- package/components/tryghost-domain-events-5.116.0.tgz +0 -0
- package/components/tryghost-donations-5.116.0.tgz +0 -0
- package/components/tryghost-email-addresses-5.116.0.tgz +0 -0
- package/components/tryghost-email-service-5.116.0.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.116.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.116.0.tgz +0 -0
- package/components/tryghost-i18n-5.116.0.tgz +0 -0
- package/components/tryghost-job-manager-5.116.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.116.0.tgz +0 -0
- package/components/tryghost-magic-link-5.116.0.tgz +0 -0
- package/components/{tryghost-member-attribution-5.115.1.tgz → tryghost-member-attribution-5.116.0.tgz} +0 -0
- package/components/tryghost-member-events-5.116.0.tgz +0 -0
- package/components/tryghost-members-api-5.116.0.tgz +0 -0
- package/components/tryghost-members-csv-5.116.0.tgz +0 -0
- package/components/{tryghost-members-offers-5.115.1.tgz → tryghost-members-offers-5.116.0.tgz} +0 -0
- package/components/{tryghost-milestones-5.115.1.tgz → tryghost-milestones-5.116.0.tgz} +0 -0
- package/components/{tryghost-mw-error-handler-5.115.1.tgz → tryghost-mw-error-handler-5.116.0.tgz} +0 -0
- package/components/tryghost-mw-vhost-5.116.0.tgz +0 -0
- package/components/tryghost-post-events-5.116.0.tgz +0 -0
- package/components/{tryghost-post-revisions-5.115.1.tgz → tryghost-post-revisions-5.116.0.tgz} +0 -0
- package/components/tryghost-posts-service-5.116.0.tgz +0 -0
- package/components/{tryghost-prometheus-metrics-5.115.1.tgz → tryghost-prometheus-metrics-5.116.0.tgz} +0 -0
- package/components/tryghost-security-5.116.0.tgz +0 -0
- package/components/{tryghost-tiers-5.115.1.tgz → tryghost-tiers-5.116.0.tgz} +0 -0
- package/components/tryghost-webmentions-5.116.0.tgz +0 -0
- 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.8371443ef8f60db429d0.js +35 -0
- package/core/built/admin/assets/chunk.582.f90151775f2e53dd21d9.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-domain-events-5.115.1.tgz +0 -0
- package/components/tryghost-donations-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-post-events-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,123 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const DomainEvents = require('@tryghost/domain-events');
|
|
3
|
+
const RedirectEvent = require('./RedirectEvent');
|
|
4
|
+
const LinkRedirect = require('./LinkRedirect');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} ILinkRedirectRepository
|
|
8
|
+
* @prop {(url: URL) => Promise<LinkRedirect|undefined>} getByURL
|
|
9
|
+
* @prop {({filter: string}) => Promise<LinkRedirect[]>} getAll
|
|
10
|
+
* @prop {({filter: string}) => Promise<String[]>} getFilteredIds
|
|
11
|
+
* @prop {(linkRedirect: LinkRedirect) => Promise<void>} save
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
class LinkRedirectsService {
|
|
15
|
+
/** @type ILinkRedirectRepository */
|
|
16
|
+
#linkRedirectRepository;
|
|
17
|
+
/** @type URL */
|
|
18
|
+
#baseURL;
|
|
19
|
+
|
|
20
|
+
/** @type String */
|
|
21
|
+
#redirectURLPrefix = 'r/';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {object} deps
|
|
25
|
+
* @param {ILinkRedirectRepository} deps.linkRedirectRepository
|
|
26
|
+
* @param {object} deps.config
|
|
27
|
+
* @param {URL} deps.config.baseURL
|
|
28
|
+
*/
|
|
29
|
+
constructor(deps) {
|
|
30
|
+
this.#linkRedirectRepository = deps.linkRedirectRepository;
|
|
31
|
+
if (!deps.config.baseURL.pathname.endsWith('/')) {
|
|
32
|
+
this.#baseURL = new URL(deps.config.baseURL);
|
|
33
|
+
this.#baseURL.pathname += '/';
|
|
34
|
+
} else {
|
|
35
|
+
this.#baseURL = deps.config.baseURL;
|
|
36
|
+
}
|
|
37
|
+
this.handleRequest = this.handleRequest.bind(this);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get a unique URL with slug for creating unique redirects
|
|
42
|
+
*
|
|
43
|
+
* @returns {Promise<URL>}
|
|
44
|
+
*/
|
|
45
|
+
async getSlugUrl() {
|
|
46
|
+
let url;
|
|
47
|
+
while (!url || await this.#linkRedirectRepository.getByURL(url)) {
|
|
48
|
+
const slug = crypto.randomBytes(4).toString('hex');
|
|
49
|
+
url = new URL(`${this.#redirectURLPrefix}${slug}`, this.#baseURL);
|
|
50
|
+
}
|
|
51
|
+
return url;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {Object} options
|
|
56
|
+
*
|
|
57
|
+
* @returns {Promise<String[]>}
|
|
58
|
+
*/
|
|
59
|
+
async getFilteredIds(options) {
|
|
60
|
+
return await this.#linkRedirectRepository.getFilteredIds(options);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {URL} from
|
|
65
|
+
* @param {URL} to
|
|
66
|
+
*
|
|
67
|
+
* @returns {Promise<LinkRedirect>}
|
|
68
|
+
*/
|
|
69
|
+
async addRedirect(from, to) {
|
|
70
|
+
const link = new LinkRedirect({
|
|
71
|
+
from,
|
|
72
|
+
to
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await this.#linkRedirectRepository.save(link);
|
|
76
|
+
|
|
77
|
+
return link;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {import('express').Request} req
|
|
82
|
+
* @param {import('express').Response} res
|
|
83
|
+
* @param {import('express').NextFunction} next
|
|
84
|
+
*
|
|
85
|
+
* @returns {Promise<void>}
|
|
86
|
+
*/
|
|
87
|
+
async handleRequest(req, res, next) {
|
|
88
|
+
try {
|
|
89
|
+
// skip handling if original url doesn't match the prefix
|
|
90
|
+
const fullURLWithRedirectPrefix = `${this.#baseURL.pathname}${this.#redirectURLPrefix}`;
|
|
91
|
+
// @NOTE: below is equivalent to doing:
|
|
92
|
+
// router.get('/r/'), (req, res) ...
|
|
93
|
+
// To make it cleaner we should rework it to:
|
|
94
|
+
// linkRedirects.service.handleRequest(router);
|
|
95
|
+
// and mount routes on top like for example sitemapHandler does
|
|
96
|
+
// Cleanup issue: https://github.com/TryGhost/Toolbox/issues/516
|
|
97
|
+
if (!req.originalUrl.startsWith(fullURLWithRedirectPrefix)) {
|
|
98
|
+
return next();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const url = new URL(req.originalUrl, this.#baseURL);
|
|
102
|
+
const link = await this.#linkRedirectRepository.getByURL(url);
|
|
103
|
+
|
|
104
|
+
if (!link) {
|
|
105
|
+
return next();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const event = RedirectEvent.create({
|
|
109
|
+
url,
|
|
110
|
+
link
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
DomainEvents.dispatch(event);
|
|
114
|
+
|
|
115
|
+
res.setHeader('X-Robots-Tag', 'noindex, nofollow');
|
|
116
|
+
return res.redirect(link.to.href);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return next(e);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = LinkRedirectsService;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Link Redirects
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## Usage
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## Develop
|
|
8
|
+
|
|
9
|
+
This is a monorepo package.
|
|
10
|
+
|
|
11
|
+
Follow the instructions for the top-level repo.
|
|
12
|
+
1. `git clone` this repo & `cd` into it as usual
|
|
13
|
+
2. Run `yarn` to install top-level dependencies.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Test
|
|
18
|
+
|
|
19
|
+
- `yarn lint` run just eslint
|
|
20
|
+
- `yarn test` run lint and tests
|
|
21
|
+
|
|
22
|
+
## Overview of how Ghost handles LinkRedirects
|
|
23
|
+
### Summary
|
|
24
|
+
When a publisher sends an email newsletter with email click analytics enabled, Ghost will replace all the links in the email's content with a link of the form `https://{site_url}/r/{redirect hash}?m={member UUID}`. When a member clicks on a link in their email, Ghost receives the request, redirects the user to the original link's URL, then updates some analytics data in the database.
|
|
25
|
+
|
|
26
|
+
### The details
|
|
27
|
+
The following deep-dive covers the link redirect flow from when the member clicks on a link in an email.
|
|
28
|
+
|
|
29
|
+
First, we lookup the redirect by the `/r/{hash}` value in the URL:
|
|
30
|
+
```
|
|
31
|
+
select `redirects`.* from `redirects` where `redirects`.`from` = ? limit ? undefined
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If the redirect exists, the `LinkRedirectsService` emits a `RedirectEvent`, and then responds to the HTTP request with a 302.
|
|
35
|
+
|
|
36
|
+
The `LinkClickTrackingService` subscribes to the `RedirectEvent` and kicks off the analytics inserts/updates. First we grab the `uuid` from the `?m={uuid}` parameter and lookup the member by `uuid`:
|
|
37
|
+
```
|
|
38
|
+
select `members`.* from `members` where `members`.`uuid` = ? limit ? undefined
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then we insert a row into the `members_click_events` table to record the click:
|
|
42
|
+
```
|
|
43
|
+
insert into `members_click_events` (`created_at`, `id`, `member_id`, `redirect_id`) values (?, ?, ?, ?) undefined
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then we query for the row we just inserted:
|
|
47
|
+
```
|
|
48
|
+
select `members_click_events`.* from `members_click_events` where `members_click_events`.`id` = ? limit ? undefined
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
At this point, we emit a `MemberLinkClickEvent` with the member ID and `lastSeenAt` timestamp.
|
|
52
|
+
|
|
53
|
+
The `LastSeenAtUpdater` subscribes to the `MemberLinkClickEvent`. First, it checks if the `lastSeenAt` value has already been updated in the current day.
|
|
54
|
+
|
|
55
|
+
If it has, we stop here.
|
|
56
|
+
|
|
57
|
+
If it hasn't, we continue to update the member. First, we select the member by ID:
|
|
58
|
+
```
|
|
59
|
+
select `members`.* from `members` where `members`.`id` = ? limit ? undefined
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Then we start a transaction and get a lock on the member for updating:
|
|
63
|
+
```
|
|
64
|
+
BEGIN; trx34
|
|
65
|
+
select `members`.* from `members` where `members`.`id` = ? limit ? for update trx34
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Since we're editing the member, we will eventually need to emit a `member.edited` webhook with the standard includes (labels and newsletters) so we also query them here:
|
|
69
|
+
```
|
|
70
|
+
select `labels`.*, `members_labels`.`member_id` as `_pivot_member_id`, `members_labels`.`label_id` as `_pivot_label_id`, `members_labels`.`sort_order` as `_pivot_sort_order` from `labels` inner join `members_labels` on `members_labels`.`label_id` = `labels`.`id` where `members_labels`.`member_id` in (?) order by `sort_order` ASC for update trx34
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then we query the member's newsletters:
|
|
74
|
+
```
|
|
75
|
+
select `newsletters`.*, `members_newsletters`.`member_id` as `_pivot_member_id`, `members_newsletters`.`newsletter_id` as `_pivot_newsletter_id` from `newsletters` inner join `members_newsletters` on `members_newsletters`.`newsletter_id` = `newsletters`.`id` where `members_newsletters`.`member_id` in (?) order by `newsletters`.`sort_order` ASC for update trx34
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then we update the member:
|
|
79
|
+
```
|
|
80
|
+
update `members` set `uuid` = ?, `transient_id` = ?, `email` = ?, `status` = ?, `name` = ?, `expertise` = ?, `note` = ?, `geolocation` = ?, `enable_comment_notifications` = ?, `email_count` = ?, `email_opened_count` = ?, `email_open_rate` = ?, `email_disabled` = ?, `last_seen_at` = ?, `last_commented_at` = ?, `created_at` = ?, `created_by` = ?, `updated_at` = ?, `updated_by` = ? where `id` = ? trx34
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then we select the member by ID again to get the freshly updated values from the DB:
|
|
84
|
+
```
|
|
85
|
+
select `members`.* from `members` where `members`.`id` = ? limit ? trx34
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Then we commit the transaction:
|
|
89
|
+
```
|
|
90
|
+
COMMIT; trx34
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Finally, we query for any member.edited webhooks and fire the `member.edited` event:
|
|
94
|
+
```
|
|
95
|
+
select `webhooks`.* from `webhooks` where `event` = ? trx34
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
### Sequence Diagram
|
|
100
|
+
```mermaid
|
|
101
|
+
sequenceDiagram
|
|
102
|
+
actor Member
|
|
103
|
+
participant Ghost
|
|
104
|
+
participant Ghost Async
|
|
105
|
+
participant DB
|
|
106
|
+
rect rgba(0,100,0, 0.1)
|
|
107
|
+
Member ->>Ghost: Clicks link in email
|
|
108
|
+
Ghost ->> DB: Query: lookup redirect
|
|
109
|
+
DB ->> Ghost: Redirect record
|
|
110
|
+
Note right of DB: Serve the redirect
|
|
111
|
+
Ghost -->> Ghost Async: Emit RedirectEvent
|
|
112
|
+
Ghost ->> Member: 302 Redirect
|
|
113
|
+
end
|
|
114
|
+
rect rgba(100,0,0,0.1)
|
|
115
|
+
Ghost Async ->> DB: Lookup Member by `uuid` from URL param
|
|
116
|
+
DB ->> Ghost Async: `member` record
|
|
117
|
+
Ghost Async ->> DB: Insert `member_click_event`
|
|
118
|
+
Note right of DB: Insert click event
|
|
119
|
+
DB ->> Ghost Async: 👌
|
|
120
|
+
Ghost Async ->> DB: Select `member_click_event`
|
|
121
|
+
DB ->> Ghost Async: `member_click_event` record
|
|
122
|
+
end
|
|
123
|
+
rect rgba(0,0,100, 0.1)
|
|
124
|
+
Ghost Async ->> DB: Select `member` by id
|
|
125
|
+
DB ->> Ghost Async: `member` record
|
|
126
|
+
Ghost Async ->> DB: Begin transaction
|
|
127
|
+
activate DB
|
|
128
|
+
DB ->> Ghost Async: 👌
|
|
129
|
+
Ghost Async ->> DB: Select `member` for update
|
|
130
|
+
DB ->> Ghost Async: `member` record
|
|
131
|
+
Ghost Async ->> DB: Select member labels for update
|
|
132
|
+
Note right of DB: Update member `lastSeenAt`
|
|
133
|
+
DB ->> Ghost Async: Member's labels
|
|
134
|
+
Ghost Async ->> DB: Select member newsletters for update
|
|
135
|
+
DB ->> Ghost Async: Member's newsletters
|
|
136
|
+
Ghost Async ->> DB: Update member's `lastSeenAt` timestamp
|
|
137
|
+
DB ->> Ghost Async: 👌
|
|
138
|
+
Ghost Async ->> DB: Select `member` by ID
|
|
139
|
+
DB ->> Ghost Async: `member` record
|
|
140
|
+
Ghost Async ->> DB: Commit transaction
|
|
141
|
+
DB ->> Ghost Async: 👌
|
|
142
|
+
deactivate DB
|
|
143
|
+
end
|
|
144
|
+
rect rgba(100,100,0,0.1)
|
|
145
|
+
Ghost Async ->> DB: Select `webhooks`
|
|
146
|
+
Note right of DB: Send `member.edited` webhook
|
|
147
|
+
DB ->> Ghost Async: `webhook` records
|
|
148
|
+
create participant Webhook Recipient
|
|
149
|
+
Ghost Async ->> Webhook Recipient: `member.edited` webhook
|
|
150
|
+
end
|
|
151
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} RedirectEventData
|
|
3
|
+
* @prop {URL} url
|
|
4
|
+
* @prop {import('./LinkRedirect')} link
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = class RedirectEvent {
|
|
8
|
+
/**
|
|
9
|
+
* @param {RedirectEventData} data
|
|
10
|
+
* @param {Date} timestamp
|
|
11
|
+
*/
|
|
12
|
+
constructor(data, timestamp) {
|
|
13
|
+
this.data = data;
|
|
14
|
+
this.timestamp = timestamp;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {RedirectEventData} data
|
|
19
|
+
* @param {Date} [timestamp]
|
|
20
|
+
*/
|
|
21
|
+
static create(data, timestamp) {
|
|
22
|
+
return new RedirectEvent(data, timestamp ?? new Date);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -14,7 +14,7 @@ class LinkRedirectsServiceWrapper {
|
|
|
14
14
|
// Wire up all the dependencies
|
|
15
15
|
const models = require('../../models');
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const LinkRedirectsService = require('./LinkRedirectsService');
|
|
18
18
|
|
|
19
19
|
this.linkRedirectRepository = new LinkRedirectRepository({
|
|
20
20
|
LinkRedirect: models.Redirect,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const urlUtils = require('../../../shared/url-utils');
|
|
3
3
|
const settingsCache = require('../../../shared/settings-cache');
|
|
4
|
-
const EmailContentGenerator = require('
|
|
4
|
+
const EmailContentGenerator = require('../lib/EmailContentGenerator');
|
|
5
5
|
|
|
6
6
|
const emailContentGenerator = new EmailContentGenerator({
|
|
7
7
|
getSiteUrl: () => urlUtils.urlFor('home', true),
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.InMemoryMailEventRepository = void 0;
|
|
4
|
-
const
|
|
5
|
-
class InMemoryMailEventRepository extends
|
|
4
|
+
const InMemoryRepository_1 = require("../lib/InMemoryRepository");
|
|
5
|
+
class InMemoryMailEventRepository extends InMemoryRepository_1.InMemoryRepository {
|
|
6
6
|
toPrimitive() {
|
|
7
7
|
return {};
|
|
8
8
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const {MemberPageViewEvent, MemberCommentEvent, MemberLinkClickEvent} = require('@tryghost/member-events');
|
|
2
2
|
const moment = require('moment-timezone');
|
|
3
3
|
const {IncorrectUsageError} = require('@tryghost/errors');
|
|
4
|
-
const {EmailOpenedEvent} = require('@tryghost/email-
|
|
4
|
+
const {EmailOpenedEvent} = require('@tryghost/email-service');
|
|
5
5
|
const logging = require('@tryghost/logging');
|
|
6
6
|
const LastSeenAtCache = require('./LastSeenAtCache');
|
|
7
7
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const DynamicRedirectManager = require('@tryghost/express-dynamic-redirects');
|
|
2
1
|
const OffersModule = require('@tryghost/members-offers');
|
|
3
2
|
|
|
3
|
+
const DynamicRedirectManager = require('../lib/DynamicRedirectManager');
|
|
4
4
|
const config = require('../../../shared/config');
|
|
5
5
|
const urlUtils = require('../../../shared/url-utils');
|
|
6
6
|
const models = require('../../models');
|
|
@@ -4,37 +4,37 @@ const logging = require('@tryghost/logging');
|
|
|
4
4
|
|
|
5
5
|
class RecommendationServiceWrapper {
|
|
6
6
|
/**
|
|
7
|
-
* @type {import('
|
|
7
|
+
* @type {import('./service').RecommendationRepository}
|
|
8
8
|
*/
|
|
9
9
|
repository;
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* @type {import('
|
|
12
|
+
* @type {import('./service').BookshelfClickEventRepository}
|
|
13
13
|
*/
|
|
14
14
|
clickEventRepository;
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* @type {import('
|
|
17
|
+
* @type {import('./service').BookshelfSubscribeEventRepository}
|
|
18
18
|
*/
|
|
19
19
|
subscribeEventRepository;
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* @type {import('
|
|
22
|
+
* @type {import('./service').RecommendationController}
|
|
23
23
|
*/
|
|
24
24
|
controller;
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* @type {import('
|
|
27
|
+
* @type {import('./service').RecommendationService}
|
|
28
28
|
*/
|
|
29
29
|
service;
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* @type {import('
|
|
32
|
+
* @type {import('./service').IncomingRecommendationController}
|
|
33
33
|
*/
|
|
34
34
|
incomingRecommendationController;
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* @type {import('
|
|
37
|
+
* @type {import('./service').IncomingRecommendationService}
|
|
38
38
|
*/
|
|
39
39
|
incomingRecommendationService;
|
|
40
40
|
|
|
@@ -65,7 +65,7 @@ class RecommendationServiceWrapper {
|
|
|
65
65
|
IncomingRecommendationService,
|
|
66
66
|
IncomingRecommendationEmailRenderer,
|
|
67
67
|
RecommendationMetadataService
|
|
68
|
-
} = require('
|
|
68
|
+
} = require('./service');
|
|
69
69
|
|
|
70
70
|
const mentions = require('../mentions');
|
|
71
71
|
|
|
@@ -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.BookshelfClickEventRepository = void 0;
|
|
7
|
+
const BookshelfRepository_1 = require("./BookshelfRepository");
|
|
8
|
+
const logging_1 = __importDefault(require("@tryghost/logging"));
|
|
9
|
+
const ClickEvent_1 = require("./ClickEvent");
|
|
10
|
+
class BookshelfClickEventRepository 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 ClickEvent_1.ClickEvent.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.BookshelfClickEventRepository = BookshelfClickEventRepository;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {BookshelfRepository, ModelClass, ModelInstance} from './BookshelfRepository';
|
|
2
|
+
import logger from '@tryghost/logging';
|
|
3
|
+
import {ClickEvent} from './ClickEvent';
|
|
4
|
+
|
|
5
|
+
type Sentry = {
|
|
6
|
+
captureException(err: unknown): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class BookshelfClickEventRepository extends BookshelfRepository<string, ClickEvent> {
|
|
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: ClickEvent): 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>): ClickEvent | null {
|
|
27
|
+
try {
|
|
28
|
+
return ClickEvent.create({
|
|
29
|
+
id: model.id,
|
|
30
|
+
recommendationId: model.get('recommendation_id') as string,
|
|
31
|
+
memberId: model.get('member_id') as string | null,
|
|
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 ClickEvent, string>;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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.BookshelfRecommendationRepository = void 0;
|
|
7
|
+
const BookshelfRepository_1 = require("./BookshelfRepository");
|
|
8
|
+
const logging_1 = __importDefault(require("@tryghost/logging"));
|
|
9
|
+
const Recommendation_1 = require("./Recommendation");
|
|
10
|
+
class BookshelfRecommendationRepository extends BookshelfRepository_1.BookshelfRepository {
|
|
11
|
+
sentry;
|
|
12
|
+
constructor(Model, deps = {}) {
|
|
13
|
+
super(Model);
|
|
14
|
+
this.sentry = deps.sentry;
|
|
15
|
+
}
|
|
16
|
+
applyCustomQuery(query, options) {
|
|
17
|
+
query.select('recommendations.*');
|
|
18
|
+
if (options.include?.includes('clickCount') || options.order?.find(o => o.field === 'clickCount')) {
|
|
19
|
+
query.select((knex) => {
|
|
20
|
+
knex.count('*').from('recommendation_click_events').where('recommendation_click_events.recommendation_id', knex.client.raw('recommendations.id')).as('count__clicks');
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
if (options.include?.includes('subscriberCount') || options.order?.find(o => o.field === 'subscriberCount')) {
|
|
24
|
+
query.select((knex) => {
|
|
25
|
+
knex.count('*').from('recommendation_subscribe_events').where('recommendation_subscribe_events.recommendation_id', knex.client.raw('recommendations.id')).as('count__subscribers');
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
toPrimitive(entity) {
|
|
30
|
+
return {
|
|
31
|
+
id: entity.id,
|
|
32
|
+
title: entity.title,
|
|
33
|
+
description: entity.description,
|
|
34
|
+
excerpt: entity.excerpt,
|
|
35
|
+
featured_image: entity.featuredImage?.toString(),
|
|
36
|
+
favicon: entity.favicon?.toString(),
|
|
37
|
+
url: entity.url.toString(),
|
|
38
|
+
one_click_subscribe: entity.oneClickSubscribe,
|
|
39
|
+
created_at: entity.createdAt,
|
|
40
|
+
updated_at: entity.updatedAt
|
|
41
|
+
// Count relations are not saveable: so don't set them here
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
modelToEntity(model) {
|
|
45
|
+
try {
|
|
46
|
+
return Recommendation_1.Recommendation.create({
|
|
47
|
+
id: model.id,
|
|
48
|
+
title: model.get('title'),
|
|
49
|
+
description: model.get('description'),
|
|
50
|
+
excerpt: model.get('excerpt'),
|
|
51
|
+
featuredImage: model.get('featured_image'),
|
|
52
|
+
favicon: model.get('favicon'),
|
|
53
|
+
url: model.get('url'),
|
|
54
|
+
oneClickSubscribe: model.get('one_click_subscribe'),
|
|
55
|
+
createdAt: model.get('created_at'),
|
|
56
|
+
updatedAt: model.get('updated_at'),
|
|
57
|
+
clickCount: (model.get('count__clicks') ?? undefined),
|
|
58
|
+
subscriberCount: (model.get('count__subscribers') ?? undefined)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
logging_1.default.error(err);
|
|
63
|
+
this.sentry?.captureException(err);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
getFieldToColumnMap() {
|
|
68
|
+
return {
|
|
69
|
+
id: 'id',
|
|
70
|
+
title: 'title',
|
|
71
|
+
description: 'description',
|
|
72
|
+
excerpt: 'excerpt',
|
|
73
|
+
featuredImage: 'featured_image',
|
|
74
|
+
favicon: 'favicon',
|
|
75
|
+
url: 'url',
|
|
76
|
+
oneClickSubscribe: 'one_click_subscribe',
|
|
77
|
+
createdAt: 'created_at',
|
|
78
|
+
updatedAt: 'updated_at',
|
|
79
|
+
clickCount: 'count__clicks',
|
|
80
|
+
subscriberCount: 'count__subscribers'
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async getByUrl(url) {
|
|
84
|
+
const urlFilter = `url:~'${url.host.replace('www.', '')}${url.pathname.replace(/\/$/, '')}'`;
|
|
85
|
+
const recommendations = await this.getAll({ filter: urlFilter });
|
|
86
|
+
if (!recommendations || recommendations.length === 0) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
// Find URL based on the hostname and pathname.
|
|
90
|
+
// Query params, hash fragements, protocol and www are ignored.
|
|
91
|
+
const existing = recommendations.find((r) => {
|
|
92
|
+
return r.url.hostname.replace('www.', '') === url.hostname.replace('www.', '') &&
|
|
93
|
+
r.url.pathname.replace(/\/$/, '') === url.pathname.replace(/\/$/, '');
|
|
94
|
+
}) || null;
|
|
95
|
+
return existing;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.BookshelfRecommendationRepository = BookshelfRecommendationRepository;
|