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,212 @@
|
|
|
1
|
+
const TableImporter = require('./TableImporter');
|
|
2
|
+
const {faker} = require('@faker-js/faker');
|
|
3
|
+
const generateEvents = require('../utils/event-generator');
|
|
4
|
+
const dateToDatabaseString = require('../utils/database-date');
|
|
5
|
+
const debug = require('@tryghost/debug')('EmailRecipientsImporter');
|
|
6
|
+
|
|
7
|
+
const emailStatus = {
|
|
8
|
+
delivered: Symbol(),
|
|
9
|
+
opened: Symbol(),
|
|
10
|
+
failed: Symbol(),
|
|
11
|
+
none: Symbol()
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function findFirstHigherIndex(arr, target) {
|
|
15
|
+
let start = 0;
|
|
16
|
+
let end = arr.length - 1;
|
|
17
|
+
let result = -1;
|
|
18
|
+
|
|
19
|
+
while (start <= end) {
|
|
20
|
+
let mid = Math.floor((start + end) / 2);
|
|
21
|
+
|
|
22
|
+
if (arr[mid] >= target) {
|
|
23
|
+
result = mid;
|
|
24
|
+
end = mid - 1; // Continue searching in the left half
|
|
25
|
+
} else {
|
|
26
|
+
start = mid + 1; // Continue searching in the right half
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result; // Return -1 if no element is higher than target
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class EmailRecipientsImporter extends TableImporter {
|
|
34
|
+
static table = 'email_recipients';
|
|
35
|
+
static dependencies = ['emails', 'email_batches', 'members', 'members_subscribe_events'];
|
|
36
|
+
|
|
37
|
+
constructor(knex, transaction) {
|
|
38
|
+
super(EmailRecipientsImporter.table, knex, transaction);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async import(quantity) {
|
|
42
|
+
if (quantity === 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const emails = await this.transaction
|
|
48
|
+
.select(
|
|
49
|
+
'id',
|
|
50
|
+
'newsletter_id',
|
|
51
|
+
'email_count',
|
|
52
|
+
'delivered_count',
|
|
53
|
+
'opened_count',
|
|
54
|
+
'failed_count')
|
|
55
|
+
.from('emails');
|
|
56
|
+
this.emails = new Map();
|
|
57
|
+
|
|
58
|
+
for (const email of emails) {
|
|
59
|
+
this.emails.set(email.id, email);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.emailBatches = await this.transaction.select('id', 'email_id', 'updated_at').from('email_batches').orderBy('email_id');
|
|
63
|
+
const members = await this.transaction.select('id', 'uuid', 'email', 'name').from('members');
|
|
64
|
+
this.membersSubscribeEvents = await this.transaction.select('id', 'newsletter_id', 'created_at', 'member_id').from('members_subscribe_events').orderBy('created_at', 'asc'); // Order required for better performance in setReferencedModel
|
|
65
|
+
|
|
66
|
+
// Create a map for fast lookups
|
|
67
|
+
this.members = new Map();
|
|
68
|
+
for (const member of members) {
|
|
69
|
+
this.members.set(member.id, member);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Save indexes of each batch for performance (remarkable faster than doing findIndex on each generate call)
|
|
73
|
+
let lastEmailId = null;
|
|
74
|
+
let lastIndex = 0;
|
|
75
|
+
for (const batch of this.emailBatches) {
|
|
76
|
+
if (batch.email_id !== lastEmailId) {
|
|
77
|
+
lastIndex = 0;
|
|
78
|
+
lastEmailId = batch.email_id;
|
|
79
|
+
}
|
|
80
|
+
batch.index = lastIndex;
|
|
81
|
+
lastIndex += 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Now reorder by email id
|
|
85
|
+
|
|
86
|
+
debug (`Prepared data for ${this.name} in ${Date.now() - now}ms`);
|
|
87
|
+
|
|
88
|
+
// We use the same event curve for all emails to speed up the generation
|
|
89
|
+
// Spread over 14 days
|
|
90
|
+
this.eventStartTimeUsed = new Date();
|
|
91
|
+
const endTime = new Date(this.eventStartTimeUsed.getTime() + 1000 * 60 * 60 * 24 * 14);
|
|
92
|
+
this.eventCurve = generateEvents({
|
|
93
|
+
shape: 'ease-out',
|
|
94
|
+
trend: 'negative',
|
|
95
|
+
total: 1000,
|
|
96
|
+
startTime: this.eventStartTimeUsed,
|
|
97
|
+
endTime
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this.membersSubscribeEventsByNewsletterId = new Map();
|
|
101
|
+
this.membersSubscribeEventsCreatedAtsByNewsletterId = new Map();
|
|
102
|
+
for (const memberSubscribeEvent of this.membersSubscribeEvents) {
|
|
103
|
+
if (!this.membersSubscribeEventsByNewsletterId.has(memberSubscribeEvent.newsletter_id)) {
|
|
104
|
+
this.membersSubscribeEventsByNewsletterId.set(memberSubscribeEvent.newsletter_id, []);
|
|
105
|
+
}
|
|
106
|
+
this.membersSubscribeEventsByNewsletterId.get(memberSubscribeEvent.newsletter_id).push(memberSubscribeEvent);
|
|
107
|
+
|
|
108
|
+
if (!this.membersSubscribeEventsCreatedAtsByNewsletterId.has(memberSubscribeEvent.newsletter_id)) {
|
|
109
|
+
this.membersSubscribeEventsCreatedAtsByNewsletterId.set(memberSubscribeEvent.newsletter_id, []);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!(memberSubscribeEvent.created_at instanceof Date)) {
|
|
113
|
+
// SQLite fix
|
|
114
|
+
memberSubscribeEvent.created_at = new Date(memberSubscribeEvent.created_at);
|
|
115
|
+
}
|
|
116
|
+
this.membersSubscribeEventsCreatedAtsByNewsletterId.get(memberSubscribeEvent.newsletter_id).push(memberSubscribeEvent.created_at.getTime());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await this.importForEach(this.emailBatches, quantity ? quantity / emails.length : 1000);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setReferencedModel(model) {
|
|
123
|
+
this.batch = model;
|
|
124
|
+
this.model = this.emails.get(this.batch.email_id);
|
|
125
|
+
this.batchIndex = this.batch.index;
|
|
126
|
+
|
|
127
|
+
// Shallow clone members list so we can shuffle and modify it
|
|
128
|
+
const earliestOpenTime = new Date(this.batch.updated_at);
|
|
129
|
+
const latestOpenTime = new Date(this.batch.updated_at);
|
|
130
|
+
latestOpenTime.setDate(latestOpenTime.getDate() + 14);
|
|
131
|
+
|
|
132
|
+
// Get all members that were subscribed to this newsletter BEFORE the batch was sent
|
|
133
|
+
// We use binary search to speed up it up
|
|
134
|
+
const lastIndex = findFirstHigherIndex(this.membersSubscribeEventsCreatedAtsByNewsletterId.get(this.model.newsletter_id), earliestOpenTime);
|
|
135
|
+
|
|
136
|
+
this.membersList = this.membersSubscribeEventsByNewsletterId.get(this.model.newsletter_id).slice(0, Math.max(0, lastIndex - 1))
|
|
137
|
+
.slice(this.batchIndex * 1000, (this.batchIndex + 1) * 1000)
|
|
138
|
+
.map(memberSubscribeEvent => memberSubscribeEvent.member_id);
|
|
139
|
+
|
|
140
|
+
this.events = faker.helpers.shuffle(this.eventCurve.slice(0, this.membersList.length));
|
|
141
|
+
this.eventIndex = 0;
|
|
142
|
+
|
|
143
|
+
this.emailMeta = {
|
|
144
|
+
// delievered and not opened
|
|
145
|
+
deliveredCount: this.model.delivered_count - this.model.opened_count,
|
|
146
|
+
openedCount: this.model.opened_count,
|
|
147
|
+
failedCount: this.model.failed_count
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
let offset = this.batchIndex * 1000;
|
|
151
|
+
|
|
152
|
+
// We always first create the failures, then the opened, then the delivered, so we need to remove those from the meta so we don't generate them multiple times
|
|
153
|
+
this.emailMeta = {
|
|
154
|
+
failedCount: Math.max(0, this.emailMeta.failedCount - offset),
|
|
155
|
+
openedCount: Math.max(0, this.emailMeta.openedCount - Math.max(0, offset - this.emailMeta.failedCount)),
|
|
156
|
+
deliveredCount: Math.max(0, this.emailMeta.deliveredCount - Math.max(0, offset - this.emailMeta.failedCount - this.emailMeta.openedCount))
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
generate() {
|
|
161
|
+
let timestamp = this.events.pop();
|
|
162
|
+
if (!timestamp) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// The events are generated for a different time, so we need to move them to the batch time
|
|
167
|
+
timestamp = new Date(timestamp.getTime() - this.eventStartTimeUsed.getTime() + new Date(this.batch.updated_at).getTime());
|
|
168
|
+
|
|
169
|
+
if (timestamp > new Date()) {
|
|
170
|
+
timestamp = new Date();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const memberId = this.membersList[this.events.length];
|
|
174
|
+
const member = this.members.get(memberId);
|
|
175
|
+
|
|
176
|
+
let status = emailStatus.none;
|
|
177
|
+
if (this.emailMeta.failedCount > 0) {
|
|
178
|
+
status = emailStatus.failed;
|
|
179
|
+
this.emailMeta.failedCount -= 1;
|
|
180
|
+
} else if (this.emailMeta.openedCount > 0) {
|
|
181
|
+
status = emailStatus.opened;
|
|
182
|
+
this.emailMeta.openedCount -= 1;
|
|
183
|
+
} else if (this.emailMeta.deliveredCount > 0) {
|
|
184
|
+
status = emailStatus.delivered;
|
|
185
|
+
this.emailMeta.deliveredCount -= 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let deliveredTime;
|
|
189
|
+
if (status === emailStatus.opened) {
|
|
190
|
+
const startDate = this.batch.updated_at;
|
|
191
|
+
const endDate = timestamp;
|
|
192
|
+
deliveredTime = faker.date.between(startDate, endDate);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
// Using sorted ids are much much faster (35% as far as my testing goes) for huge imports
|
|
197
|
+
id: this.fastFakeObjectId(),
|
|
198
|
+
email_id: this.model.id,
|
|
199
|
+
batch_id: this.batch.id,
|
|
200
|
+
member_id: member.id,
|
|
201
|
+
processed_at: dateToDatabaseString(this.batch.updated_at),
|
|
202
|
+
delivered_at: status === emailStatus.opened ? dateToDatabaseString(deliveredTime) : status === emailStatus.delivered ? dateToDatabaseString(timestamp) : null,
|
|
203
|
+
opened_at: status === emailStatus.opened ? dateToDatabaseString(timestamp) : null,
|
|
204
|
+
failed_at: status === emailStatus.failed ? dateToDatabaseString(timestamp) : null,
|
|
205
|
+
member_uuid: member.uuid,
|
|
206
|
+
member_email: member.email,
|
|
207
|
+
member_name: member.name
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = EmailRecipientsImporter;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const TableImporter = require('./TableImporter');
|
|
2
|
+
const {faker} = require('@faker-js/faker');
|
|
3
|
+
const generateEvents = require('../utils/event-generator');
|
|
4
|
+
const {luck} = require('../utils/random');
|
|
5
|
+
const dateToDatabaseString = require('../utils/database-date');
|
|
6
|
+
|
|
7
|
+
class EmailsImporter extends TableImporter {
|
|
8
|
+
static table = 'emails';
|
|
9
|
+
static dependencies = ['posts', 'newsletters', 'members_subscribe_events'];
|
|
10
|
+
|
|
11
|
+
constructor(knex, transaction) {
|
|
12
|
+
super(EmailsImporter.table, knex, transaction);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async import(quantity) {
|
|
16
|
+
if (quantity === 0) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const posts = await this.transaction.select('id', 'title', 'published_at').from('posts').where('type', 'post').where('status', 'published').orderBy('published_at', 'desc');
|
|
21
|
+
this.newsletters = await this.transaction.select('id').from('newsletters').orderBy('sort_order');
|
|
22
|
+
this.membersSubscribeEvents = await this.transaction.select('id', 'newsletter_id', 'created_at').from('members_subscribe_events');
|
|
23
|
+
|
|
24
|
+
// Only generate emails for last 25% of posts, and only generate emails for 50% of those
|
|
25
|
+
await this.importForEach(posts.slice(0, Math.ceil(posts.length / 4)), quantity ? quantity / posts.length : 0.5);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
generate() {
|
|
29
|
+
const id = this.fastFakeObjectId();
|
|
30
|
+
|
|
31
|
+
let newsletter;
|
|
32
|
+
if (this.newsletters.length === 0) {
|
|
33
|
+
return null;
|
|
34
|
+
} else if (this.newsletters.length === 1) {
|
|
35
|
+
newsletter = this.newsletters[0];
|
|
36
|
+
} else {
|
|
37
|
+
// Choose between first two newsletters
|
|
38
|
+
newsletter = luck(90)
|
|
39
|
+
// Regular premium
|
|
40
|
+
? this.newsletters[0]
|
|
41
|
+
// Occasional freebie
|
|
42
|
+
: this.newsletters[1];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const timestamp = luck(60)
|
|
46
|
+
? new Date(this.model.published_at)
|
|
47
|
+
: generateEvents({
|
|
48
|
+
shape: 'ease-out',
|
|
49
|
+
trend: 'negative',
|
|
50
|
+
total: 1,
|
|
51
|
+
startTime: new Date(this.model.published_at),
|
|
52
|
+
endTime: new Date()
|
|
53
|
+
})[0];
|
|
54
|
+
|
|
55
|
+
const recipientCount = this.membersSubscribeEvents
|
|
56
|
+
.filter(entry => entry.newsletter_id === newsletter.id)
|
|
57
|
+
.filter(entry => new Date(entry.created_at) < timestamp).length;
|
|
58
|
+
const deliveredCount = Math.ceil(recipientCount * faker.datatype.float({
|
|
59
|
+
max: 1,
|
|
60
|
+
min: 0.9,
|
|
61
|
+
precision: 0.001
|
|
62
|
+
}));
|
|
63
|
+
const openedCount = Math.ceil(deliveredCount * faker.datatype.float({
|
|
64
|
+
max: 0.95,
|
|
65
|
+
min: 0.6,
|
|
66
|
+
precision: 0.001
|
|
67
|
+
}));
|
|
68
|
+
const failedCount = Math.floor((recipientCount - deliveredCount) * faker.datatype.float({
|
|
69
|
+
max: 0.05,
|
|
70
|
+
min: 0,
|
|
71
|
+
precision: 0.001
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
id,
|
|
76
|
+
uuid: faker.datatype.uuid(),
|
|
77
|
+
post_id: this.model.id,
|
|
78
|
+
status: 'submitted',
|
|
79
|
+
recipient_filter: '', // TODO: Add recipient filter?
|
|
80
|
+
email_count: recipientCount,
|
|
81
|
+
delivered_count: deliveredCount,
|
|
82
|
+
opened_count: openedCount,
|
|
83
|
+
failed_count: failedCount,
|
|
84
|
+
subject: this.model.title,
|
|
85
|
+
source_type: 'html',
|
|
86
|
+
track_opens: true,
|
|
87
|
+
track_clicks: true,
|
|
88
|
+
feedback_enabled: true,
|
|
89
|
+
submitted_at: dateToDatabaseString(timestamp),
|
|
90
|
+
newsletter_id: newsletter.id,
|
|
91
|
+
created_at: dateToDatabaseString(timestamp),
|
|
92
|
+
created_by: 'unused',
|
|
93
|
+
updated_at: dateToDatabaseString(timestamp),
|
|
94
|
+
updated_by: 'unused'
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = EmailsImporter;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const TableImporter = require('./TableImporter');
|
|
2
|
+
const {faker} = require('@faker-js/faker');
|
|
3
|
+
const {slugify} = require('@tryghost/string');
|
|
4
|
+
const {blogStartDate} = require('../utils/blog-info');
|
|
5
|
+
const dateToDatabaseString = require('../utils/database-date');
|
|
6
|
+
|
|
7
|
+
class LabelsImporter extends TableImporter {
|
|
8
|
+
static table = 'labels';
|
|
9
|
+
static dependencies = [];
|
|
10
|
+
defaultQuantity = 10;
|
|
11
|
+
|
|
12
|
+
constructor(knex, transaction) {
|
|
13
|
+
super(LabelsImporter.table, knex, transaction);
|
|
14
|
+
this.generatedNames = new Set();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
generateName() {
|
|
18
|
+
let name;
|
|
19
|
+
do {
|
|
20
|
+
name = `${faker.color.human()} ${faker.name.jobType()}`;
|
|
21
|
+
name = `${name[0].toUpperCase()}${name.slice(1)}`;
|
|
22
|
+
} while (this.generatedNames.has(name));
|
|
23
|
+
this.generatedNames.add(name);
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
generate() {
|
|
28
|
+
const name = this.generateName();
|
|
29
|
+
return {
|
|
30
|
+
id: this.fastFakeObjectId(),
|
|
31
|
+
name: name,
|
|
32
|
+
slug: `${slugify(name)}`,
|
|
33
|
+
created_at: dateToDatabaseString(blogStartDate),
|
|
34
|
+
created_by: '1',
|
|
35
|
+
updated_at: dateToDatabaseString(blogStartDate),
|
|
36
|
+
updated_by: '1'
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = LabelsImporter;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const TableImporter = require('./TableImporter');
|
|
2
|
+
const {faker} = require('@faker-js/faker');
|
|
3
|
+
const {luck} = require('../utils/random');
|
|
4
|
+
const dateToDatabaseString = require('../utils/database-date');
|
|
5
|
+
|
|
6
|
+
class MembersClickEventsImporter extends TableImporter {
|
|
7
|
+
static table = 'members_click_events';
|
|
8
|
+
static dependencies = ['email_recipients', 'redirects', 'emails'];
|
|
9
|
+
|
|
10
|
+
constructor(knex, transaction) {
|
|
11
|
+
super(MembersClickEventsImporter.table, knex, transaction);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async import(quantity) {
|
|
15
|
+
const emailRecipients = await this.transaction.select('id', 'opened_at', 'email_id', 'member_id').from('email_recipients').whereNotNull('opened_at');
|
|
16
|
+
const redirects = await this.transaction.select('id', 'post_id').from('redirects');
|
|
17
|
+
const emails = await this.transaction.select('id', 'post_id').from('emails');
|
|
18
|
+
this.quantity = quantity ? quantity / emailRecipients.length : 2;
|
|
19
|
+
|
|
20
|
+
// Create maps for faster lookups (this does make a difference for large data generation)
|
|
21
|
+
this.emails = new Map();
|
|
22
|
+
for (const email of emails) {
|
|
23
|
+
this.emails.set(email.id, email);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.redirects = new Map();
|
|
27
|
+
for (const redirect of redirects) {
|
|
28
|
+
if (!this.redirects.has(redirect.post_id)) {
|
|
29
|
+
this.redirects.set(redirect.post_id, []);
|
|
30
|
+
}
|
|
31
|
+
this.redirects.get(redirect.post_id).push(redirect);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await this.importForEach(emailRecipients, this.quantity);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setReferencedModel(model) {
|
|
38
|
+
this.model = model;
|
|
39
|
+
this.amount = model.opened_at === null ? 0 : luck(40) ? faker.datatype.number({
|
|
40
|
+
min: 0,
|
|
41
|
+
max: this.quantity
|
|
42
|
+
}) : 0;
|
|
43
|
+
const email = this.emails.get(model.email_id);
|
|
44
|
+
this.redirectList = this.redirects.get(email.post_id) ?? [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
generate() {
|
|
48
|
+
if (this.amount <= 0 || this.redirectList.length === 0 || !this.model.opened_at) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.amount -= 1;
|
|
52
|
+
|
|
53
|
+
const openedAt = new Date(this.model.opened_at);
|
|
54
|
+
const laterOn = new Date(openedAt.getTime() + 1000 * 60 * 15);
|
|
55
|
+
const clickTime = faker.date.between(openedAt.getTime(), laterOn.getTime()); //added getTime here because it threw random errors
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
id: this.fastFakeObjectId(),
|
|
59
|
+
member_id: this.model.member_id,
|
|
60
|
+
redirect_id: this.redirectList[this.redirectList.length === 1 ? 0 : (faker.datatype.number({
|
|
61
|
+
min: 0,
|
|
62
|
+
max: this.redirectList.length - 1
|
|
63
|
+
}))].id,
|
|
64
|
+
created_at: dateToDatabaseString(clickTime)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = MembersClickEventsImporter;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const TableImporter = require('./TableImporter');
|
|
2
|
+
const {faker} = require('@faker-js/faker');
|
|
3
|
+
const {luck} = require('../utils/random');
|
|
4
|
+
const dateToDatabaseString = require('../utils/database-date');
|
|
5
|
+
|
|
6
|
+
class MembersCreatedEventsImporter extends TableImporter {
|
|
7
|
+
static table = 'members_created_events';
|
|
8
|
+
static dependencies = ['members', 'posts', 'mentions'];
|
|
9
|
+
|
|
10
|
+
constructor(knex, transaction) {
|
|
11
|
+
super(MembersCreatedEventsImporter.table, knex, transaction);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async import(quantity) {
|
|
15
|
+
const members = await this.transaction.select('id', 'created_at').from('members');
|
|
16
|
+
this.posts = await this.transaction.select('id', 'published_at', 'visibility', 'type', 'slug').from('posts').orderBy('published_at', 'desc');
|
|
17
|
+
this.incomingRecommendations = await this.transaction.select('id', 'source', 'created_at').from('mentions');
|
|
18
|
+
|
|
19
|
+
await this.importForEach(members, quantity ? quantity / members.length : 1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
generateSource() {
|
|
23
|
+
let source = 'member';
|
|
24
|
+
if (luck(10)) {
|
|
25
|
+
source = 'admin';
|
|
26
|
+
} else if (luck(5)) {
|
|
27
|
+
source = 'api';
|
|
28
|
+
} else if (luck(5)) { // eslint-disable-line no-dupe-else-if
|
|
29
|
+
source = 'import';
|
|
30
|
+
}
|
|
31
|
+
return source;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
generate() {
|
|
35
|
+
const source = this.generateSource();
|
|
36
|
+
|
|
37
|
+
// We need to add all properties here already otherwise CSV imports won't know all the columns
|
|
38
|
+
let attribution = {
|
|
39
|
+
attribution_id: null,
|
|
40
|
+
attribution_type: null,
|
|
41
|
+
attribution_url: null
|
|
42
|
+
};
|
|
43
|
+
let referrer = {
|
|
44
|
+
referrer_source: null,
|
|
45
|
+
referrer_url: null,
|
|
46
|
+
referrer_medium: null
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (source === 'member' && luck(30)) {
|
|
50
|
+
const post = this.posts.find(p => p.visibility === 'public' && new Date(p.published_at) < new Date(this.model.created_at));
|
|
51
|
+
if (post) {
|
|
52
|
+
attribution = {
|
|
53
|
+
attribution_id: post.id,
|
|
54
|
+
attribution_type: post.type,
|
|
55
|
+
attribution_url: post.slug
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (source === 'member' && luck(40)) {
|
|
61
|
+
if (luck(20)) {
|
|
62
|
+
// Ghost network
|
|
63
|
+
referrer = {
|
|
64
|
+
referrer_source: luck(20) ? 'Ghost.org' : 'Ghost Explore',
|
|
65
|
+
referrer_url: 'ghost.org',
|
|
66
|
+
referrer_medium: 'Ghost Network'
|
|
67
|
+
};
|
|
68
|
+
} else {
|
|
69
|
+
// Incoming recommendation
|
|
70
|
+
const incomingRecommendation = faker.helpers.arrayElement(this.incomingRecommendations);
|
|
71
|
+
|
|
72
|
+
const hostname = new URL(incomingRecommendation.source).hostname;
|
|
73
|
+
referrer = {
|
|
74
|
+
referrer_source: hostname,
|
|
75
|
+
referrer_url: hostname,
|
|
76
|
+
referrer_medium: faker.helpers.arrayElement([null, 'Email'])
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (source === 'import') {
|
|
82
|
+
referrer.referrer_source = 'Imported';
|
|
83
|
+
referrer.referrer_medium = 'Member Importer';
|
|
84
|
+
} else if (source === 'admin') {
|
|
85
|
+
referrer.referrer_source = 'Created manually';
|
|
86
|
+
referrer.referrer_medium = 'Ghost Admin';
|
|
87
|
+
} else if (source === 'api') {
|
|
88
|
+
referrer.referrer_source = 'Created via API';
|
|
89
|
+
referrer.referrer_medium = 'Admin API';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
id: this.fastFakeObjectId(),
|
|
94
|
+
created_at: dateToDatabaseString(this.model.created_at),
|
|
95
|
+
member_id: this.model.id,
|
|
96
|
+
source,
|
|
97
|
+
...attribution,
|
|
98
|
+
...referrer
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = MembersCreatedEventsImporter;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const TableImporter = require('./TableImporter');
|
|
2
|
+
const {faker} = require('@faker-js/faker');
|
|
3
|
+
const {luck} = require('../utils/random');
|
|
4
|
+
const dateToDatabaseString = require('../utils/database-date');
|
|
5
|
+
|
|
6
|
+
class MembersFeedbackImporter extends TableImporter {
|
|
7
|
+
static table = 'members_feedback';
|
|
8
|
+
static dependencies = ['emails', 'email_recipients'];
|
|
9
|
+
|
|
10
|
+
constructor(knex, transaction, {emails}) {
|
|
11
|
+
super(MembersFeedbackImporter.table, knex, transaction);
|
|
12
|
+
this.emails = emails;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async import(quantity) {
|
|
16
|
+
const emailRecipients = await this.transaction.select('id', 'opened_at', 'email_id', 'member_id').from('email_recipients');
|
|
17
|
+
this.emails = await this.transaction.select('id', 'post_id').from('emails');
|
|
18
|
+
|
|
19
|
+
await this.importForEach(emailRecipients, quantity ? quantity / emailRecipients.length : 1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
generate() {
|
|
23
|
+
// ~10% of people who opened the email will leave feedback
|
|
24
|
+
if (!this.model.opened_at || luck(90)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const openedAt = new Date(this.model.opened_at);
|
|
29
|
+
const laterOn = new Date(this.model.opened_at);
|
|
30
|
+
laterOn.setMinutes(laterOn.getMinutes() + 60);
|
|
31
|
+
const feedbackTime = faker.date.between(openedAt, laterOn);
|
|
32
|
+
|
|
33
|
+
const postId = this.emails.find(email => email.id === this.model.email_id).post_id;
|
|
34
|
+
return {
|
|
35
|
+
id: this.fastFakeObjectId(),
|
|
36
|
+
score: luck(70) ? 1 : 0,
|
|
37
|
+
member_id: this.model.member_id,
|
|
38
|
+
post_id: postId,
|
|
39
|
+
created_at: dateToDatabaseString(feedbackTime),
|
|
40
|
+
updated_at: dateToDatabaseString(feedbackTime)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = MembersFeedbackImporter;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const TableImporter = require('./TableImporter');
|
|
2
|
+
const {faker} = require('@faker-js/faker');
|
|
3
|
+
const {faker: americanFaker} = require('@faker-js/faker/locale/en_US');
|
|
4
|
+
const {blogStartDate: startTime} = require('../utils/blog-info');
|
|
5
|
+
const generateEvents = require('../utils/event-generator');
|
|
6
|
+
const {luck} = require('../utils/random');
|
|
7
|
+
const dateToDatabaseString = require('../utils/database-date');
|
|
8
|
+
const debug = require('@tryghost/debug')('MembersImporter');
|
|
9
|
+
|
|
10
|
+
class MembersImporter extends TableImporter {
|
|
11
|
+
static table = 'members';
|
|
12
|
+
static dependencies = [];
|
|
13
|
+
defaultQuantity = faker.datatype.number({
|
|
14
|
+
min: 7000,
|
|
15
|
+
max: 8000
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
constructor(knex, transaction) {
|
|
19
|
+
super(MembersImporter.table, knex, transaction);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async import(quantity = this.defaultQuantity) {
|
|
23
|
+
const generateNow = Date.now();
|
|
24
|
+
|
|
25
|
+
this.timestamps = generateEvents({
|
|
26
|
+
shape: 'ease-in',
|
|
27
|
+
trend: 'positive',
|
|
28
|
+
total: quantity,
|
|
29
|
+
startTime,
|
|
30
|
+
endTime: new Date()
|
|
31
|
+
}).sort();
|
|
32
|
+
debug(`${this.name} generated ${this.timestamps.length} timestamps in ${Date.now() - generateNow}ms`);
|
|
33
|
+
|
|
34
|
+
await super.import(quantity);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Add open rate data to members table
|
|
39
|
+
*/
|
|
40
|
+
async finalise() {
|
|
41
|
+
const emailRecipients = await this.transaction.select('id', 'member_id', 'opened_at').from('email_recipients');
|
|
42
|
+
|
|
43
|
+
const memberData = {};
|
|
44
|
+
for (const emailRecipient of emailRecipients) {
|
|
45
|
+
if (!(emailRecipient.member_id in memberData)) {
|
|
46
|
+
memberData[emailRecipient.member_id] = {
|
|
47
|
+
emailCount: 1,
|
|
48
|
+
openedCount: emailRecipient.opened_at ? 1 : 0
|
|
49
|
+
};
|
|
50
|
+
} else {
|
|
51
|
+
memberData[emailRecipient.member_id].emailCount += 1;
|
|
52
|
+
if (emailRecipient.opened_at) {
|
|
53
|
+
memberData[emailRecipient.member_id].openedCount += 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const [memberId, emailInfo] of Object.entries(memberData)) {
|
|
59
|
+
const openRate = Math.round(100 * (emailInfo.openedCount / emailInfo.emailCount));
|
|
60
|
+
await this.transaction('members').update({
|
|
61
|
+
email_count: emailInfo.emailCount,
|
|
62
|
+
email_opened_count: emailInfo.openedCount,
|
|
63
|
+
email_open_rate: emailInfo.emailCount >= 5 ? openRate : null
|
|
64
|
+
}).where({id: memberId});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
generate() {
|
|
69
|
+
const id = this.fastFakeObjectId();
|
|
70
|
+
// Use name from American locale to reflect an English-speaking audience
|
|
71
|
+
const name = `${americanFaker.name.firstName()} ${americanFaker.name.lastName()}`;
|
|
72
|
+
const timestamp = this.timestamps.pop();
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
id,
|
|
76
|
+
uuid: faker.datatype.uuid(),
|
|
77
|
+
transient_id: faker.datatype.uuid(),
|
|
78
|
+
email: `${name.replace(' ', '.').replace(/[^a-zA-Z0-9]/g, '').toLowerCase()}${faker.datatype.number({min: 0, max: 999999})}@example.com`,
|
|
79
|
+
status: luck(5) ? 'comped' : luck(15) ? 'paid' : 'free',
|
|
80
|
+
name: name,
|
|
81
|
+
expertise: luck(30) ? faker.name.jobTitle() : undefined,
|
|
82
|
+
geolocation: JSON.stringify({
|
|
83
|
+
organization_name: faker.company.name(),
|
|
84
|
+
region: faker.address.state(),
|
|
85
|
+
accuracy: 50,
|
|
86
|
+
asn: parseInt(faker.random.numeric(4)),
|
|
87
|
+
organization: `${faker.random.alpha({count: 2, casing: 'upper'})}${faker.random.numeric(4)} ${faker.company.name()}`,
|
|
88
|
+
timezone: faker.address.timeZone(),
|
|
89
|
+
longitude: faker.address.longitude(),
|
|
90
|
+
country_code3: faker.address.countryCode('alpha-3'),
|
|
91
|
+
area_code: '0',
|
|
92
|
+
ip: faker.internet.ipv4(),
|
|
93
|
+
city: faker.address.cityName(),
|
|
94
|
+
country: faker.address.country(),
|
|
95
|
+
continent_code: 'EU',
|
|
96
|
+
country_code: faker.address.countryCode('alpha-2'),
|
|
97
|
+
latitude: faker.address.latitude()
|
|
98
|
+
}),
|
|
99
|
+
email_count: 0, // Depends on number of emails sent since created_at, the newsletter they're a part of and subscription status
|
|
100
|
+
email_opened_count: 0,
|
|
101
|
+
email_open_rate: null,
|
|
102
|
+
// 40% of users logged in within a week, 60% sometime since registering
|
|
103
|
+
last_seen_at: luck(40) ? dateToDatabaseString(faker.date.recent(7)) : dateToDatabaseString(faker.date.between(timestamp, new Date())),
|
|
104
|
+
created_at: dateToDatabaseString(timestamp),
|
|
105
|
+
created_by: id,
|
|
106
|
+
updated_at: dateToDatabaseString(timestamp)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = MembersImporter;
|