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,54 @@
|
|
|
1
|
+
const _ = require('lodash').runInContext();
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
_.templateSettings.interpolate = /{{([\s\S]+?)}}/g;
|
|
6
|
+
|
|
7
|
+
class EmailContentGenerator {
|
|
8
|
+
/**
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} options
|
|
11
|
+
* @param {function} options.getSiteUrl
|
|
12
|
+
* @param {function} options.getSiteTitle
|
|
13
|
+
* @param {string} options.templatesDir - path to the directory containing email templates
|
|
14
|
+
*/
|
|
15
|
+
constructor({getSiteUrl, getSiteTitle, templatesDir}) {
|
|
16
|
+
this.getSiteUrl = getSiteUrl;
|
|
17
|
+
this.getSiteTitle = getSiteTitle;
|
|
18
|
+
this.templatesDir = templatesDir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} options
|
|
24
|
+
* @param {string} options.template - HTML template name to use for generation
|
|
25
|
+
* @param {Object} [options.data] - variable data to use during HTML template compilation
|
|
26
|
+
* @returns {Promise<{html: String, text: String}>} resolves with an object containing html and text properties
|
|
27
|
+
*/
|
|
28
|
+
async getContent(options) {
|
|
29
|
+
const defaults = {
|
|
30
|
+
siteUrl: this.getSiteUrl(),
|
|
31
|
+
siteTitle: this.getSiteTitle()
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const data = _.defaults(defaults, options.data);
|
|
35
|
+
|
|
36
|
+
// read the proper email body template
|
|
37
|
+
const content = await fs.readFile(path.join(this.templatesDir, options.template + '.html'), 'utf8');
|
|
38
|
+
|
|
39
|
+
// insert user-specific data into the email
|
|
40
|
+
const compiled = _.template(content);
|
|
41
|
+
const htmlContent = compiled(data);
|
|
42
|
+
|
|
43
|
+
// lazyload the lib, and generate a plain-text version of the same email
|
|
44
|
+
const htmlToText = require('html-to-text');
|
|
45
|
+
const textContent = htmlToText.fromString(htmlContent);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
html: htmlContent,
|
|
49
|
+
text: textContent
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = EmailContentGenerator;
|
|
@@ -0,0 +1,62 @@
|
|
|
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.InMemoryRepository = void 0;
|
|
7
|
+
const nql_1 = __importDefault(require("@tryghost/nql"));
|
|
8
|
+
class InMemoryRepository {
|
|
9
|
+
store = [];
|
|
10
|
+
ids = new Map();
|
|
11
|
+
async save(entity) {
|
|
12
|
+
if (entity.deleted) {
|
|
13
|
+
this.store = this.store.filter(item => item.id !== entity.id);
|
|
14
|
+
this.ids.delete(entity.id);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (this.ids.has(entity.id)) {
|
|
18
|
+
this.store = this.store.map((item) => {
|
|
19
|
+
if (item.id === entity.id) {
|
|
20
|
+
return entity;
|
|
21
|
+
}
|
|
22
|
+
return item;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
this.store.push(entity);
|
|
27
|
+
this.ids.set(entity.id, true);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async getById(id) {
|
|
31
|
+
return this.store.find(item => item.id === id) || null;
|
|
32
|
+
}
|
|
33
|
+
async getAll(options = {}) {
|
|
34
|
+
const filter = (0, nql_1.default)(options.filter);
|
|
35
|
+
const results = this.store.slice().filter(item => filter.queryJSON(this.toPrimitive(item)));
|
|
36
|
+
if (options.order) {
|
|
37
|
+
for (const order of options.order) {
|
|
38
|
+
results.sort((a, b) => {
|
|
39
|
+
if (order.direction === 'asc') {
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
return a[order.field] > b[order.field] ? 1 : -1;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
return a[order.field] < b[order.field] ? 1 : -1;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
async getPage(options = { page: 1, limit: 15 }) {
|
|
52
|
+
const results = await this.getAll(options);
|
|
53
|
+
const start = (options.page - 1) * options.limit;
|
|
54
|
+
const end = start + options.limit;
|
|
55
|
+
return results.slice(start, end);
|
|
56
|
+
}
|
|
57
|
+
async getCount(options) {
|
|
58
|
+
const results = await this.getAll(options);
|
|
59
|
+
return results.length;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.InMemoryRepository = InMemoryRepository;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import nql from '@tryghost/nql';
|
|
2
|
+
|
|
3
|
+
type Entity<T> = {
|
|
4
|
+
id: T;
|
|
5
|
+
deleted: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type Order<T> = {
|
|
9
|
+
field: keyof T;
|
|
10
|
+
direction: 'asc' | 'desc';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
type OrderOption<T extends Entity<any>> = Order<T>[];
|
|
15
|
+
|
|
16
|
+
export abstract class InMemoryRepository<IDType, T extends Entity<IDType>> {
|
|
17
|
+
protected store: T[] = [];
|
|
18
|
+
private ids: Map<IDType, true> = new Map();
|
|
19
|
+
|
|
20
|
+
protected abstract toPrimitive(entity: T): object;
|
|
21
|
+
|
|
22
|
+
public async save(entity: T): Promise<void> {
|
|
23
|
+
if (entity.deleted) {
|
|
24
|
+
this.store = this.store.filter(item => item.id !== entity.id);
|
|
25
|
+
this.ids.delete(entity.id);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (this.ids.has(entity.id)) {
|
|
30
|
+
this.store = this.store.map((item) => {
|
|
31
|
+
if (item.id === entity.id) {
|
|
32
|
+
return entity;
|
|
33
|
+
}
|
|
34
|
+
return item;
|
|
35
|
+
});
|
|
36
|
+
} else {
|
|
37
|
+
this.store.push(entity);
|
|
38
|
+
this.ids.set(entity.id, true);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public async getById(id: string): Promise<T | null> {
|
|
43
|
+
return this.store.find(item => item.id === id) || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public async getAll(options: { filter?: string; order?: OrderOption<T> } = {}): Promise<T[]> {
|
|
47
|
+
const filter = nql(options.filter);
|
|
48
|
+
|
|
49
|
+
const results = this.store.slice().filter(item => filter.queryJSON(this.toPrimitive(item)));
|
|
50
|
+
|
|
51
|
+
if (options.order) {
|
|
52
|
+
for (const order of options.order) {
|
|
53
|
+
results.sort((a, b) => {
|
|
54
|
+
if (order.direction === 'asc') {
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
|
+
return a[order.field] as any > (b[order.field] as any) ? 1 : -1;
|
|
57
|
+
} else {
|
|
58
|
+
return a[order.field] < b[order.field] ? 1 : -1;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public async getPage(options: { filter?: string; page: number; limit: number; order?: Order<T>[] } = {page: 1, limit: 15}): Promise<T[]> {
|
|
68
|
+
const results = await this.getAll(options);
|
|
69
|
+
|
|
70
|
+
const start = (options.page - 1) * options.limit;
|
|
71
|
+
const end = start + options.limit;
|
|
72
|
+
|
|
73
|
+
return results.slice(start, end);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public async getCount(options: { filter?: string }): Promise<number> {
|
|
77
|
+
const results = await this.getAll(options);
|
|
78
|
+
return results.length;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
const debug = require('@tryghost/debug');
|
|
3
|
+
const logging = require('@tryghost/logging');
|
|
4
|
+
const metrics = require('@tryghost/metrics');
|
|
5
|
+
const errors = require('@tryghost/errors');
|
|
6
|
+
|
|
7
|
+
module.exports = class MailgunClient {
|
|
8
|
+
#config;
|
|
9
|
+
#settings;
|
|
10
|
+
|
|
11
|
+
static DEFAULT_BATCH_SIZE = 1000;
|
|
12
|
+
|
|
13
|
+
constructor({config, settings}) {
|
|
14
|
+
this.#config = config;
|
|
15
|
+
this.#settings = settings;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates the data payload and sends to Mailgun
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} message
|
|
22
|
+
* @param {Object} recipientData
|
|
23
|
+
* @param {Array<Object>} replacements
|
|
24
|
+
*
|
|
25
|
+
* recipientData format:
|
|
26
|
+
* {
|
|
27
|
+
* 'test@example.com': {
|
|
28
|
+
* name: 'Test User',
|
|
29
|
+
* unsubscribe_url: 'https://example.com/unsub/me',
|
|
30
|
+
* list_unsubscribe: 'https://example.com/unsub/me'
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
*/
|
|
34
|
+
async send(message, recipientData, replacements) {
|
|
35
|
+
const mailgunInstance = this.getInstance();
|
|
36
|
+
if (!mailgunInstance) {
|
|
37
|
+
logging.warn(`Mailgun is not configured`);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const batchSize = this.getBatchSize();
|
|
42
|
+
if (Object.keys(recipientData).length > batchSize) {
|
|
43
|
+
throw new errors.IncorrectUsageError({
|
|
44
|
+
message: `Mailgun only supports sending to ${batchSize} recipients at a time`
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let messageData = {};
|
|
49
|
+
|
|
50
|
+
let startTime;
|
|
51
|
+
try {
|
|
52
|
+
const bulkEmailConfig = this.#config.get('bulkEmail');
|
|
53
|
+
const messageContent = _.pick(message, 'subject', 'html', 'plaintext');
|
|
54
|
+
|
|
55
|
+
// update content to use Mailgun variable syntax for replacements
|
|
56
|
+
replacements.forEach((replacement) => {
|
|
57
|
+
messageContent[replacement.format] = messageContent[replacement.format].replace(
|
|
58
|
+
replacement.regexp,
|
|
59
|
+
`%recipient.${replacement.id}%`
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
messageData = {
|
|
64
|
+
to: Object.keys(recipientData),
|
|
65
|
+
from: message.from,
|
|
66
|
+
'h:Reply-To': message.replyTo || message.reply_to,
|
|
67
|
+
subject: messageContent.subject,
|
|
68
|
+
html: messageContent.html,
|
|
69
|
+
text: messageContent.plaintext,
|
|
70
|
+
'recipient-variables': JSON.stringify(recipientData)
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Do we have a custom List-Unsubscribe header set?
|
|
74
|
+
// (we need a variable for this, as this is a per-email setting)
|
|
75
|
+
if (Object.keys(recipientData)[0] && recipientData[Object.keys(recipientData)[0]].list_unsubscribe) {
|
|
76
|
+
messageData['h:List-Unsubscribe'] = '<%recipient.list_unsubscribe%>, <%tag_unsubscribe_email%>';
|
|
77
|
+
messageData['h:List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// add a reference to the original email record for easier mapping of mailgun event -> email
|
|
81
|
+
if (message.id) {
|
|
82
|
+
messageData['v:email-id'] = message.id;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const tags = ['bulk-email', 'ghost-email'];
|
|
86
|
+
if (bulkEmailConfig?.mailgun?.tag) {
|
|
87
|
+
tags.push(bulkEmailConfig.mailgun.tag);
|
|
88
|
+
}
|
|
89
|
+
messageData['o:tag'] = tags;
|
|
90
|
+
|
|
91
|
+
if (bulkEmailConfig?.mailgun?.testmode) {
|
|
92
|
+
messageData['o:testmode'] = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// enable tracking if turned on for this email
|
|
96
|
+
if (message.track_opens) {
|
|
97
|
+
messageData['o:tracking-opens'] = true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// set the delivery time if specified
|
|
101
|
+
if (message.deliveryTime && message.deliveryTime instanceof Date) {
|
|
102
|
+
messageData['o:deliverytime'] = message.deliveryTime.toUTCString();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const mailgunConfig = this.#getConfig();
|
|
106
|
+
startTime = Date.now();
|
|
107
|
+
const response = await mailgunInstance.messages.create(mailgunConfig.domain, messageData);
|
|
108
|
+
metrics.metric('mailgun-send-mail', {
|
|
109
|
+
value: Date.now() - startTime,
|
|
110
|
+
statusCode: 200
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
id: response.id
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
logging.error(error);
|
|
118
|
+
metrics.metric('mailgun-send-mail', {
|
|
119
|
+
value: Date.now() - startTime,
|
|
120
|
+
statusCode: error.status
|
|
121
|
+
});
|
|
122
|
+
return Promise.reject({error, messageData});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {import('mailgun.js').default} mailgunInstance
|
|
128
|
+
* @param {Object} mailgunConfig
|
|
129
|
+
* @param {Object} mailgunOptions
|
|
130
|
+
*/
|
|
131
|
+
async getEventsFromMailgun(mailgunInstance, mailgunConfig, mailgunOptions) {
|
|
132
|
+
const startTime = Date.now();
|
|
133
|
+
try {
|
|
134
|
+
const page = await mailgunInstance.events.get(mailgunConfig.domain, mailgunOptions);
|
|
135
|
+
metrics.metric('mailgun-get-events', {
|
|
136
|
+
value: Date.now() - startTime,
|
|
137
|
+
statusCode: 200
|
|
138
|
+
});
|
|
139
|
+
return page;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
metrics.metric('mailgun-get-events', {
|
|
142
|
+
value: Date.now() - startTime,
|
|
143
|
+
statusCode: error.status
|
|
144
|
+
});
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Fetches events from Mailgun
|
|
151
|
+
* @param {Object} mailgunOptions
|
|
152
|
+
* @param {Function} batchHandler
|
|
153
|
+
* @param {Object} options
|
|
154
|
+
* @param {Number} options.maxEvents Not a strict maximum. We stop fetching after we reached the maximum AND received at least one event after begin (not equal) to prevent deadlocks.
|
|
155
|
+
* @returns {Promise<void>}
|
|
156
|
+
*/
|
|
157
|
+
async fetchEvents(mailgunOptions, batchHandler, {maxEvents = Infinity} = {}) {
|
|
158
|
+
const mailgunInstance = this.getInstance();
|
|
159
|
+
if (!mailgunInstance) {
|
|
160
|
+
logging.warn(`Mailgun is not configured`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
debug(`[MailgunClient fetchEvents]: starting fetching first events page`);
|
|
165
|
+
const mailgunConfig = this.#getConfig();
|
|
166
|
+
const startDate = new Date();
|
|
167
|
+
const overallStartTime = Date.now();
|
|
168
|
+
|
|
169
|
+
let batchCount = 0;
|
|
170
|
+
let totalBatchTime = 0;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
let page = await this.getEventsFromMailgun(mailgunInstance, mailgunConfig, mailgunOptions);
|
|
174
|
+
|
|
175
|
+
// By limiting the processed events to ones created before this job started we cancel early ready for the next job run.
|
|
176
|
+
// Avoids chance of events being missed in long job runs due to mailgun's eventual-consistency creating events outside of our 30min sliding re-check window
|
|
177
|
+
let events = (page?.items?.map(this.normalizeEvent) || []).filter(e => !!e && e.timestamp <= startDate);
|
|
178
|
+
debug(`[MailgunClient fetchEvents]: finished fetching first page with ${events.length} events`);
|
|
179
|
+
|
|
180
|
+
let eventCount = 0;
|
|
181
|
+
const beginTimestamp = mailgunOptions.begin ? Math.ceil(mailgunOptions.begin * 1000) : undefined; // ceil here if we have rounding errors
|
|
182
|
+
|
|
183
|
+
while (events.length !== 0) {
|
|
184
|
+
const batchStartTime = Date.now();
|
|
185
|
+
await batchHandler(events);
|
|
186
|
+
const batchEndTime = Date.now();
|
|
187
|
+
const batchDuration = batchEndTime - batchStartTime;
|
|
188
|
+
|
|
189
|
+
batchCount += 1;
|
|
190
|
+
totalBatchTime += batchDuration;
|
|
191
|
+
|
|
192
|
+
eventCount += events.length;
|
|
193
|
+
|
|
194
|
+
if (eventCount >= maxEvents && (!beginTimestamp || !events[events.length - 1].timestamp || (events[events.length - 1].timestamp.getTime() > beginTimestamp))) {
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const nextPageId = page.pages.next.page;
|
|
199
|
+
debug(`[MailgunClient fetchEvents]: starting fetching next page ${nextPageId}`);
|
|
200
|
+
page = await this.getEventsFromMailgun(mailgunInstance, mailgunConfig, {
|
|
201
|
+
page: nextPageId,
|
|
202
|
+
...mailgunOptions
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// We need to cap events at the time we started fetching them (see comment above)
|
|
206
|
+
events = (page?.items?.map(this.normalizeEvent) || []).filter(e => !!e && e.timestamp <= startDate);
|
|
207
|
+
debug(`[MailgunClient fetchEvents]: finished fetching next page with ${events.length} events`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const overallEndTime = Date.now();
|
|
211
|
+
const totalDuration = overallEndTime - overallStartTime;
|
|
212
|
+
const averageBatchTime = batchCount > 0 ? totalBatchTime / batchCount : 0;
|
|
213
|
+
|
|
214
|
+
logging.info(`[MailgunClient fetchEvents]: Processed ${batchCount} batches in ${(totalDuration / 1000).toFixed(2)}s. Average batch time: ${(averageBatchTime / 1000).toFixed(2)}s`);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
logging.error(error);
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async removeSuppression(type, email) {
|
|
222
|
+
if (!this.isConfigured()) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
const instance = this.getInstance();
|
|
226
|
+
const config = this.#getConfig();
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
await instance.suppressions.destroy(
|
|
230
|
+
config.domain,
|
|
231
|
+
type,
|
|
232
|
+
email
|
|
233
|
+
);
|
|
234
|
+
return true;
|
|
235
|
+
} catch (err) {
|
|
236
|
+
logging.error(err);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async removeBounce(email) {
|
|
242
|
+
return this.removeSuppression('bounces', email);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async removeComplaint(email) {
|
|
246
|
+
return this.removeSuppression('complaints', email);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async removeUnsubscribe(email) {
|
|
250
|
+
return this.removeSuppression('unsubscribes', email);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
normalizeEvent(event) {
|
|
254
|
+
const providerId = event?.message?.headers['message-id'];
|
|
255
|
+
|
|
256
|
+
if (!providerId && !(event['user-variables'] && event['user-variables']['email-id'])) {
|
|
257
|
+
logging.error('Received invalid event from Mailgun');
|
|
258
|
+
logging.error(event);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
id: event.id,
|
|
264
|
+
type: event.event,
|
|
265
|
+
severity: event.severity,
|
|
266
|
+
recipientEmail: event.recipient,
|
|
267
|
+
emailId: event['user-variables'] && event['user-variables']['email-id'],
|
|
268
|
+
providerId: providerId,
|
|
269
|
+
timestamp: new Date(event.timestamp * 1000),
|
|
270
|
+
|
|
271
|
+
error: event['delivery-status'] && (typeof (event['delivery-status'].message || event['delivery-status'].description) === 'string') ? {
|
|
272
|
+
code: event['delivery-status'].code,
|
|
273
|
+
message: (event['delivery-status'].message || event['delivery-status'].description).substring(0, 2000),
|
|
274
|
+
enhancedCode: event['delivery-status']['enhanced-code']?.toString()?.substring(0, 50) ?? null
|
|
275
|
+
} : null
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#getConfig() {
|
|
280
|
+
const bulkEmailConfig = this.#config.get('bulkEmail');
|
|
281
|
+
const bulkEmailSetting = {
|
|
282
|
+
apiKey: this.#settings.get('mailgun_api_key'),
|
|
283
|
+
domain: this.#settings.get('mailgun_domain'),
|
|
284
|
+
baseUrl: this.#settings.get('mailgun_base_url')
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const hasMailgunConfig = !!(bulkEmailConfig?.mailgun);
|
|
288
|
+
const hasMailgunSetting = !!(bulkEmailSetting && bulkEmailSetting.apiKey && bulkEmailSetting.baseUrl && bulkEmailSetting.domain);
|
|
289
|
+
|
|
290
|
+
if (!hasMailgunConfig && !hasMailgunSetting) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const mailgunConfig = hasMailgunConfig ? bulkEmailConfig.mailgun : bulkEmailSetting;
|
|
295
|
+
return mailgunConfig;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Returns an instance of the Mailgun client based upon the config or settings values
|
|
300
|
+
*
|
|
301
|
+
* We don't cache the instance so we can always get a fresh one based upon changed settings
|
|
302
|
+
* or config values over time
|
|
303
|
+
*
|
|
304
|
+
* Note: if the credentials are not configure, this method returns `null` and it is down to the
|
|
305
|
+
* consumer to act upon this/log this out
|
|
306
|
+
*
|
|
307
|
+
* @returns {import('mailgun.js')|null} the Mailgun client instance
|
|
308
|
+
*/
|
|
309
|
+
getInstance() {
|
|
310
|
+
const mailgunConfig = this.#getConfig();
|
|
311
|
+
if (!mailgunConfig) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const formData = require('form-data');
|
|
316
|
+
const Mailgun = require('mailgun.js');
|
|
317
|
+
|
|
318
|
+
const baseUrl = new URL(mailgunConfig.baseUrl);
|
|
319
|
+
const mailgun = new Mailgun(formData);
|
|
320
|
+
|
|
321
|
+
return mailgun.client({
|
|
322
|
+
username: 'api',
|
|
323
|
+
key: mailgunConfig.apiKey,
|
|
324
|
+
url: baseUrl.origin,
|
|
325
|
+
timeout: 60000
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Returns whether the Mailgun instance is configured via config/settings
|
|
331
|
+
*
|
|
332
|
+
* @returns {boolean}
|
|
333
|
+
*/
|
|
334
|
+
isConfigured() {
|
|
335
|
+
const instance = this.getInstance();
|
|
336
|
+
return !!instance;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Returns configured batch size
|
|
341
|
+
*
|
|
342
|
+
* @returns {number}
|
|
343
|
+
*/
|
|
344
|
+
getBatchSize() {
|
|
345
|
+
return this.#config.get('bulkEmail')?.batchSize ?? this.DEFAULT_BATCH_SIZE;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Returns the configured target delivery window in seconds
|
|
350
|
+
* Ghost will attempt to deliver emails evenly distributed over this window
|
|
351
|
+
*
|
|
352
|
+
* Defaults to 0 (no delay) if not set
|
|
353
|
+
*
|
|
354
|
+
* @returns {number}
|
|
355
|
+
*/
|
|
356
|
+
getTargetDeliveryWindow() {
|
|
357
|
+
const targetDeliveryWindow = this.#config.get('bulkEmail')?.targetDeliveryWindow;
|
|
358
|
+
// If targetDeliveryWindow is not set or is not a positive integer, return 0
|
|
359
|
+
if (targetDeliveryWindow === undefined || !Number.isInteger(parseInt(targetDeliveryWindow)) || parseInt(targetDeliveryWindow) < 0) {
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
return parseInt(targetDeliveryWindow);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const ObjectID = require('bson-objectid').default;
|
|
2
|
+
|
|
3
|
+
module.exports = class LinkRedirect {
|
|
4
|
+
/** @type {ObjectID} */
|
|
5
|
+
link_id;
|
|
6
|
+
/** @type {URL} */
|
|
7
|
+
from;
|
|
8
|
+
/** @type {URL} */
|
|
9
|
+
to;
|
|
10
|
+
/** @type {boolean} */
|
|
11
|
+
edited;
|
|
12
|
+
|
|
13
|
+
constructor(data) {
|
|
14
|
+
if (!data.id) {
|
|
15
|
+
this.link_id = new ObjectID();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof data.id === 'string') {
|
|
19
|
+
this.link_id = ObjectID.createFromHexString(data.id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.from = data.from;
|
|
23
|
+
this.to = data.to;
|
|
24
|
+
this.edited = !!data.edited;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const LinkRedirect = require('
|
|
1
|
+
const LinkRedirect = require('./LinkRedirect');
|
|
2
2
|
const ObjectID = require('bson-objectid').default;
|
|
3
3
|
const debug = require('@tryghost/debug')('LinkRedirectRepository');
|
|
4
4
|
|
|
@@ -17,7 +17,7 @@ module.exports = class LinkRedirectRepository {
|
|
|
17
17
|
* @param {object} deps.LinkRedirect - Bookshelf Model
|
|
18
18
|
* @param {object} deps.urlUtils
|
|
19
19
|
* @param {object} deps.cacheAdapter - Cache Adapter instance, or null if cache is disabled
|
|
20
|
-
* @param {object} deps.EventRegistry
|
|
20
|
+
* @param {object} deps.EventRegistry
|
|
21
21
|
*/
|
|
22
22
|
constructor(deps) {
|
|
23
23
|
debug('Creating LinkRedirectRepository');
|
|
@@ -61,7 +61,7 @@ module.exports = class LinkRedirectRepository {
|
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Trim the leading slash from a URL path
|
|
64
|
-
* @param {string} url
|
|
64
|
+
* @param {string} url
|
|
65
65
|
* @returns {string} url without leading slash
|
|
66
66
|
*/
|
|
67
67
|
#trimLeadingSlash(url) {
|
|
@@ -70,14 +70,14 @@ module.exports = class LinkRedirectRepository {
|
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
72
|
* Returns a LinkRedirect object from a model
|
|
73
|
-
* @param {object} model - Bookshelf model instance
|
|
73
|
+
* @param {object} model - Bookshelf model instance
|
|
74
74
|
* @returns {InstanceType<LinkRedirect>} LinkRedirect
|
|
75
75
|
*/
|
|
76
76
|
fromModel(model) {
|
|
77
77
|
// Store if link has been edited
|
|
78
78
|
// Note: in some edge cases updated_at is set directly after created_at, sometimes with a second difference, so we need to check for that
|
|
79
79
|
const edited = model.get('updated_at')?.getTime() > (model.get('created_at')?.getTime() + 1000);
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
return new LinkRedirect({
|
|
82
82
|
id: model.id,
|
|
83
83
|
from: new URL(this.#trimLeadingSlash(model.get('from')), this.#urlUtils.urlFor('home', true)),
|
|
@@ -106,7 +106,7 @@ module.exports = class LinkRedirectRepository {
|
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
108
|
* Serialize a LinkRedirect object to a plain object (e.g. for caching)
|
|
109
|
-
* @param {InstanceType<LinkRedirect>} linkRedirect
|
|
109
|
+
* @param {InstanceType<LinkRedirect>} linkRedirect
|
|
110
110
|
* @returns {object} - serialized LinkRedirect
|
|
111
111
|
*/
|
|
112
112
|
#serialize(linkRedirect) {
|
|
@@ -120,7 +120,7 @@ module.exports = class LinkRedirectRepository {
|
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
122
|
* Get all LinkRedirects from the DB, with optional filters
|
|
123
|
-
* @param {object} options - options passed directly to LinkRedirect.findAll
|
|
123
|
+
* @param {object} options - options passed directly to LinkRedirect.findAll
|
|
124
124
|
* @returns {Promise<InstanceType<LinkRedirect>[]>} array of LinkRedirects
|
|
125
125
|
*/
|
|
126
126
|
async getAll(options) {
|