ghost 6.54.0 → 6.54.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/ghost-storage-base-3.0.0.tgz +0 -0
- package/components/tryghost-adapter-base-cache-0.2.0.tgz +0 -0
- package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
- package/components/tryghost-adapter-base-sso-0.1.1.tgz +0 -0
- package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
- package/core/built/admin/assets/Growth-BTP1LthE.js +3 -0
- package/core/built/admin/assets/Newsletters-CSEX0myc.js +1 -0
- package/core/built/admin/assets/Overview-BjACEsJF.js +1 -0
- package/core/built/admin/assets/Web-CxxxjCcv.js +2 -0
- package/core/built/admin/assets/{account-migration-DLvVA5yq.js → account-migration-BklF2I_I.js} +1 -1
- package/core/built/admin/assets/activitypub/{account-migration-DHbZcxGM.js → account-migration-CNovr6W-.js} +4 -4
- package/core/built/admin/assets/activitypub/activitypub.js +2 -2
- package/core/built/admin/assets/activitypub/{bluesky-sharing-zfxUey_L.js → bluesky-sharing-Ceslb1_M.js} +4 -4
- package/core/built/admin/assets/activitypub/{dist-BqkDUQxJ2.js → dist-ByFmDSNP2.js} +64 -63
- package/core/built/admin/assets/activitypub/{dist-Ba_QxHHl.js → dist-DHAjBcV3.js} +2 -2
- package/core/built/admin/assets/activitypub/{domain-CmwSZLHK.js → domain-C1qQvJ8C.js} +5 -5
- package/core/built/admin/assets/activitypub/{edit-profile-CXdwNmVG.js → edit-profile-BcH-jEfM.js} +3 -3
- package/core/built/admin/assets/activitypub/{explore-DfbjzGM1.js → explore-AxwBh9dI.js} +3 -3
- package/core/built/admin/assets/activitypub/{feed-DSEBJOqf.js → feed-BaOlbh7a.js} +3 -3
- package/core/built/admin/assets/activitypub/{field-CY7XVcx6.js → field-B7mYYQpN.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-DtQ0kytT.js → inbox-vXez-5uJ.js} +4 -4
- package/core/built/admin/assets/activitypub/{label-Dw96GQ7u.js → label-Chv5T8bw.js} +2 -2
- package/core/built/admin/assets/activitypub/layout-CziktJXo.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-Dg_-I-8C.js → moderation-DtSM2i1I.js} +4 -4
- package/core/built/admin/assets/activitypub/{note-Cy1-uKMY.js → note-Dckp4S-2.js} +3 -3
- package/core/built/admin/assets/activitypub/{notifications-B_fvoLIM.js → notifications-CG5T22vg.js} +3 -3
- package/core/built/admin/assets/activitypub/{preferences-ByJseZp6.js → preferences-D_799jdl.js} +6 -6
- package/core/built/admin/assets/activitypub/{profile-BFtbnIjz.js → profile-D2JfXBQl.js} +6 -6
- package/core/built/admin/assets/activitypub/{routes-Dh3xXfce.js → routes-CCHe2dQp.js} +28 -25
- package/core/built/admin/assets/activitypub/{settings-DTgqJPxM.js → settings-kx-9Enkz.js} +3 -3
- package/core/built/admin/assets/activitypub/{site-CdL9ZLqm.js → site-x4_IjVJ0.js} +2 -2
- package/core/built/admin/assets/activitypub/{tabs-ogqwoBlb.js → tabs-CyPRQQ1N.js} +3 -3
- package/core/built/admin/assets/activitypub/{use-reply-chain-data-CxonteFD.js → use-reply-chain-data-BAB6fNGt.js} +2 -2
- package/core/built/admin/assets/{audience-BKRdsFmy.js → audience-X_XsZivI.js} +1 -1
- package/core/built/admin/assets/{automated-emails-Cn87L5Md.js → automated-emails-of1MvVsv.js} +1 -1
- package/core/built/admin/assets/{automations-BP_aHamJ.js → automations-DRFjW5fw.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-Bh36XPh4.js → bluesky-sharing-BbbjWdzs.js} +1 -1
- package/core/built/admin/assets/{breadcrumb-D34YWnaw.js → breadcrumb-B-M9Ip_2.js} +1 -1
- package/core/built/admin/assets/{chart-helpers-DbklaJw9.js → chart-helpers-xiSsW8r0.js} +1 -1
- package/core/built/admin/assets/checkbox-EhJKHFdZ.js +1 -0
- package/core/built/admin/assets/{chunk.962.b73442b07be930bb5d85.js → chunk.211.6f7328d63c41536451b3.js} +737 -559
- package/core/built/admin/assets/{chunk.962.b73442b07be930bb5d85.js.LICENSE.txt → chunk.211.6f7328d63c41536451b3.js.LICENSE.txt} +2 -0
- package/core/built/admin/assets/{chunk.589.096f4d0bb775e5229aa9.js → chunk.361.3cdb584b973959e52ccc.js} +3 -3
- package/core/built/admin/assets/chunk.524.8365cd9280405c0f6216.js +25 -0
- package/core/built/admin/assets/chunk.582.61d078937e6102769f2c.js +27 -0
- package/core/built/admin/assets/code-editor-view-CZtJFGhR.js +1 -0
- package/core/built/admin/assets/comments-CvyXxOyN.js +1 -0
- package/core/built/admin/assets/{date-range-select-B5vSsrpx.js → date-range-select-CTlyo8zW.js} +1 -1
- package/core/built/admin/assets/{disabled-sources-indicator-NkOIoMue.js → disabled-sources-indicator-qs2GfLvl.js} +1 -1
- package/core/built/admin/assets/dist-2Tnz2Yfq.js +1 -0
- package/core/built/admin/assets/dist-CjY-f5Z1.js +1 -0
- package/core/built/admin/assets/dist-DIC8yz5C.js +1 -0
- package/core/built/admin/assets/{domain-B7_n5nfx.js → domain-CPNXKVOV.js} +1 -1
- package/core/built/admin/assets/dropzone-BmCXnw6J.js +5 -0
- package/core/built/admin/assets/{edit-profile-rvDGrS8J.js → edit-profile-SGrJve5M.js} +1 -1
- package/core/built/admin/assets/editor-BOG2YPUm.js +8 -0
- package/core/built/admin/assets/{explore-BZAZOg5X.js → explore-CUYLv9G9.js} +1 -1
- package/core/built/admin/assets/{feed-CFV8xDw9.js → feed-CPdJMttL.js} +1 -1
- package/core/built/admin/assets/{field-BW-M2Zwp.js → field-D0XISCK2.js} +1 -1
- package/core/built/admin/assets/{filter-sources-Fx8wiYYw.js → filter-sources-Bieps75g.js} +1 -1
- package/core/built/admin/assets/{filters-CGX_pf2N.js → filters-CuB-A7ma.js} +1 -1
- package/core/built/admin/assets/filters-bXkkWro8.js +1 -0
- package/core/built/admin/assets/{ghost-b1dce91ad3232cc259ee5fbbf9526243.js → ghost-05f9a7182ed56903dba05878a5b8422b.js} +290 -289
- package/core/built/admin/assets/{gift-link-modal-Bk8qpnMw.js → gift-link-modal-su8MsRdg.js} +1 -1
- package/core/built/admin/assets/{growth-Ck2s3JjR.js → growth-Ba6S5FQX.js} +1 -1
- package/core/built/admin/assets/{inbox-BRp1_aaC.js → inbox-Xx1CdnfO.js} +8 -8
- package/core/built/admin/assets/index-D8PbzuT9.css +1 -0
- package/core/built/admin/assets/index-ukW_0Uie.js +67 -0
- package/core/built/admin/assets/input-group-CGOXcy4l.js +1 -0
- package/core/built/admin/assets/input-surface-BpAjZXY5.js +1 -0
- package/core/built/admin/assets/{koenig-lexical-CM7UFNdL.js → koenig-lexical-B61IALf5.js} +1 -1
- package/core/built/admin/assets/{kpi-card-CIGyijjx.js → kpi-card-BlZaG1Lo.js} +1 -1
- package/core/built/admin/assets/{kpi-card-BrGH6FsT.js → kpi-card-DCdlyaSS.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-_HRRsklE.js → kpi-helpers-DpAuS1Nb.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-BSvh9eoq.js → kpi-tabs-Dme44bQS.js} +1 -1
- package/core/built/admin/assets/{label-BOR9XdKl.js → label-dSs5aepE.js} +1 -1
- package/core/built/admin/assets/{label-picker-BdNAWuUX.js → label-picker-Uq91atex.js} +1 -1
- package/core/built/admin/assets/layout-WUcz6tu1.js +1 -0
- package/core/built/admin/assets/{member-custom-fields-DvFyamLu.js → member-custom-fields-NCzrMuba.js} +1 -1
- package/core/built/admin/assets/member-detail-DPWDtYnN.js +1 -0
- package/core/built/admin/assets/{members-DsPxYAVm.js → members-B9rO7qr4.js} +2 -2
- package/core/built/admin/assets/modals-CxRaan1J.js +31 -0
- package/core/built/admin/assets/{moderation-Dlp7M5sa.js → moderation-D8zon60E.js} +1 -1
- package/core/built/admin/assets/{multi-select-combobox-CH1o8XXQ.js → multi-select-combobox-1wYm3nle.js} +1 -1
- package/core/built/admin/assets/newsletter-DbYMRxRS.js +1 -0
- package/core/built/admin/assets/{note-BrAZ7Z0o.js → note-PiWPnnrh.js} +1 -1
- package/core/built/admin/assets/{notifications-BnbHLOPC.js → notifications-DEgqqB9G.js} +1 -1
- package/core/built/admin/assets/{onboarding-route-eU146kuA.js → onboarding-route-BzF6HeVn.js} +1 -1
- package/core/built/admin/assets/{overview-BpbaK-xu.js → overview-DixLeIqY.js} +1 -1
- package/core/built/admin/assets/{page-header-C0jVxtS8.js → page-header-op0eN36I.js} +1 -1
- package/core/built/admin/assets/{post-analytics-Iw7dk1mT.js → post-analytics-WwRDEnrV.js} +1 -1
- package/core/built/admin/assets/post-analytics-header-DsMoLx0R.js +1 -0
- package/core/built/admin/assets/{post-analytics-provider-BDqcfBor.js → post-analytics-provider-BJCPLMIb.js} +1 -1
- package/core/built/admin/assets/{post-share-modal-DBfKl2GZ.js → post-share-modal-Dx-MEjhI.js} +1 -1
- package/core/built/admin/assets/preferences-D7-j6A4C.js +5 -0
- package/core/built/admin/assets/profile-Dl69QPp0.js +1 -0
- package/core/built/admin/assets/select-C1qDZnW4.js +1 -0
- package/core/built/admin/assets/settings-BbmC_Iqq.js +4 -0
- package/core/built/admin/assets/{settings-Bz3RZRmE.js → settings-H6HxFgfI.js} +1 -1
- package/core/built/admin/assets/shade-app-B3heOAfz-CGXQwWeO.js +3 -0
- package/core/built/admin/assets/{share-modal-CkdTTMJd.js → share-modal-BJCadJyp.js} +1 -1
- package/core/built/admin/assets/social-urls-BPL1nADp.js +8 -0
- package/core/built/admin/assets/{sort-button-DZp9Y2r1.js → sort-button-DyYlfYp7.js} +1 -1
- package/core/built/admin/assets/{source-icon-CHr9qJ2Y.js → source-icon-_ubRDbg_.js} +1 -1
- package/core/built/admin/assets/{stats-DGoXZxWE.js → stats-BQZMGMvn.js} +1 -1
- package/core/built/admin/assets/{stats-view-D2-1L0ms.js → stats-view-Doei-qCX.js} +1 -1
- package/core/built/admin/assets/{step-1-RdahbwyG.js → step-1-IKiLBPoC.js} +1 -1
- package/core/built/admin/assets/{step-2-D83PeRwd.js → step-2-jJ41PsDN.js} +1 -1
- package/core/built/admin/assets/{step-3-p_Op4wv4.js → step-3-CaqFAEqf.js} +2 -2
- package/core/built/admin/assets/switch-t-WyENU2.js +1 -0
- package/core/built/admin/assets/{table-B8LgTQJZ.js → table-DxUL78jU.js} +1 -1
- package/core/built/admin/assets/{tabs-D3ZOYkM7.js → tabs-BQcJQ1ke.js} +1 -1
- package/core/built/admin/assets/{tags-B4l891z2.js → tags-CNujGwkT.js} +1 -1
- package/core/built/admin/assets/textarea-Bjccvh_E.js +1 -0
- package/core/built/admin/assets/{toggle-group-DNXDkAwf.js → toggle-group-BeEpQ69p.js} +1 -1
- package/core/built/admin/assets/{topic-filter-DZGjEcz3.js → topic-filter-Bb5MOD-1.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-CaT8RirX.js → use-analytics-data-Cds6XDjg.js} +1 -1
- package/core/built/admin/assets/use-filter-params-DjXkhO54.js +1 -0
- package/core/built/admin/assets/{use-growth-stats-9Db2CuJe.js → use-growth-stats-D8uf98-G.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-BV54YcQS.js → use-post-referrers-CRtgyuXB.js} +1 -1
- package/core/built/admin/assets/{use-reply-chain-data-DuYZGDan.js → use-reply-chain-data-DbKYX50v.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-8fTua9Nt.js → use-simple-pagination-DC01kpo0.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-query-Dz156HvZ.js → use-tinybird-query-C7bQRJWw.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-C9RUdL_n.js → use-tinybird-token-Az4tam7_.js} +1 -1
- package/core/built/admin/assets/{virtual-list-BCvP0YF-.js → virtual-list-Dhuh9fGx.js} +1 -1
- package/core/built/admin/assets/web-LtHNiNBV.js +1 -0
- package/core/built/admin/index.html +11 -7
- package/core/frontend/services/proxy.js +11 -1
- package/core/server/adapters/storage/LocalFilesStorage.js +13 -11
- package/core/server/adapters/storage/LocalFilesStorage.ts +24 -0
- package/core/server/adapters/storage/LocalImagesStorage.js +14 -12
- package/core/server/adapters/storage/LocalImagesStorage.ts +24 -0
- package/core/server/adapters/storage/LocalMediaStorage.js +13 -11
- package/core/server/adapters/storage/LocalMediaStorage.ts +24 -0
- package/core/server/adapters/storage/LocalStorageBase.js +107 -167
- package/core/server/adapters/storage/LocalStorageBase.ts +342 -0
- package/core/server/adapters/storage/utils.js +36 -50
- package/core/server/adapters/storage/utils.ts +60 -0
- package/core/server/api/endpoints/db.js +6 -0
- package/core/server/api/endpoints/members.js +25 -28
- package/core/server/api/endpoints/pages.js +6 -1
- package/core/server/api/endpoints/posts.js +11 -2
- package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +4 -1
- package/core/server/api/endpoints/utils/serializers/output/members.js +18 -87
- package/core/server/data/exporter/export-filename.js +2 -1
- package/core/server/data/importer/importers/data/revue-subscriber-importer.js +6 -10
- package/core/server/data/importer/importers/json-to-html.js +24 -4
- package/core/server/lib/image/image-size.js +3 -17
- package/core/server/lib/image/image-utils.js +15 -1
- package/core/server/lib/request-external.js +1 -1
- package/core/server/models/user.js +46 -70
- package/core/server/services/auth/session/session-service.js +28 -1
- package/core/server/services/automations/automations-api.js +6 -6
- package/core/server/services/automations/automations-api.ts +6 -6
- package/core/server/services/automations/automations-repository.ts +1 -0
- package/core/server/services/automations/database-automations-repository.js +7 -2
- package/core/server/services/automations/database-automations-repository.ts +8 -2
- package/core/server/services/comments/comments-service-emails.js +4 -1
- package/core/server/services/members/{importer → import-export}/config.js +4 -9
- package/core/server/services/members/{importer → import-export}/config.ts +4 -9
- package/core/server/services/members/{csv → import-export/csv}/index.js +3 -3
- package/core/server/services/members/import-export/csv/index.ts +2 -0
- package/core/server/services/members/import-export/csv/parse.js +64 -0
- package/core/server/services/members/import-export/csv/parse.ts +69 -0
- package/core/server/services/members/import-export/csv/serialize.js +13 -0
- package/core/server/services/members/import-export/csv/serialize.ts +18 -0
- package/core/server/services/members/import-export/export/exporter.js +202 -0
- package/core/server/services/members/import-export/export/exporter.ts +325 -0
- package/core/server/services/members/import-export/import/completion-email.js +65 -0
- package/core/server/services/members/import-export/import/completion-email.ts +119 -0
- package/core/server/services/members/import-export/import/importer.js +276 -0
- package/core/server/services/members/import-export/import/importer.ts +430 -0
- package/core/server/services/members/import-export/import/reader.js +32 -0
- package/core/server/services/members/import-export/import/reader.ts +33 -0
- package/core/server/services/members/import-export/import/row.js +33 -0
- package/core/server/services/members/import-export/import/row.ts +52 -0
- package/core/server/services/members/import-export/import/spool.js +30 -0
- package/core/server/services/members/import-export/import/spool.ts +38 -0
- package/core/server/services/members/import-export/index.js +90 -0
- package/core/server/services/members/import-export/index.ts +118 -0
- package/core/server/services/members/members-api/controllers/router-controller.js +7 -0
- package/core/server/services/members/service.js +17 -19
- package/core/server/services/posts/posts-exporter.js +6 -4
- package/core/server/services/posts/posts-service.js +2 -2
- package/core/server/services/themes/storage.js +9 -0
- package/core/server/services/themes/theme-storage.js +1 -1
- package/core/shared/config/defaults.json +2 -1
- package/core/shared/express.js +6 -1
- package/core/shared/labs.js +2 -1
- package/package.json +9 -9
- package/pnpm-lock.yaml +17 -17
- package/pnpm-workspace.yaml +7 -7
- package/components/tryghost-adapter-base-scheduling-0.1.1.tgz +0 -0
- package/core/built/admin/assets/Growth-CSaHoSVD.js +0 -3
- package/core/built/admin/assets/Newsletters-C5cs6FGo.js +0 -1
- package/core/built/admin/assets/Overview-j4NaeUNm.js +0 -1
- package/core/built/admin/assets/Web-_fD935OX.js +0 -2
- package/core/built/admin/assets/activitypub/layout-IdVq3lRy.js +0 -1
- package/core/built/admin/assets/checkbox-DA4-1UiI.js +0 -1
- package/core/built/admin/assets/chunk.524.45e0875f41b71e46e849.js +0 -25
- package/core/built/admin/assets/chunk.582.51defd4b009f0de8d4d5.js +0 -27
- package/core/built/admin/assets/code-editor-view-BKBrkffZ.js +0 -1
- package/core/built/admin/assets/comments-CzVNIT8b.js +0 -1
- package/core/built/admin/assets/dist-CT46_f_J.js +0 -1
- package/core/built/admin/assets/dropzone-DcuJEEUf.js +0 -5
- package/core/built/admin/assets/editor-BCiIyFjm.js +0 -8
- package/core/built/admin/assets/filters-DtcXMZDU.js +0 -1
- package/core/built/admin/assets/index-BBf502Wg.js +0 -69
- package/core/built/admin/assets/index-BCCZKxP2.css +0 -1
- package/core/built/admin/assets/input-group-DjuYnNCd.js +0 -1
- package/core/built/admin/assets/layout-Bw4GnxXp.js +0 -1
- package/core/built/admin/assets/member-detail-Cg2vTqvJ.js +0 -1
- package/core/built/admin/assets/modals-Bf_fyvHZ.js +0 -31
- package/core/built/admin/assets/newsletter-CQ5CAYUI.js +0 -1
- package/core/built/admin/assets/post-analytics-header-D9pJ4iNm.js +0 -1
- package/core/built/admin/assets/preferences-D4EANGoc.js +0 -5
- package/core/built/admin/assets/profile-dJ44Bn_W.js +0 -1
- package/core/built/admin/assets/select--2uVN9_-.js +0 -1
- package/core/built/admin/assets/settings-SD9bxIpt.js +0 -4
- package/core/built/admin/assets/social-urls-BvFZwYcE.js +0 -8
- package/core/built/admin/assets/switch-qp518gFQ.js +0 -1
- package/core/built/admin/assets/textarea-CTlb3Uu6.js +0 -1
- package/core/built/admin/assets/use-filter-params-DeQ8sX4N.js +0 -1
- package/core/built/admin/assets/web-DTZnnUGr.js +0 -1
- package/core/server/services/mail/templates/newsletter.html +0 -876
- package/core/server/services/members/csv/index.ts +0 -3
- package/core/server/services/members/csv/parse.js +0 -94
- package/core/server/services/members/csv/parse.ts +0 -111
- package/core/server/services/members/csv/types.js +0 -2
- package/core/server/services/members/csv/types.ts +0 -39
- package/core/server/services/members/csv/unparse.js +0 -69
- package/core/server/services/members/csv/unparse.ts +0 -79
- package/core/server/services/members/exporter/query.js +0 -233
- package/core/server/services/members/importer/index.js +0 -30
- package/core/server/services/members/importer/labels.js +0 -125
- package/core/server/services/members/importer/members-csv-importer.js +0 -520
- /package/core/server/services/members/{importer → import-export/import}/email-template.js +0 -0
- /package/core/server/services/members/{importer/members-csv-importer-stripe-utils.js → import-export/import/stripe-utils.js} +0 -0
|
@@ -65,6 +65,7 @@ interface ActionRow {
|
|
|
65
65
|
|
|
66
66
|
type ActionStatsRow = {
|
|
67
67
|
action_id: string;
|
|
68
|
+
email_clicked_count: number | null;
|
|
68
69
|
email_sent_count: number | null;
|
|
69
70
|
email_opened_count: number | null;
|
|
70
71
|
};
|
|
@@ -1264,6 +1265,7 @@ async function loadActionStats(
|
|
|
1264
1265
|
const rows: ActionStatsRow[] = await trx('automation_action_revisions')
|
|
1265
1266
|
.select('action_id')
|
|
1266
1267
|
.sum({
|
|
1268
|
+
email_clicked_count: 'email_clicked_count',
|
|
1267
1269
|
email_sent_count: 'email_sent_count',
|
|
1268
1270
|
email_opened_count: 'email_opened_count'
|
|
1269
1271
|
})
|
|
@@ -1319,6 +1321,7 @@ function buildActionPayload(row: ActionRow, stats: AutomationEmailStats | null):
|
|
|
1319
1321
|
}
|
|
1320
1322
|
|
|
1321
1323
|
const EMPTY_EMAIL_STATS: AutomationEmailStats = {
|
|
1324
|
+
email_clicked_count: 0,
|
|
1322
1325
|
email_sent_count: 0,
|
|
1323
1326
|
email_opened_count: 0,
|
|
1324
1327
|
opened_rate: null,
|
|
@@ -1326,16 +1329,19 @@ const EMPTY_EMAIL_STATS: AutomationEmailStats = {
|
|
|
1326
1329
|
};
|
|
1327
1330
|
|
|
1328
1331
|
function buildEmailStats(row: ActionStatsRow): AutomationEmailStats {
|
|
1332
|
+
const emailClickedCount = row.email_clicked_count ?? 0;
|
|
1329
1333
|
const emailSentCount = row.email_sent_count ?? 0;
|
|
1330
1334
|
const emailOpenedCount = row.email_opened_count ?? 0;
|
|
1331
1335
|
return {
|
|
1336
|
+
email_clicked_count: emailClickedCount,
|
|
1332
1337
|
email_sent_count: emailSentCount,
|
|
1333
1338
|
email_opened_count: emailOpenedCount,
|
|
1334
1339
|
opened_rate: emailSentCount
|
|
1335
1340
|
? Math.round(emailOpenedCount / emailSentCount * 100)
|
|
1336
1341
|
: null,
|
|
1337
|
-
|
|
1338
|
-
|
|
1342
|
+
clicked_rate: emailSentCount
|
|
1343
|
+
? Math.round(emailClickedCount / emailSentCount * 100)
|
|
1344
|
+
: null
|
|
1339
1345
|
};
|
|
1340
1346
|
}
|
|
1341
1347
|
|
|
@@ -26,7 +26,10 @@ class CommentsServiceEmails {
|
|
|
26
26
|
*/
|
|
27
27
|
getPostUrl(post, commentId) {
|
|
28
28
|
const postData = toPlain(post);
|
|
29
|
-
|
|
29
|
+
// Comments live on pages as well as posts; the URL service routes by
|
|
30
|
+
// the passed type, so a page typed 'posts' resolves to /404/.
|
|
31
|
+
const routerType = postData.type === 'page' ? 'pages' : 'posts';
|
|
32
|
+
const baseUrl = this.urlService.facade.getUrlForResource({...postData, type: routerType}, {absolute: true});
|
|
30
33
|
return `${baseUrl}#ghost-comments-${commentId}`;
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -2,15 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resolveInlineThreshold = resolveInlineThreshold;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* higher layer — an operator's config file, an environment variable, argv — can
|
|
10
|
-
* replace it with anything at all. Turning that into something usable is a
|
|
11
|
-
* config concern, so it happens here, at the boundary. The importer is handed an
|
|
12
|
-
* answer and never learns where it came from.
|
|
13
|
-
*/
|
|
5
|
+
// Config arrives untyped: defaults.json ships a value, and any higher layer -- an
|
|
6
|
+
// operator's config file, an environment variable, argv -- can replace it with anything.
|
|
7
|
+
// Validating it into a usable number is a config concern, so it happens here at the
|
|
8
|
+
// boundary; the importer is handed a resolved value.
|
|
14
9
|
// The row count at or below which an import is performed while the request is
|
|
15
10
|
// still open. Zero is allowed and means every import is deferred. A numeric
|
|
16
11
|
// string is accepted because a value supplied through an environment variable
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import {z} from 'zod';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* higher layer — an operator's config file, an environment variable, argv — can
|
|
8
|
-
* replace it with anything at all. Turning that into something usable is a
|
|
9
|
-
* config concern, so it happens here, at the boundary. The importer is handed an
|
|
10
|
-
* answer and never learns where it came from.
|
|
11
|
-
*/
|
|
3
|
+
// Config arrives untyped: defaults.json ships a value, and any higher layer -- an
|
|
4
|
+
// operator's config file, an environment variable, argv -- can replace it with anything.
|
|
5
|
+
// Validating it into a usable number is a config concern, so it happens here at the
|
|
6
|
+
// boundary; the importer is handed a resolved value.
|
|
12
7
|
|
|
13
8
|
// The row count at or below which an import is performed while the request is
|
|
14
9
|
// still open. Zero is allowed and means every import is deferred. A numeric
|
|
@@ -3,8 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.serialize = exports.parse = void 0;
|
|
7
7
|
var parse_1 = require("./parse");
|
|
8
8
|
Object.defineProperty(exports, "parse", { enumerable: true, get: function () { return __importDefault(parse_1).default; } });
|
|
9
|
-
var
|
|
10
|
-
Object.defineProperty(exports, "
|
|
9
|
+
var serialize_1 = require("./serialize");
|
|
10
|
+
Object.defineProperty(exports, "serialize", { enumerable: true, get: function () { return __importDefault(serialize_1).default; } });
|
|
@@ -0,0 +1,64 @@
|
|
|
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.default = parse;
|
|
7
|
+
const node_stream_1 = require("node:stream");
|
|
8
|
+
const papaparse_1 = __importDefault(require("papaparse"));
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
// A column named after an Object.prototype member (toString, __proto__, ...) is unsafe
|
|
11
|
+
// as a key: writing it would shadow a method or reach the prototype.
|
|
12
|
+
function isSafeColumnName(name) {
|
|
13
|
+
return !(name in Object.prototype);
|
|
14
|
+
}
|
|
15
|
+
// headerMapping renames the CSV's headers to the columns they emit under; unmapped
|
|
16
|
+
// columns carry through under their own name.
|
|
17
|
+
function parse(path, headerMapping) {
|
|
18
|
+
return new Promise(function (resolve, reject) {
|
|
19
|
+
const csvFileStream = fs_extra_1.default.createReadStream(path);
|
|
20
|
+
const csvParserStream = papaparse_1.default.parse(papaparse_1.default.NODE_STREAM_INPUT, {
|
|
21
|
+
header: true
|
|
22
|
+
});
|
|
23
|
+
const rows = [];
|
|
24
|
+
const parsedCSVStream = (0, node_stream_1.pipeline)(csvFileStream, csvParserStream, (err) => {
|
|
25
|
+
if (err) {
|
|
26
|
+
return reject(err);
|
|
27
|
+
}
|
|
28
|
+
resolve(rows);
|
|
29
|
+
});
|
|
30
|
+
parsedCSVStream.on('data', (parsedRow) => {
|
|
31
|
+
// a throw here escapes as an uncaught exception and leaves this
|
|
32
|
+
// promise forever unsettled, so it has to become a rejection
|
|
33
|
+
try {
|
|
34
|
+
const row = {};
|
|
35
|
+
for (const [header, value] of Object.entries(parsedRow)) {
|
|
36
|
+
// papaparse gathers the overflow from a row carrying more
|
|
37
|
+
// fields than there are headers under __parsed_extra, as an
|
|
38
|
+
// array rather than a cell any mapping can name
|
|
39
|
+
if (typeof value !== 'string') {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// hasOwn, not `in`: a prototype-named header would otherwise match an
|
|
43
|
+
// inherited method on the mapping and take a function as its mapped name.
|
|
44
|
+
if (headerMapping && Object.hasOwn(headerMapping, header)) {
|
|
45
|
+
row[headerMapping[header]] = value;
|
|
46
|
+
}
|
|
47
|
+
else if (isSafeColumnName(header)) {
|
|
48
|
+
// Carry any unmapped column through untouched, so the import is not
|
|
49
|
+
// constrained to a known vocabulary: a custom_fields.* column
|
|
50
|
+
// survives parsing even though nothing consumes it yet.
|
|
51
|
+
row[header] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!Object.keys(row).length) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
rows.push(row);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
reject(err);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {pipeline} from 'node:stream';
|
|
2
|
+
import papaparse from 'papaparse';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
|
|
5
|
+
// A parsed CSV row: raw string cells, keyed by (renamed) column. Parsing is mechanical;
|
|
6
|
+
// giving the columns meaning and coercing their values is the domain's job (the import
|
|
7
|
+
// schema).
|
|
8
|
+
export type Row = Record<string, string>;
|
|
9
|
+
|
|
10
|
+
// A column named after an Object.prototype member (toString, __proto__, ...) is unsafe
|
|
11
|
+
// as a key: writing it would shadow a method or reach the prototype.
|
|
12
|
+
function isSafeColumnName(name: string): boolean {
|
|
13
|
+
return !(name in Object.prototype);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// headerMapping renames the CSV's headers to the columns they emit under; unmapped
|
|
17
|
+
// columns carry through under their own name.
|
|
18
|
+
export default function parse(path: string, headerMapping?: Record<string, string>): Promise<Row[]> {
|
|
19
|
+
return new Promise(function (resolve, reject) {
|
|
20
|
+
const csvFileStream = fs.createReadStream(path);
|
|
21
|
+
const csvParserStream = papaparse.parse(papaparse.NODE_STREAM_INPUT, {
|
|
22
|
+
header: true
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const rows: Row[] = [];
|
|
26
|
+
const parsedCSVStream = pipeline(csvFileStream, csvParserStream, (err) => {
|
|
27
|
+
if (err) {
|
|
28
|
+
return reject(err);
|
|
29
|
+
}
|
|
30
|
+
resolve(rows);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
parsedCSVStream.on('data', (parsedRow: Record<string, unknown>) => {
|
|
34
|
+
// a throw here escapes as an uncaught exception and leaves this
|
|
35
|
+
// promise forever unsettled, so it has to become a rejection
|
|
36
|
+
try {
|
|
37
|
+
const row: Row = {};
|
|
38
|
+
|
|
39
|
+
for (const [header, value] of Object.entries(parsedRow)) {
|
|
40
|
+
// papaparse gathers the overflow from a row carrying more
|
|
41
|
+
// fields than there are headers under __parsed_extra, as an
|
|
42
|
+
// array rather than a cell any mapping can name
|
|
43
|
+
if (typeof value !== 'string') {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// hasOwn, not `in`: a prototype-named header would otherwise match an
|
|
48
|
+
// inherited method on the mapping and take a function as its mapped name.
|
|
49
|
+
if (headerMapping && Object.hasOwn(headerMapping, header)) {
|
|
50
|
+
row[headerMapping[header]] = value;
|
|
51
|
+
} else if (isSafeColumnName(header)) {
|
|
52
|
+
// Carry any unmapped column through untouched, so the import is not
|
|
53
|
+
// constrained to a known vocabulary: a custom_fields.* column
|
|
54
|
+
// survives parsing even though nothing consumes it yet.
|
|
55
|
+
row[header] = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!Object.keys(row).length) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
rows.push(row);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
reject(err);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
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.default = serialize;
|
|
7
|
+
const papaparse_1 = __importDefault(require("papaparse"));
|
|
8
|
+
// The papaparse boundary: given rows already shaped into cells, it writes CSV and
|
|
9
|
+
// nothing else. escapeFormulae is forced on -- papaparse defaults it off, but this CSV
|
|
10
|
+
// is opened by a person, so a leading =, +, -, @ or tab must be escaped.
|
|
11
|
+
function serialize(rows, { columns, header = true } = {}) {
|
|
12
|
+
return papaparse_1.default.unparse(rows, { columns, header, escapeFormulae: true });
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import papaparse from 'papaparse';
|
|
2
|
+
|
|
3
|
+
interface SerializeOptions {
|
|
4
|
+
// Omitted, papaparse derives the columns (and their order) from the first row's
|
|
5
|
+
// keys -- so a typed shaper's row can define them, with no separate list to drift.
|
|
6
|
+
columns?: string[];
|
|
7
|
+
header?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// The papaparse boundary: given rows already shaped into cells, it writes CSV and
|
|
11
|
+
// nothing else. escapeFormulae is forced on -- papaparse defaults it off, but this CSV
|
|
12
|
+
// is opened by a person, so a leading =, +, -, @ or tab must be escaped.
|
|
13
|
+
export default function serialize(
|
|
14
|
+
rows: Array<Record<string, unknown>>,
|
|
15
|
+
{columns, header = true}: SerializeOptions = {}
|
|
16
|
+
): string {
|
|
17
|
+
return papaparse.unparse(rows, {columns, header, escapeFormulae: true});
|
|
18
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
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.toExportCsvRow = toExportCsvRow;
|
|
7
|
+
const moment_1 = __importDefault(require("moment"));
|
|
8
|
+
const node_stream_1 = require("node:stream");
|
|
9
|
+
const logging = require('@tryghost/logging');
|
|
10
|
+
const errors = require('@tryghost/errors');
|
|
11
|
+
const { csvCellsForFields } = require('@tryghost/custom-field-types/csv');
|
|
12
|
+
// The columns the export reads off the members table, checked against MemberDbRow so the
|
|
13
|
+
// query cannot select a column the row type does not declare.
|
|
14
|
+
const MEMBER_COLUMNS = ['id', 'email', 'name', 'note', 'status', 'created_at'];
|
|
15
|
+
function namesToCsv(items) {
|
|
16
|
+
return items.map(item => item.name).join(',');
|
|
17
|
+
}
|
|
18
|
+
// Encode a flattened export row into its CSV cells, type-checked against MemberExportRow:
|
|
19
|
+
// a change to that row must be handled here or this stops compiling. deleted_at is always
|
|
20
|
+
// null -- the export reads live members -- and complimentary_plan is written from comped.
|
|
21
|
+
function toExportCsvRow(row) {
|
|
22
|
+
return {
|
|
23
|
+
id: row.id,
|
|
24
|
+
email: row.email,
|
|
25
|
+
name: row.name,
|
|
26
|
+
note: row.note,
|
|
27
|
+
subscribed_to_emails: row.subscribed === true ? 'true' : 'false',
|
|
28
|
+
complimentary_plan: row.comped === true ? 'true' : '',
|
|
29
|
+
stripe_customer_id: row.stripe_customer_id,
|
|
30
|
+
created_at: row.created_at,
|
|
31
|
+
deleted_at: null,
|
|
32
|
+
labels: namesToCsv(row.labels),
|
|
33
|
+
tiers: namesToCsv(row.tiers),
|
|
34
|
+
gift_id: row.gift_id || '',
|
|
35
|
+
...row.custom_field_cells
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const BATCH_SIZE = 1000;
|
|
39
|
+
// Streams the matching members as flat rows, each carrying its related data (tiers,
|
|
40
|
+
// labels, subscription, gift, Stripe customer, custom fields) for the output serializer
|
|
41
|
+
// to turn into CSV lines. The members aggregate and custom fields are reached only
|
|
42
|
+
// through the ports above, so nothing Bookshelf- or flag-shaped leaks into the export.
|
|
43
|
+
class MembersCSVExporter {
|
|
44
|
+
_knex;
|
|
45
|
+
_members;
|
|
46
|
+
_customFields;
|
|
47
|
+
constructor({ knex, members, customFields }) {
|
|
48
|
+
this._knex = knex;
|
|
49
|
+
this._members = members;
|
|
50
|
+
this._customFields = customFields;
|
|
51
|
+
}
|
|
52
|
+
async export(options = {}) {
|
|
53
|
+
const start = Date.now();
|
|
54
|
+
const hasFilter = options.limit !== 'all' || options.filter || options.search;
|
|
55
|
+
const ids = hasFilter ? await this._members.findFilteredIds(options) : null;
|
|
56
|
+
if (ids) {
|
|
57
|
+
logging.info(`[MembersExporter] Found ${ids.length} members matching filter criteria`);
|
|
58
|
+
}
|
|
59
|
+
const reference = await this.fetchReferenceData();
|
|
60
|
+
const membersQuery = this._knex('members').select(...MEMBER_COLUMNS);
|
|
61
|
+
if (ids) {
|
|
62
|
+
membersQuery.whereIn('id', ids);
|
|
63
|
+
}
|
|
64
|
+
logging.info('[MembersExporter] Starting streaming export of members');
|
|
65
|
+
const batchingTransform = this.createBatchingTransform();
|
|
66
|
+
const processingTransform = this.createProcessingTransform(reference);
|
|
67
|
+
(0, node_stream_1.pipeline)(membersQuery.stream(), batchingTransform, processingTransform, (err) => {
|
|
68
|
+
if (err) {
|
|
69
|
+
logging.error({ event: { name: 'members-export.stream.error' }, err }, 'Members export stream failed');
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
logging.info('[MembersExporter] Total time taken for member export: ' + (Date.now() - start) / 1000 + 's');
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return processingTransform;
|
|
76
|
+
}
|
|
77
|
+
// products and labels are small, stable tables, read once up front as id->name
|
|
78
|
+
// maps. Custom field definitions are read once too, which fixes the column set for
|
|
79
|
+
// the whole file: archiving a field mid-export cannot leave the header ragged. It
|
|
80
|
+
// can still leave that field's cells empty from the batch after the archive
|
|
81
|
+
// onwards, because the per-batch value read applies its own active filter.
|
|
82
|
+
async fetchReferenceData() {
|
|
83
|
+
const start = Date.now();
|
|
84
|
+
const allProducts = await this._knex('products').select('id', 'name').then(rows => rows.reduce((acc, product) => {
|
|
85
|
+
acc[product.id] = product.name;
|
|
86
|
+
return acc;
|
|
87
|
+
}, {}));
|
|
88
|
+
const allLabels = await this._knex('labels').select('id', 'name').then(rows => rows.reduce((acc, label) => {
|
|
89
|
+
acc[label.id] = label.name;
|
|
90
|
+
return acc;
|
|
91
|
+
}, {}));
|
|
92
|
+
logging.info('[MembersExporter] Fetched products and labels in ' + (Date.now() - start) + 'ms');
|
|
93
|
+
const activeCustomFields = await this._customFields.activeDefinitions();
|
|
94
|
+
return { allProducts, allLabels, activeCustomFields };
|
|
95
|
+
}
|
|
96
|
+
// Group the member stream into batches so the related-data reads are one query per
|
|
97
|
+
// batch rather than one per member.
|
|
98
|
+
createBatchingTransform() {
|
|
99
|
+
let currentBatch = [];
|
|
100
|
+
return new node_stream_1.Transform({
|
|
101
|
+
objectMode: true,
|
|
102
|
+
transform(member, encoding, callback) {
|
|
103
|
+
currentBatch.push(member);
|
|
104
|
+
if (currentBatch.length >= BATCH_SIZE) {
|
|
105
|
+
this.push(currentBatch);
|
|
106
|
+
currentBatch = [];
|
|
107
|
+
}
|
|
108
|
+
callback();
|
|
109
|
+
},
|
|
110
|
+
flush(callback) {
|
|
111
|
+
if (currentBatch.length > 0) {
|
|
112
|
+
this.push(currentBatch);
|
|
113
|
+
}
|
|
114
|
+
callback();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
// Each flattened row is pushed individually so a batch's rows do not pile up in memory.
|
|
119
|
+
createProcessingTransform(reference) {
|
|
120
|
+
const assembleRelatedData = (memberIds) => this.assembleRelatedData(memberIds, reference.activeCustomFields);
|
|
121
|
+
const flattenBatch = (members, related) => this.flattenBatch(members, related, reference);
|
|
122
|
+
return new node_stream_1.Transform({
|
|
123
|
+
objectMode: true,
|
|
124
|
+
highWaterMark: BATCH_SIZE,
|
|
125
|
+
async transform(batch, encoding, callback) {
|
|
126
|
+
try {
|
|
127
|
+
const memberIds = batch.map(member => member.id);
|
|
128
|
+
const related = await assembleRelatedData(memberIds);
|
|
129
|
+
const rows = flattenBatch(batch, related);
|
|
130
|
+
rows.forEach(row => this.push(row));
|
|
131
|
+
callback();
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
callback(err instanceof Error ? err : new errors.InternalServerError({ message: String(err) }));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// One query per related table for the whole batch, not one per member. Custom field
|
|
140
|
+
// values are read only when there are columns to fill.
|
|
141
|
+
async assembleRelatedData(memberIds, activeCustomFields) {
|
|
142
|
+
const knex = this._knex;
|
|
143
|
+
const [tiers, labels, stripeCustomers, subscriptions, gifts, customFieldValuesMap] = await Promise.all([
|
|
144
|
+
knex('members_products')
|
|
145
|
+
.select('member_id', knex.raw('GROUP_CONCAT(product_id) as tiers'))
|
|
146
|
+
.whereIn('member_id', memberIds)
|
|
147
|
+
.groupBy('member_id'),
|
|
148
|
+
knex('members_labels')
|
|
149
|
+
.select('member_id', knex.raw('GROUP_CONCAT(label_id) as labels'))
|
|
150
|
+
.whereIn('member_id', memberIds)
|
|
151
|
+
.groupBy('member_id'),
|
|
152
|
+
knex('members_stripe_customers')
|
|
153
|
+
.select('member_id', knex.raw('MIN(customer_id) as stripe_customer_id'))
|
|
154
|
+
.whereIn('member_id', memberIds)
|
|
155
|
+
.groupBy('member_id'),
|
|
156
|
+
knex('members_newsletters')
|
|
157
|
+
.distinct('member_id')
|
|
158
|
+
.whereIn('member_id', memberIds),
|
|
159
|
+
knex('gifts')
|
|
160
|
+
.select('id', 'redeemer_member_id')
|
|
161
|
+
.where('status', 'redeemed')
|
|
162
|
+
.whereIn('redeemer_member_id', memberIds),
|
|
163
|
+
activeCustomFields.length > 0
|
|
164
|
+
? this._customFields.valuesForMembers(memberIds)
|
|
165
|
+
: new Map()
|
|
166
|
+
]);
|
|
167
|
+
return {
|
|
168
|
+
tiersMap: new Map(tiers.map((row) => [row.member_id, row.tiers])),
|
|
169
|
+
labelsMap: new Map(labels.map((row) => [row.member_id, row.labels])),
|
|
170
|
+
stripeCustomerMap: new Map(stripeCustomers.map((row) => [row.member_id, row.stripe_customer_id])),
|
|
171
|
+
subscribedSet: new Set(subscriptions.map((row) => row.member_id)),
|
|
172
|
+
giftIdMap: new Map(gifts.map((row) => [row.redeemer_member_id, row.id])),
|
|
173
|
+
customFieldValuesMap
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
flattenBatch(members, related, reference) {
|
|
177
|
+
const { tiersMap, labelsMap, stripeCustomerMap, subscribedSet, giftIdMap, customFieldValuesMap } = related;
|
|
178
|
+
const { allProducts, allLabels, activeCustomFields } = reference;
|
|
179
|
+
return members.map((row) => {
|
|
180
|
+
const tierConcat = tiersMap.get(row.id);
|
|
181
|
+
const tierIds = tierConcat ? tierConcat.split(',') : [];
|
|
182
|
+
const labelConcat = labelsMap.get(row.id);
|
|
183
|
+
const labelIds = labelConcat ? labelConcat.split(',') : [];
|
|
184
|
+
return {
|
|
185
|
+
...row,
|
|
186
|
+
subscribed: subscribedSet.has(row.id),
|
|
187
|
+
comped: row.status === 'comped',
|
|
188
|
+
gift_id: giftIdMap.get(row.id) || null,
|
|
189
|
+
stripe_customer_id: stripeCustomerMap.get(row.id) || null,
|
|
190
|
+
created_at: (0, moment_1.default)(row.created_at).toISOString(),
|
|
191
|
+
tiers: tierIds.map(id => ({ name: allProducts[id] })),
|
|
192
|
+
labels: labelIds.map(id => ({ name: allLabels[id] })),
|
|
193
|
+
// Flattened here rather than in the serializer because the column set is
|
|
194
|
+
// only knowable from the database. Every member carries a cell for every
|
|
195
|
+
// active field's column, so a member with no values still contributes the
|
|
196
|
+
// full set -- the CSV header is taken from whichever row streams first.
|
|
197
|
+
custom_field_cells: csvCellsForFields(activeCustomFields, customFieldValuesMap.get(row.id) || {})
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
exports.default = MembersCSVExporter;
|