ghost 6.54.0 → 6.55.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/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-custom-field-types-0.0.0.tgz +0 -0
- package/components/tryghost-kg-default-cards-10.3.3.tgz +0 -0
- package/core/boot.js +3 -6
- package/core/built/admin/assets/{account-migration-DLvVA5yq.js → account-migration-sjHLx5dy.js} +1 -1
- package/core/built/admin/assets/activitypub/{account-migration-DHbZcxGM.js → account-migration-DoYXtnZ8.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-Bxeh6MKW.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-D8sFfdpC.js} +2 -2
- package/core/built/admin/assets/activitypub/{domain-CmwSZLHK.js → domain-CWueFW6q.js} +5 -5
- package/core/built/admin/assets/activitypub/{edit-profile-CXdwNmVG.js → edit-profile-BGxPU7N5.js} +3 -3
- package/core/built/admin/assets/activitypub/{explore-DfbjzGM1.js → explore-DBK-u0Ch.js} +3 -3
- package/core/built/admin/assets/activitypub/{feed-DSEBJOqf.js → feed-Bcir5RQZ.js} +3 -3
- package/core/built/admin/assets/activitypub/{field-CY7XVcx6.js → field-BV-CIT3d.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-DtQ0kytT.js → inbox-yC0nfvzl.js} +4 -4
- package/core/built/admin/assets/activitypub/{label-Dw96GQ7u.js → label-DTRS9TDk.js} +2 -2
- package/core/built/admin/assets/activitypub/layout-qx-bUL1k.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-Dg_-I-8C.js → moderation-Dwl9t0pb.js} +4 -4
- package/core/built/admin/assets/activitypub/{note-Cy1-uKMY.js → note-B-zKsZ7r.js} +3 -3
- package/core/built/admin/assets/activitypub/{notifications-B_fvoLIM.js → notifications-DjJZD54t.js} +3 -3
- package/core/built/admin/assets/activitypub/{preferences-ByJseZp6.js → preferences-CoU8vs6d.js} +6 -6
- package/core/built/admin/assets/activitypub/{profile-BFtbnIjz.js → profile-BAsaJekx.js} +6 -6
- package/core/built/admin/assets/activitypub/{routes-Dh3xXfce.js → routes-CLH3OVTP.js} +28 -25
- package/core/built/admin/assets/activitypub/{settings-DTgqJPxM.js → settings-DA6pNJ-Q.js} +3 -3
- package/core/built/admin/assets/activitypub/{site-CdL9ZLqm.js → site-CQD42Pw5.js} +2 -2
- package/core/built/admin/assets/activitypub/{tabs-ogqwoBlb.js → tabs-Cg0saow1.js} +7 -7
- package/core/built/admin/assets/activitypub/{use-reply-chain-data-CxonteFD.js → use-reply-chain-data-Br4aOWOh.js} +2 -2
- package/core/built/admin/assets/audience-DjEWNhp3.js +1 -0
- package/core/built/admin/assets/{automated-emails-Cn87L5Md.js → automated-emails-DRy7qY-J.js} +2 -2
- package/core/built/admin/assets/{automations-BP_aHamJ.js → automations-Bx3VcFIL.js} +1 -1
- package/core/built/admin/assets/{automations-B7-y3dXt.js → automations-DhKeS_U5.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-Bh36XPh4.js → bluesky-sharing-ClMd0-Ft.js} +1 -1
- package/core/built/admin/assets/{breadcrumb-D34YWnaw.js → breadcrumb-BD0U2ygg.js} +1 -1
- package/core/built/admin/assets/{chart-helpers-DbklaJw9.js → chart-helpers-fjQKRVyY.js} +1 -1
- package/core/built/admin/assets/checkbox-CWa28lgO.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.481.6c7395a49484bb4583aa.js} +41 -38
- package/core/built/admin/assets/chunk.524.cffd399783a519e404a9.js +25 -0
- package/core/built/admin/assets/chunk.582.eebf0d0b2d202bb746a5.js +27 -0
- package/core/built/admin/assets/code-editor-view-CCr0rBWS.js +1 -0
- package/core/built/admin/assets/comments-DyUSMUdj.js +1 -0
- package/core/built/admin/assets/{date-range-select-B5vSsrpx.js → date-range-select-BFp58EMk.js} +1 -1
- package/core/built/admin/assets/{disabled-sources-indicator-NkOIoMue.js → disabled-sources-indicator-CUxMfiPa.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-C0bV2UV8.js +1 -0
- package/core/built/admin/assets/dropzone-DlK-K2DQ.js +5 -0
- package/core/built/admin/assets/{edit-profile-rvDGrS8J.js → edit-profile-BKT-REJw.js} +1 -1
- package/core/built/admin/assets/editor-Dp865O-4.js +8 -0
- package/core/built/admin/assets/{explore-BZAZOg5X.js → explore-ClUpJZOp.js} +1 -1
- package/core/built/admin/assets/{feed-CFV8xDw9.js → feed-DF92dUMP.js} +1 -1
- package/core/built/admin/assets/field-Do7lieBA.js +1 -0
- package/core/built/admin/assets/{filter-sources-Fx8wiYYw.js → filter-sources-BV2rnpfA.js} +1 -1
- package/core/built/admin/assets/filters-CUXJ2Nm9.js +1 -0
- package/core/built/admin/assets/{filters-CGX_pf2N.js → filters-DfbIWc_l.js} +1 -1
- package/core/built/admin/assets/{ghost-3063eae29e6c9cc6d6e7131928a27da0.css → ghost-a9c788f6f0b1ea6cf0f14ca4b13f4496.css} +1 -1
- package/core/built/admin/assets/{ghost-b1dce91ad3232cc259ee5fbbf9526243.js → ghost-c4057474943effc568c5659ccc4909d7.js} +407 -406
- package/core/built/admin/assets/{ghost-dark-c8d0ab7f515b2010c173e23e9b32ea8a.css → ghost-dark-db36d795d21c61426837df40af270966.css} +1 -1
- package/core/built/admin/assets/gift-link-modal-f9VIMNDv.js +1 -0
- package/core/built/admin/assets/{gift-links-nBxJjZiQ.js → gift-links-CsUhefWG.js} +1 -1
- package/core/built/admin/assets/growth-B3I6gNLm.js +1 -0
- package/core/built/admin/assets/{Growth-CSaHoSVD.js → growth-CVw1My4S.js} +2 -2
- package/core/built/admin/assets/{hooks-BApwwBE0.js → hooks-CixO01lL.js} +1 -1
- package/core/built/admin/assets/{inbox-BRp1_aaC.js → inbox-CFBSu4Ed.js} +12 -12
- package/core/built/admin/assets/index-9Cw8z3rA.css +1 -0
- package/core/built/admin/assets/index-BGG7NckO.js +67 -0
- package/core/built/admin/assets/input-group-BSgG_lVw.js +1 -0
- package/core/built/admin/assets/input-surface-BpAjZXY5.js +1 -0
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +974 -937
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +28 -28
- package/core/built/admin/assets/koenig-lexical/style.css +1 -1
- package/core/built/admin/assets/{koenig-lexical-CM7UFNdL.js → koenig-lexical-f8vHRYvz.js} +28 -28
- package/core/built/admin/assets/{kpi-card-BrGH6FsT.js → kpi-card-DSap9rAy.js} +1 -1
- package/core/built/admin/assets/{kpi-card-CIGyijjx.js → kpi-card-DU0rn9Cj.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-_HRRsklE.js → kpi-helpers-Dz33AV6h.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-BSvh9eoq.js → kpi-tabs-BtTTrKvJ.js} +1 -1
- package/core/built/admin/assets/{label-BOR9XdKl.js → label-DS8Mi6G5.js} +1 -1
- package/core/built/admin/assets/label-picker-1DvSuLA1.js +1 -0
- package/core/built/admin/assets/labels-CDSUt4J_.js +1 -0
- package/core/built/admin/assets/layout-yQEi2qDM.js +1 -0
- package/core/built/admin/assets/{link-helpers-tOlxmuH5.js → link-helpers-7EYqvvHs.js} +1 -1
- package/core/built/admin/assets/member-custom-fields-yOMHyAg9.js +1 -0
- package/core/built/admin/assets/member-detail-BAq7gDaa.js +1 -0
- package/core/built/admin/assets/members-CQpVFJu4.js +10 -0
- package/core/built/admin/assets/modals-ReORvWnu.js +31 -0
- package/core/built/admin/assets/moderation-DySbgyUS.js +1 -0
- package/core/built/admin/assets/newsletter-CJQvtHqN.js +1 -0
- package/core/built/admin/assets/{Newsletters-C5cs6FGo.js → newsletters-B3QQXfGT.js} +1 -1
- package/core/built/admin/assets/{newsletters-D1jbtV-h.js → newsletters-BXJbJgA-.js} +1 -1
- package/core/built/admin/assets/note-C8kxrz6w.js +1 -0
- package/core/built/admin/assets/notifications-CgtSfX7t.js +1 -0
- package/core/built/admin/assets/{offers-CrY6nSRR.js → offers-DEniVRD5.js} +1 -1
- package/core/built/admin/assets/onboarding-route-4qtziA9d.js +1 -0
- package/core/built/admin/assets/overview-Bf2sbznq.js +1 -0
- package/core/built/admin/assets/overview-CBfePxcr.js +1 -0
- package/core/built/admin/assets/{page-header-C0jVxtS8.js → page-header-B3Wb9Jqv.js} +1 -1
- package/core/built/admin/assets/{pages-BbNWFcGj.js → pages-B4goRPUn.js} +1 -1
- package/core/built/admin/assets/{post-analytics-Iw7dk1mT.js → post-analytics-We-ko5Cb.js} +1 -1
- package/core/built/admin/assets/post-analytics-header-Bmc9su6q.js +1 -0
- package/core/built/admin/assets/post-analytics-provider-B6UynTGb.js +1 -0
- package/core/built/admin/assets/{post-share-modal-DBfKl2GZ.js → post-share-modal-DFfO93_0.js} +1 -1
- package/core/built/admin/assets/{posts-EWpZIjkr.js → posts-DhXE7b6r.js} +1 -1
- package/core/built/admin/assets/{preferences-D4EANGoc.js → preferences-DJPfHEGu.js} +2 -2
- package/core/built/admin/assets/profile-DqBQp-gT.js +1 -0
- package/core/built/admin/assets/{referrers-BCfFYSHc.js → referrers-pmJT4k3-.js} +1 -1
- package/core/built/admin/assets/select-C5_xEt5b.js +1 -0
- package/core/built/admin/assets/settings-BwiVIXau.js +4 -0
- package/core/built/admin/assets/{settings-Bz3RZRmE.js → settings-WXNeWyU1.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-DsgW3wbg.js} +1 -1
- package/core/built/admin/assets/social-urls-BQPFEk1_.js +8 -0
- package/core/built/admin/assets/{sort-button-DZp9Y2r1.js → sort-button-Bb_FCl7e.js} +1 -1
- package/core/built/admin/assets/{source-icon-CHr9qJ2Y.js → source-icon-BeaRxUn9.js} +1 -1
- package/core/built/admin/assets/{stats-DGoXZxWE.js → stats-Bjj_kmJ1.js} +1 -1
- package/core/built/admin/assets/{stats-view-D2-1L0ms.js → stats-view-BxPbW6K1.js} +1 -1
- package/core/built/admin/assets/{step-1-RdahbwyG.js → step-1-B1e3-1F8.js} +1 -1
- package/core/built/admin/assets/{step-2-D83PeRwd.js → step-2-C5WaeBJo.js} +1 -1
- package/core/built/admin/assets/{step-3-p_Op4wv4.js → step-3-B46iVPwu.js} +2 -2
- package/core/built/admin/assets/switch-z5Y6Uzt6.js +1 -0
- package/core/built/admin/assets/{table-B8LgTQJZ.js → table-CB-4_HBJ.js} +1 -1
- package/core/built/admin/assets/tabs-CCEgKYzq.js +1 -0
- package/core/built/admin/assets/{tags-B4l891z2.js → tags-Cg2VJGmG.js} +1 -1
- package/core/built/admin/assets/textarea-Bjccvh_E.js +1 -0
- package/core/built/admin/assets/{tiers-BgYLWRJv.js → tiers-DYsa4DCo.js} +1 -1
- package/core/built/admin/assets/{toggle-group-DNXDkAwf.js → toggle-group-b3IlxvhV.js} +1 -1
- package/core/built/admin/assets/{topic-filter-DZGjEcz3.js → topic-filter-DAFT87tQ.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-CaT8RirX.js → use-analytics-data-S0FTd43s.js} +1 -1
- package/core/built/admin/assets/use-filter-params-Gpoq2E1L.js +1 -0
- package/core/built/admin/assets/{use-growth-stats-9Db2CuJe.js → use-growth-stats-CIaiHOVb.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-BV54YcQS.js → use-post-referrers-Cq2vBPrA.js} +1 -1
- package/core/built/admin/assets/{use-reply-chain-data-DuYZGDan.js → use-reply-chain-data-DBDRtGu-.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-8fTua9Nt.js → use-simple-pagination-CjYNxdwe.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-query-Dz156HvZ.js → use-tinybird-query-falHAtuK.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-C9RUdL_n.js → use-tinybird-token-D7-8ND1y.js} +1 -1
- package/core/built/admin/assets/{users-CcSCQFcQ.js → users-Dkv8NQZe.js} +1 -1
- package/core/built/admin/assets/{virtual-list-BCvP0YF-.js → virtual-list-CvnC9zF2.js} +1 -1
- package/core/built/admin/assets/web-B_5GZRvH.js +1 -0
- package/core/built/admin/assets/web-jFYWCKDn.js +2 -0
- package/core/built/admin/index.html +14 -10
- package/core/frontend/services/proxy.js +10 -1
- package/core/frontend/services/routing/router-manager.js +14 -14
- package/core/frontend/services/routing/static-routes-router.js +8 -10
- 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/automation-action-links.js +68 -0
- package/core/server/api/endpoints/automation-action-links.ts +46 -0
- package/core/server/api/endpoints/db.js +6 -0
- package/core/server/api/endpoints/index.js +4 -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/settings.js +0 -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/api/endpoints/utils/serializers/output/utils/url.js +11 -1
- 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/data/migrations/versions/6.55/2026-07-27-11-25-44-add-to-hash-to-redirects.js +51 -0
- package/core/server/data/migrations/versions/6.55/2026-07-28-12-08-00-remove-routes-hash-from-settings.js +5 -0
- package/core/server/data/schema/commands.js +5 -0
- package/core/server/data/schema/default-settings/default-settings.json +0 -4
- package/core/server/data/schema/schema.js +5 -1
- 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 +21 -6
- package/core/server/services/automations/automations-api.ts +25 -6
- package/core/server/services/automations/automations-repository.ts +19 -0
- package/core/server/services/automations/database-automations-repository.js +116 -28
- package/core/server/services/automations/database-automations-repository.ts +118 -9
- package/core/server/services/automations/database-date.js +10 -0
- package/core/server/services/automations/database-date.ts +5 -0
- package/core/server/services/automations/poll.js +5 -2
- package/core/server/services/automations/poll.ts +7 -2
- package/core/server/services/automations/scheduler-poll-time.js +40 -0
- package/core/server/services/automations/scheduler-poll-time.ts +38 -0
- package/core/server/services/automations/service.js +15 -3
- package/core/server/services/automations/service.ts +16 -3
- package/core/server/services/comments/comments-service-emails.js +14 -8
- package/core/server/services/comments/comments-service.js +9 -4
- package/core/server/services/email-rendering/partials/email-wrapper.hbs +3 -1
- package/core/server/services/email-service/email-service-wrapper.js +6 -1
- package/core/server/services/email-service/email-templates/partials/styles.hbs +8 -0
- package/core/server/services/email-service/newsletter-email-event-storage.js +41 -10
- package/core/server/services/lib/dynamic-redirect-manager.js +12 -3
- package/core/server/services/link-redirection/link-redirect-repository.js +41 -4
- package/core/server/services/link-redirection/link-redirect.js +2 -0
- package/core/server/services/link-redirection/link-redirect.ts +3 -1
- package/core/server/services/link-redirection/link-redirects-service.js +38 -2
- package/core/server/services/link-tracking/click-event.js +3 -0
- package/core/server/services/link-tracking/index.js +38 -6
- package/core/server/services/link-tracking/link-click-repository.js +30 -9
- package/core/server/services/link-tracking/link-click-tracking-service.js +44 -3
- package/core/server/services/member-attribution/outbound-link-tagger.js +3 -1
- package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +19 -4
- package/core/server/services/member-welcome-emails/service.js +11 -3
- 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/import-export/csv/formula.js +16 -0
- package/core/server/services/members/import-export/csv/formula.ts +14 -0
- package/core/server/services/members/import-export/csv/index.js +12 -0
- package/core/server/services/members/import-export/csv/index.ts +3 -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 +84 -0
- package/core/server/services/members/import-export/import/completion-email.ts +141 -0
- package/core/server/services/members/import-export/import/importer.js +297 -0
- package/core/server/services/members/import-export/import/importer.ts +468 -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 +99 -0
- package/core/server/services/members/import-export/index.ts +137 -0
- package/core/server/services/members/members-api/controllers/router-controller.js +7 -0
- package/core/server/services/members/service.js +25 -20
- package/core/server/services/members-custom-fields/values-service.js +19 -7
- package/core/server/services/members-custom-fields/values-service.ts +19 -7
- package/core/server/services/mentions/service.js +5 -1
- package/core/server/services/posts/posts-exporter.js +6 -4
- package/core/server/services/posts/posts-service.js +2 -2
- package/core/server/services/route-settings/activation-bridge.js +59 -49
- package/core/server/services/route-settings/activation-bridge.ts +91 -51
- package/core/server/services/route-settings/dynamic-routing-service.js +22 -51
- package/core/server/services/route-settings/index.js +1 -16
- package/core/server/services/settings/settings-service.js +0 -18
- package/core/server/services/themes/storage.js +9 -0
- package/core/server/services/themes/theme-storage.js +1 -1
- package/core/server/services/url/url-service-facade.js +1 -1
- package/core/server/services/url/url-service-facade.ts +8 -1
- package/core/server/web/api/endpoints/admin/routes.js +1 -0
- 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 +11 -11
- package/pnpm-lock.yaml +18 -18
- package/pnpm-workspace.yaml +8 -7
- package/components/tryghost-adapter-base-scheduling-0.1.1.tgz +0 -0
- 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/audience-BKRdsFmy.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/constants-c-JNF9c2.js +0 -1
- package/core/built/admin/assets/dist-CT46_f_J.js +0 -1
- package/core/built/admin/assets/domain-B7_n5nfx.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/field-BW-M2Zwp.js +0 -1
- package/core/built/admin/assets/filters-DtcXMZDU.js +0 -1
- package/core/built/admin/assets/gift-link-modal-Bk8qpnMw.js +0 -1
- package/core/built/admin/assets/growth-Ck2s3JjR.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/label-picker-BdNAWuUX.js +0 -1
- package/core/built/admin/assets/labels-Cyayfd-e.js +0 -1
- package/core/built/admin/assets/layout-Bw4GnxXp.js +0 -1
- package/core/built/admin/assets/member-custom-fields-DvFyamLu.js +0 -1
- package/core/built/admin/assets/member-detail-Cg2vTqvJ.js +0 -1
- package/core/built/admin/assets/members-DsPxYAVm.js +0 -10
- package/core/built/admin/assets/modals-Bf_fyvHZ.js +0 -31
- package/core/built/admin/assets/moderation-Dlp7M5sa.js +0 -1
- package/core/built/admin/assets/multi-select-combobox-CH1o8XXQ.js +0 -1
- package/core/built/admin/assets/newsletter-CQ5CAYUI.js +0 -1
- package/core/built/admin/assets/note-BrAZ7Z0o.js +0 -1
- package/core/built/admin/assets/notifications-BnbHLOPC.js +0 -1
- package/core/built/admin/assets/onboarding-route-eU146kuA.js +0 -1
- package/core/built/admin/assets/overview-BpbaK-xu.js +0 -1
- package/core/built/admin/assets/post-analytics-header-D9pJ4iNm.js +0 -1
- package/core/built/admin/assets/post-analytics-provider-BDqcfBor.js +0 -1
- 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/tabs-D3ZOYkM7.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.js +0 -10
- 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/route-settings/default-settings-manager.js +0 -63
- package/core/server/services/route-settings/route-settings.js +0 -170
- package/core/server/services/route-settings/settings-loader.js +0 -60
- package/core/server/services/route-settings/settings-path-manager.js +0 -47
- package/core/server/services/route-settings/validate.js +0 -440
- /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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import type {MemberImportRow} from './row';
|
|
6
|
+
|
|
7
|
+
// A handle to rows written to a spool: the deferred job reads them back once it
|
|
8
|
+
// runs, then removes the file. Removal swallows errors -- a failed cleanup must
|
|
9
|
+
// never fail the import it was cleaning up after.
|
|
10
|
+
export interface SpooledRows {
|
|
11
|
+
read(): Promise<MemberImportRow[]>;
|
|
12
|
+
remove(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RowSpool {
|
|
16
|
+
write(rows: MemberImportRow[]): Promise<SpooledRows>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Spools import rows to a private JSON file under the OS temp dir, so a deferred
|
|
20
|
+
// import can hand them to a background job and read them back after the request has
|
|
21
|
+
// already returned. The rows go in and come out as MemberImportRow, so nothing but
|
|
22
|
+
// the import's own row shape crosses this boundary.
|
|
23
|
+
export function createRowSpool(): RowSpool {
|
|
24
|
+
return {
|
|
25
|
+
async write(rows) {
|
|
26
|
+
const spoolPath = path.join(os.tmpdir(), `members-import-${crypto.randomUUID()}.json`);
|
|
27
|
+
await fs.writeFile(spoolPath, JSON.stringify(rows), {mode: 0o600});
|
|
28
|
+
return {
|
|
29
|
+
async read() {
|
|
30
|
+
return JSON.parse(await fs.readFile(spoolPath, 'utf8'));
|
|
31
|
+
},
|
|
32
|
+
async remove() {
|
|
33
|
+
await fs.remove(spoolPath).catch(() => {});
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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.makeImporter = makeImporter;
|
|
7
|
+
exports.makeExporter = makeExporter;
|
|
8
|
+
const importer_1 = __importDefault(require("./import/importer"));
|
|
9
|
+
const reader_1 = __importDefault(require("./import/reader"));
|
|
10
|
+
const spool_1 = require("./import/spool");
|
|
11
|
+
const exporter_1 = __importDefault(require("./export/exporter"));
|
|
12
|
+
const MembersCSVImporterStripeUtils = require('./import/stripe-utils');
|
|
13
|
+
const db = require('../../../data/db');
|
|
14
|
+
const models = require('../../../models');
|
|
15
|
+
const labs = require('../../../../shared/labs');
|
|
16
|
+
// Build the members CSV importer. This is the composition root: today's models and
|
|
17
|
+
// services are wired behind the collaborators the importer declares, one per
|
|
18
|
+
// concern, so nothing Bookshelf-shaped leaks into the import service itself.
|
|
19
|
+
function makeImporter(deps) {
|
|
20
|
+
// The members repository resolves asynchronously and is stable once ready, so
|
|
21
|
+
// cache the promise and reuse it across every call the import makes.
|
|
22
|
+
let membersRepositoryPromise;
|
|
23
|
+
const getMembersRepository = () => (membersRepositoryPromise ??= deps.getMembersRepository());
|
|
24
|
+
// The members aggregate, plus the import label lookup folded in so the label
|
|
25
|
+
// (member-tagging data) does not need a source of its own.
|
|
26
|
+
const members = {
|
|
27
|
+
get: async (query, options) => (await getMembersRepository()).get(query, options),
|
|
28
|
+
create: async (values, options) => (await getMembersRepository()).create(values, options),
|
|
29
|
+
update: async (values, options) => (await getMembersRepository()).update(values, options),
|
|
30
|
+
getCustomerIdByEmail: async (email) => (await getMembersRepository()).getCustomerIdByEmail(email),
|
|
31
|
+
linkStripeCustomer: async (link, options) => (await getMembersRepository()).linkStripeCustomer(link, options),
|
|
32
|
+
getImportLabel: name => models.Label.findOne({ name })
|
|
33
|
+
};
|
|
34
|
+
// The completion email: its recipient, links and delivery in one collaborator.
|
|
35
|
+
const email = {
|
|
36
|
+
send: deps.sendEmail,
|
|
37
|
+
getDefaultRecipient: async () => (await models.User.getOwnerUser()).get('email'),
|
|
38
|
+
urlFor: deps.urlFor
|
|
39
|
+
};
|
|
40
|
+
// Gifts is initialised at boot and always present at request time; the getter
|
|
41
|
+
// resolves it lazily so the ready service is picked up whenever a row uses it.
|
|
42
|
+
const gifts = {
|
|
43
|
+
reassignRedeemer: (giftId, memberId, options) => deps.getGiftService().reassignRedeemer(giftId, memberId, options)
|
|
44
|
+
};
|
|
45
|
+
// Gated by the same labs flag as the export, so the two halves round-trip or stay
|
|
46
|
+
// silent together: off, activeFields resolves empty and every custom_fields.* column
|
|
47
|
+
// is dropped.
|
|
48
|
+
const customFields = {
|
|
49
|
+
activeFields: async () => (labs.isSet('membersCustomFields') ? deps.customFields.definitions.browse() : []),
|
|
50
|
+
planWrite: values => deps.customFields.values.planWrite(values),
|
|
51
|
+
applyWrite: (memberId, plan, executor) => deps.customFields.values.applyWrite(memberId, plan, executor)
|
|
52
|
+
};
|
|
53
|
+
return new importer_1.default({
|
|
54
|
+
knex: deps.knex,
|
|
55
|
+
readRows: reader_1.default,
|
|
56
|
+
spool: (0, spool_1.createRowSpool)(),
|
|
57
|
+
members,
|
|
58
|
+
tiers: {
|
|
59
|
+
getDefault: deps.getDefaultTier,
|
|
60
|
+
getByName: deps.getTierByName
|
|
61
|
+
},
|
|
62
|
+
stripe: new MembersCSVImporterStripeUtils({
|
|
63
|
+
stripeAPIService: deps.stripeAPIService,
|
|
64
|
+
productRepository: deps.productRepository
|
|
65
|
+
}),
|
|
66
|
+
gifts,
|
|
67
|
+
customFields,
|
|
68
|
+
email,
|
|
69
|
+
addJob: deps.addJob,
|
|
70
|
+
getTimezone: deps.getTimezone,
|
|
71
|
+
getInlineThreshold: deps.getInlineThreshold
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Build the members CSV exporter. The same composition root from the other direction:
|
|
75
|
+
// knex and the members id lookup are wired here, and the custom fields definitions and
|
|
76
|
+
// values services are injected (boot builds them before this one). The labs flag alone
|
|
77
|
+
// decides whether custom field columns appear, so nothing flag-shaped leaks into the
|
|
78
|
+
// exporter itself.
|
|
79
|
+
function makeExporter({ definitions, values }) {
|
|
80
|
+
const exporter = new exporter_1.default({
|
|
81
|
+
knex: db.knex,
|
|
82
|
+
members: {
|
|
83
|
+
// Minimal query, only to fetch the ids of the filtered members; the stream
|
|
84
|
+
// reads their related data itself.
|
|
85
|
+
findFilteredIds: async (options) => {
|
|
86
|
+
const page = await models.Member.findPage({ ...options, withRelated: [], columns: ['id'], limit: 'all' });
|
|
87
|
+
return page.data.map((member) => member.id);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
customFields: {
|
|
91
|
+
// Boot builds the definitions and values services before this one, so they
|
|
92
|
+
// are always present -- no not-initialised state to guard. The flag decides
|
|
93
|
+
// whether their columns are included at all.
|
|
94
|
+
activeDefinitions: async () => (labs.isSet('membersCustomFields') ? definitions.browse() : []),
|
|
95
|
+
valuesForMembers: memberIds => values.getValuesForMembers(memberIds)
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return (options = {}) => exporter.export(options);
|
|
99
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type {Knex} from 'knex';
|
|
2
|
+
import type {CsvField} from '@tryghost/custom-field-types/csv';
|
|
3
|
+
import MembersCSVImporter, {type MembersRepository, type GiftService, type EmailNotifications, type Tier, type CustomFieldsImport} from './import/importer';
|
|
4
|
+
import readMemberRows from './import/reader';
|
|
5
|
+
import {createRowSpool} from './import/spool';
|
|
6
|
+
import MembersCSVExporter, {type ExportOptions, type CustomFieldDefinition} from './export/exporter';
|
|
7
|
+
|
|
8
|
+
const MembersCSVImporterStripeUtils = require('./import/stripe-utils');
|
|
9
|
+
const db = require('../../../data/db');
|
|
10
|
+
const models = require('../../../models');
|
|
11
|
+
const labs = require('../../../../shared/labs');
|
|
12
|
+
|
|
13
|
+
// The raw collaborators the members service hands the import composition root, before
|
|
14
|
+
// they are adapted into the ports the importer declares. The members repository is the
|
|
15
|
+
// importer's aggregate minus the import-label lookup, which the root folds in itself.
|
|
16
|
+
interface ImporterServices {
|
|
17
|
+
knex: Knex;
|
|
18
|
+
getMembersRepository(): Promise<Omit<MembersRepository, 'getImportLabel'>>;
|
|
19
|
+
getDefaultTier(): Promise<Tier>;
|
|
20
|
+
getTierByName(name: string): Promise<Tier | null>;
|
|
21
|
+
getGiftService(): GiftService;
|
|
22
|
+
sendEmail: EmailNotifications['send'];
|
|
23
|
+
urlFor: EmailNotifications['urlFor'];
|
|
24
|
+
addJob(job: {job: () => Promise<void>; offloaded: boolean; name: string}): void;
|
|
25
|
+
getTimezone(): string;
|
|
26
|
+
getInlineThreshold(): number;
|
|
27
|
+
stripeAPIService: unknown;
|
|
28
|
+
productRepository: unknown;
|
|
29
|
+
// The custom fields services the members service hands the import composition root.
|
|
30
|
+
customFields: {
|
|
31
|
+
definitions: {browse(): Promise<CsvField[]>};
|
|
32
|
+
values: {
|
|
33
|
+
planWrite(values: Record<string, unknown>): Promise<unknown[]>;
|
|
34
|
+
applyWrite(memberId: string, plan: unknown[], executor: Knex): Promise<void>;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The custom fields services the members service hands the export composition root.
|
|
40
|
+
interface CustomFieldsServices {
|
|
41
|
+
definitions: {browse(): Promise<CustomFieldDefinition[]>};
|
|
42
|
+
values: {getValuesForMembers(memberIds: string[]): Promise<Map<string, Record<string, unknown>>>};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Build the members CSV importer. This is the composition root: today's models and
|
|
46
|
+
// services are wired behind the collaborators the importer declares, one per
|
|
47
|
+
// concern, so nothing Bookshelf-shaped leaks into the import service itself.
|
|
48
|
+
export function makeImporter(deps: ImporterServices) {
|
|
49
|
+
// The members repository resolves asynchronously and is stable once ready, so
|
|
50
|
+
// cache the promise and reuse it across every call the import makes.
|
|
51
|
+
let membersRepositoryPromise: Promise<Omit<MembersRepository, 'getImportLabel'>> | undefined;
|
|
52
|
+
const getMembersRepository = () => (membersRepositoryPromise ??= deps.getMembersRepository());
|
|
53
|
+
|
|
54
|
+
// The members aggregate, plus the import label lookup folded in so the label
|
|
55
|
+
// (member-tagging data) does not need a source of its own.
|
|
56
|
+
const members: MembersRepository = {
|
|
57
|
+
get: async (query, options) => (await getMembersRepository()).get(query, options),
|
|
58
|
+
create: async (values, options) => (await getMembersRepository()).create(values, options),
|
|
59
|
+
update: async (values, options) => (await getMembersRepository()).update(values, options),
|
|
60
|
+
getCustomerIdByEmail: async email => (await getMembersRepository()).getCustomerIdByEmail(email),
|
|
61
|
+
linkStripeCustomer: async (link, options) => (await getMembersRepository()).linkStripeCustomer(link, options),
|
|
62
|
+
getImportLabel: name => models.Label.findOne({name})
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// The completion email: its recipient, links and delivery in one collaborator.
|
|
66
|
+
const email: EmailNotifications = {
|
|
67
|
+
send: deps.sendEmail,
|
|
68
|
+
getDefaultRecipient: async () => (await models.User.getOwnerUser()).get('email'),
|
|
69
|
+
urlFor: deps.urlFor
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Gifts is initialised at boot and always present at request time; the getter
|
|
73
|
+
// resolves it lazily so the ready service is picked up whenever a row uses it.
|
|
74
|
+
const gifts: GiftService = {
|
|
75
|
+
reassignRedeemer: (giftId, memberId, options) => deps.getGiftService().reassignRedeemer(giftId, memberId, options)
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Gated by the same labs flag as the export, so the two halves round-trip or stay
|
|
79
|
+
// silent together: off, activeFields resolves empty and every custom_fields.* column
|
|
80
|
+
// is dropped.
|
|
81
|
+
const customFields: CustomFieldsImport = {
|
|
82
|
+
activeFields: async () => (labs.isSet('membersCustomFields') ? deps.customFields.definitions.browse() : []),
|
|
83
|
+
planWrite: values => deps.customFields.values.planWrite(values),
|
|
84
|
+
applyWrite: (memberId, plan, executor) => deps.customFields.values.applyWrite(memberId, plan, executor)
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return new MembersCSVImporter({
|
|
88
|
+
knex: deps.knex,
|
|
89
|
+
readRows: readMemberRows,
|
|
90
|
+
spool: createRowSpool(),
|
|
91
|
+
members,
|
|
92
|
+
tiers: {
|
|
93
|
+
getDefault: deps.getDefaultTier,
|
|
94
|
+
getByName: deps.getTierByName
|
|
95
|
+
},
|
|
96
|
+
stripe: new MembersCSVImporterStripeUtils({
|
|
97
|
+
stripeAPIService: deps.stripeAPIService,
|
|
98
|
+
productRepository: deps.productRepository
|
|
99
|
+
}),
|
|
100
|
+
gifts,
|
|
101
|
+
customFields,
|
|
102
|
+
email,
|
|
103
|
+
addJob: deps.addJob,
|
|
104
|
+
getTimezone: deps.getTimezone,
|
|
105
|
+
getInlineThreshold: deps.getInlineThreshold
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Build the members CSV exporter. The same composition root from the other direction:
|
|
110
|
+
// knex and the members id lookup are wired here, and the custom fields definitions and
|
|
111
|
+
// values services are injected (boot builds them before this one). The labs flag alone
|
|
112
|
+
// decides whether custom field columns appear, so nothing flag-shaped leaks into the
|
|
113
|
+
// exporter itself.
|
|
114
|
+
export function makeExporter({definitions, values}: CustomFieldsServices): (options?: ExportOptions) => Promise<NodeJS.ReadableStream> {
|
|
115
|
+
const exporter = new MembersCSVExporter({
|
|
116
|
+
knex: db.knex,
|
|
117
|
+
|
|
118
|
+
members: {
|
|
119
|
+
// Minimal query, only to fetch the ids of the filtered members; the stream
|
|
120
|
+
// reads their related data itself.
|
|
121
|
+
findFilteredIds: async (options) => {
|
|
122
|
+
const page = await models.Member.findPage({...options, withRelated: [], columns: ['id'], limit: 'all'});
|
|
123
|
+
return page.data.map((member: {id: string}) => member.id);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
customFields: {
|
|
128
|
+
// Boot builds the definitions and values services before this one, so they
|
|
129
|
+
// are always present -- no not-initialised state to guard. The flag decides
|
|
130
|
+
// whether their columns are included at all.
|
|
131
|
+
activeDefinitions: async (): Promise<CustomFieldDefinition[]> => (labs.isSet('membersCustomFields') ? definitions.browse() : []),
|
|
132
|
+
valuesForMembers: memberIds => values.getValuesForMembers(memberIds)
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return (options = {}) => exporter.export(options);
|
|
137
|
+
}
|
|
@@ -467,6 +467,13 @@ module.exports = class RouterController {
|
|
|
467
467
|
});
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
+
if (offer.status && offer.status !== 'active') {
|
|
471
|
+
throw new BadRequestError({
|
|
472
|
+
message: tpl(messages.offerArchived),
|
|
473
|
+
context: 'Offer with id "' + offerId + '" is no longer active'
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
470
477
|
if (!offer.tier) {
|
|
471
478
|
throw new BadRequestError({
|
|
472
479
|
message: 'Offer does not have a tier'
|
|
@@ -4,13 +4,12 @@ const tpl = require('@tryghost/tpl');
|
|
|
4
4
|
const MembersSSR = require('./members-ssr');
|
|
5
5
|
const db = require('../../data/db');
|
|
6
6
|
const MembersConfigProvider = require('./members-config-provider');
|
|
7
|
-
const
|
|
8
|
-
const {resolveInlineThreshold} = require('./
|
|
7
|
+
const {makeImporter, makeExporter} = require('./import-export');
|
|
8
|
+
const {resolveInlineThreshold} = require('./import-export/config');
|
|
9
9
|
const MembersStats = require('./stats/members-stats');
|
|
10
10
|
const memberJobs = require('./jobs');
|
|
11
11
|
const logging = require('@tryghost/logging');
|
|
12
12
|
const urlUtils = require('../../../shared/url-utils').default;
|
|
13
|
-
const labsService = require('../../../shared/labs');
|
|
14
13
|
const settingsCache = require('../../../shared/settings-cache');
|
|
15
14
|
const config = require('../../../shared/config');
|
|
16
15
|
const models = require('../../models');
|
|
@@ -47,9 +46,11 @@ const membersStats = new MembersStats({
|
|
|
47
46
|
let membersApi;
|
|
48
47
|
let verificationTrigger;
|
|
49
48
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
const buildImporterDeps = ({stripeAPIService}) => {
|
|
50
|
+
// Required here, not statically: boot builds the custom fields services before this
|
|
51
|
+
// one (the exporter below relies on the same).
|
|
52
|
+
const customFields = require('../members-custom-fields');
|
|
53
|
+
return {
|
|
53
54
|
getTimezone: () => settingsCache.get('timezone'),
|
|
54
55
|
// A getter rather than a value because the threshold is an operator
|
|
55
56
|
// setting that can change between requests
|
|
@@ -79,16 +80,16 @@ const initMembersCSVImporter = ({stripeAPIService}) => {
|
|
|
79
80
|
},
|
|
80
81
|
getGiftService: () => giftService.service,
|
|
81
82
|
sendEmail: ghostMailer.send.bind(ghostMailer),
|
|
82
|
-
isSet: flag => labsService.isSet(flag),
|
|
83
83
|
addJob: jobsService.addJob.bind(jobsService),
|
|
84
84
|
knex: db.knex,
|
|
85
85
|
urlFor: urlUtils.urlFor.bind(urlUtils),
|
|
86
|
-
context: {
|
|
87
|
-
importer: true
|
|
88
|
-
},
|
|
89
86
|
stripeAPIService,
|
|
90
|
-
productRepository: membersApi.productRepository
|
|
91
|
-
|
|
87
|
+
productRepository: membersApi.productRepository,
|
|
88
|
+
customFields: {
|
|
89
|
+
definitions: customFields.definitions,
|
|
90
|
+
values: customFields.values
|
|
91
|
+
}
|
|
92
|
+
};
|
|
92
93
|
};
|
|
93
94
|
|
|
94
95
|
const initVerificationTrigger = () => {
|
|
@@ -154,19 +155,23 @@ module.exports = {
|
|
|
154
155
|
}
|
|
155
156
|
module.exports.verificationTrigger = verificationTrigger;
|
|
156
157
|
|
|
157
|
-
const
|
|
158
|
+
const importerDeps = buildImporterDeps({stripeAPIService: stripeService.api});
|
|
159
|
+
const membersCSVImporter = makeImporter(importerDeps);
|
|
160
|
+
// The importer takes plain arguments and returns the domain outcome; callers build
|
|
161
|
+
// the request and shape the response. importCSV decides inline vs deferred by load
|
|
162
|
+
// (the endpoint); importInline always runs now (the Revue data import). This only
|
|
163
|
+
// supplies the verification trigger, a members-service internal.
|
|
164
|
+
module.exports.importCSV = (request) => membersCSVImporter.importCSV(request, verificationTrigger);
|
|
165
|
+
module.exports.importInline = (request) => membersCSVImporter.importInline(request, verificationTrigger);
|
|
166
|
+
|
|
158
167
|
// Constructed here rather than required statically: the exporter needs the
|
|
159
168
|
// custom fields services, which boot builds before this one.
|
|
160
169
|
const customFields = require('../members-custom-fields');
|
|
161
|
-
module.exports.export =
|
|
170
|
+
module.exports.export = makeExporter({
|
|
162
171
|
definitions: customFields.definitions,
|
|
163
172
|
values: customFields.values
|
|
164
173
|
});
|
|
165
174
|
|
|
166
|
-
module.exports.processImport = async (options) => {
|
|
167
|
-
return await membersCSVImporter.process({...options, verificationTrigger});
|
|
168
|
-
};
|
|
169
|
-
|
|
170
175
|
if (!env?.startsWith('testing')) {
|
|
171
176
|
const membersMigrationJobName = 'members-migrations';
|
|
172
177
|
if (!(await jobsService.hasExecutedSuccessfully(membersMigrationJobName))) {
|
|
@@ -205,10 +210,10 @@ module.exports = {
|
|
|
205
210
|
|
|
206
211
|
stripeConnect: require('./stripe-connect'),
|
|
207
212
|
|
|
208
|
-
|
|
213
|
+
importCSV: null,
|
|
214
|
+
importInline: null,
|
|
209
215
|
|
|
210
216
|
stats: membersStats,
|
|
211
|
-
|
|
212
217
|
export: null
|
|
213
218
|
};
|
|
214
219
|
|
|
@@ -137,8 +137,9 @@ class CustomFieldValuesService {
|
|
|
137
137
|
/**
|
|
138
138
|
* Resolve input into the writes it implies, rejecting anything invalid, and
|
|
139
139
|
* writing nothing. Returned so a caller can validate before it commits to a
|
|
140
|
-
* change it would have to unwind (the member edit
|
|
141
|
-
* apply the same plan without
|
|
140
|
+
* change it would have to unwind (the member edit and the importer both validate
|
|
141
|
+
* up front, before opening a transaction), then apply the same plan without
|
|
142
|
+
* re-resolving or re-validating.
|
|
142
143
|
*/
|
|
143
144
|
async planWrite(input) {
|
|
144
145
|
const values = this.parseValues(input);
|
|
@@ -191,14 +192,17 @@ class CustomFieldValuesService {
|
|
|
191
192
|
* Apply a plan from `planWrite`.
|
|
192
193
|
*
|
|
193
194
|
* Merge, not replace: only the fields in the plan are touched, so a caller
|
|
194
|
-
* that doesn't know about a field can't erase it.
|
|
195
|
-
*
|
|
195
|
+
* that doesn't know about a field can't erase it.
|
|
196
|
+
*
|
|
197
|
+
* Always transactional, so a mid-batch failure rolls the batch back. Passed an
|
|
198
|
+
* executor it joins that transaction -- the importer passes its per-member one, so a
|
|
199
|
+
* failed value write takes the member with it; passed nothing it opens its own.
|
|
196
200
|
*/
|
|
197
|
-
async applyWrite(memberId, writes) {
|
|
201
|
+
async applyWrite(memberId, writes, executor = this.knex) {
|
|
198
202
|
if (writes.length === 0) {
|
|
199
203
|
return;
|
|
200
204
|
}
|
|
201
|
-
|
|
205
|
+
const apply = async (trx) => {
|
|
202
206
|
for (const { field, value } of writes) {
|
|
203
207
|
const target = { member_id: memberId, custom_field_id: field.id };
|
|
204
208
|
if (value === undefined) {
|
|
@@ -213,7 +217,15 @@ class CustomFieldValuesService {
|
|
|
213
217
|
.onConflict(['member_id', 'custom_field_id'])
|
|
214
218
|
.merge({ ...valueColumns, updated_at: new Date() });
|
|
215
219
|
}
|
|
216
|
-
}
|
|
220
|
+
};
|
|
221
|
+
// isTransaction is knex's marker for a transactor: join an existing transaction
|
|
222
|
+
// rather than nesting a savepoint under it, otherwise open one.
|
|
223
|
+
if (executor.isTransaction) {
|
|
224
|
+
await apply(executor);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
await executor.transaction(apply);
|
|
228
|
+
}
|
|
217
229
|
}
|
|
218
230
|
}
|
|
219
231
|
exports.CustomFieldValuesService = CustomFieldValuesService;
|
|
@@ -164,8 +164,9 @@ export class CustomFieldValuesService {
|
|
|
164
164
|
/**
|
|
165
165
|
* Resolve input into the writes it implies, rejecting anything invalid, and
|
|
166
166
|
* writing nothing. Returned so a caller can validate before it commits to a
|
|
167
|
-
* change it would have to unwind (the member edit
|
|
168
|
-
* apply the same plan without
|
|
167
|
+
* change it would have to unwind (the member edit and the importer both validate
|
|
168
|
+
* up front, before opening a transaction), then apply the same plan without
|
|
169
|
+
* re-resolving or re-validating.
|
|
169
170
|
*/
|
|
170
171
|
async planWrite(input: unknown): Promise<PlannedWrite[]> {
|
|
171
172
|
const values = this.parseValues(input);
|
|
@@ -225,15 +226,18 @@ export class CustomFieldValuesService {
|
|
|
225
226
|
* Apply a plan from `planWrite`.
|
|
226
227
|
*
|
|
227
228
|
* Merge, not replace: only the fields in the plan are touched, so a caller
|
|
228
|
-
* that doesn't know about a field can't erase it.
|
|
229
|
-
*
|
|
229
|
+
* that doesn't know about a field can't erase it.
|
|
230
|
+
*
|
|
231
|
+
* Always transactional, so a mid-batch failure rolls the batch back. Passed an
|
|
232
|
+
* executor it joins that transaction -- the importer passes its per-member one, so a
|
|
233
|
+
* failed value write takes the member with it; passed nothing it opens its own.
|
|
230
234
|
*/
|
|
231
|
-
async applyWrite(memberId: string, writes: PlannedWrite[]): Promise<void> {
|
|
235
|
+
async applyWrite(memberId: string, writes: PlannedWrite[], executor: Knex = this.knex): Promise<void> {
|
|
232
236
|
if (writes.length === 0) {
|
|
233
237
|
return;
|
|
234
238
|
}
|
|
235
239
|
|
|
236
|
-
|
|
240
|
+
const apply = async (trx: Knex) => {
|
|
237
241
|
for (const {field, value} of writes) {
|
|
238
242
|
const target = {member_id: memberId, custom_field_id: field.id};
|
|
239
243
|
|
|
@@ -250,6 +254,14 @@ export class CustomFieldValuesService {
|
|
|
250
254
|
.onConflict(['member_id', 'custom_field_id'])
|
|
251
255
|
.merge({...valueColumns, updated_at: new Date()});
|
|
252
256
|
}
|
|
253
|
-
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// isTransaction is knex's marker for a transactor: join an existing transaction
|
|
260
|
+
// rather than nesting a savepoint under it, otherwise open one.
|
|
261
|
+
if (executor.isTransaction) {
|
|
262
|
+
await apply(executor);
|
|
263
|
+
} else {
|
|
264
|
+
await executor.transaction(apply);
|
|
265
|
+
}
|
|
254
266
|
}
|
|
255
267
|
}
|
|
@@ -29,7 +29,11 @@ async function getPostData(post) {
|
|
|
29
29
|
|
|
30
30
|
function getPostUrl(id, postData) {
|
|
31
31
|
const jsonModel = {...postData};
|
|
32
|
-
|
|
32
|
+
// The URL service routes by resource type. Pages and posts share the Post
|
|
33
|
+
// model, so the page's own type must reach forPost — otherwise it defaults
|
|
34
|
+
// to 'posts', matches no post collection, and 404s under the lazy service.
|
|
35
|
+
const type = postData.type === 'page' ? 'pages' : 'posts';
|
|
36
|
+
outputSerializerUrlUtil.forPost(id, jsonModel, {options: {}}, type);
|
|
33
37
|
return jsonModel.url;
|
|
34
38
|
}
|
|
35
39
|
|
|
@@ -83,7 +83,7 @@ class PostsExporter {
|
|
|
83
83
|
* @param {string|number} [options.limit]
|
|
84
84
|
* @returns {Promise<Readable>}
|
|
85
85
|
*/
|
|
86
|
-
async export({filter, order, limit}) {
|
|
86
|
+
async export({filter, order, limit, mongoTransformer}) {
|
|
87
87
|
const exportContext = await this.#getExportContext();
|
|
88
88
|
const requestedLimit = parseExportLimit(limit);
|
|
89
89
|
const pageLimit = Math.min(EXPORT_BATCH_SIZE, requestedLimit);
|
|
@@ -93,7 +93,8 @@ class PostsExporter {
|
|
|
93
93
|
order: withStableExportOrder(order),
|
|
94
94
|
requestedLimit,
|
|
95
95
|
pageLimit,
|
|
96
|
-
exportContext
|
|
96
|
+
exportContext,
|
|
97
|
+
mongoTransformer
|
|
97
98
|
}), {objectMode: true});
|
|
98
99
|
}
|
|
99
100
|
|
|
@@ -119,7 +120,7 @@ class PostsExporter {
|
|
|
119
120
|
* @param {ExportContext} options.exportContext
|
|
120
121
|
* @returns {AsyncGenerator<object>}
|
|
121
122
|
*/
|
|
122
|
-
async *#streamPosts({filter, order, requestedLimit, pageLimit, exportContext}) {
|
|
123
|
+
async *#streamPosts({filter, order, requestedLimit, pageLimit, exportContext, mongoTransformer}) {
|
|
123
124
|
let emitted = 0;
|
|
124
125
|
|
|
125
126
|
for (let page = 1; emitted < requestedLimit; page++) {
|
|
@@ -129,7 +130,8 @@ class PostsExporter {
|
|
|
129
130
|
limit: pageLimit,
|
|
130
131
|
page,
|
|
131
132
|
skipPagination: true,
|
|
132
|
-
withRelated: EXPORT_WITH_RELATED
|
|
133
|
+
withRelated: EXPORT_WITH_RELATED,
|
|
134
|
+
mongoTransformer
|
|
133
135
|
});
|
|
134
136
|
|
|
135
137
|
if (posts.data.length === 0) {
|
|
@@ -329,8 +329,8 @@ class PostsService {
|
|
|
329
329
|
return result;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
async export(
|
|
333
|
-
return await this.postsExporter.export(
|
|
332
|
+
async export(options) {
|
|
333
|
+
return await this.postsExporter.export(options);
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
async #updatePosts(data, options) {
|