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,141 @@
|
|
|
1
|
+
import {serialize} from '../csv';
|
|
2
|
+
import {isCustomFieldColumn} from '@tryghost/custom-field-types/csv';
|
|
3
|
+
import type {MemberImportRow, ImportErrorRow, ImportLabel, Label} from './row';
|
|
4
|
+
|
|
5
|
+
const emailTemplate = require('./email-template');
|
|
6
|
+
|
|
7
|
+
// The finished import as the email reads it: how many imported and which rows
|
|
8
|
+
// failed. Structural, so the importer's richer result satisfies it directly.
|
|
9
|
+
interface ImportSummary {
|
|
10
|
+
imported: number;
|
|
11
|
+
errors: ImportErrorRow[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface CompletionEmailInput {
|
|
15
|
+
result: ImportSummary;
|
|
16
|
+
recipient: string;
|
|
17
|
+
labelName: string;
|
|
18
|
+
importLabel: ImportLabel | null;
|
|
19
|
+
urlFor: (type: string, data: unknown, absolute: boolean) => string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface EmailPayload {
|
|
23
|
+
to: string;
|
|
24
|
+
subject: string;
|
|
25
|
+
html: string;
|
|
26
|
+
forceTextContent: boolean;
|
|
27
|
+
attachments: Array<{
|
|
28
|
+
filename: string;
|
|
29
|
+
content: string;
|
|
30
|
+
contentType: string;
|
|
31
|
+
contentDisposition: string;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Turn an ORM validation error into copy a member manager can act on. Presentation
|
|
36
|
+
// only: the API response keeps the raw messages, and just the emailed report is
|
|
37
|
+
// rewritten.
|
|
38
|
+
function humaniseError(message: string): string {
|
|
39
|
+
return message
|
|
40
|
+
.replace('Value in [members.email] cannot be blank.', 'Missing email address')
|
|
41
|
+
.replace('Value in [members.note] exceeds maximum length of 2000 characters.', '"Note" exceeds maximum length of 2000 characters')
|
|
42
|
+
.replace('Value in [members.subscribed] must be one of true, false, 0 or 1.', 'Value in "Subscribed to emails" must be "true" or "false"')
|
|
43
|
+
.replace('Validation (isEmail) failed for email', 'Invalid email address')
|
|
44
|
+
.replace(/No such customer:[^,]*/, 'Could not find Stripe customer');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// One row of the fixed, member-vocabulary part of the error report, in emit order.
|
|
48
|
+
// papaparse takes the fixed columns from these keys, so the report cannot drift from the
|
|
49
|
+
// shaper -- a new member column must be added here or the shaper stops compiling. tiers
|
|
50
|
+
// and deleted_at are export-vocabulary columns an import never fills; kept always-empty
|
|
51
|
+
// so the report matches the members export CSV. Any custom_fields.* columns a submitted
|
|
52
|
+
// row carried are dynamic, so they are threaded in separately by buildErrorReport.
|
|
53
|
+
type ErrorReportRow = {
|
|
54
|
+
id: MemberImportRow['id'];
|
|
55
|
+
email: MemberImportRow['email'];
|
|
56
|
+
name: MemberImportRow['name'];
|
|
57
|
+
note: MemberImportRow['note'];
|
|
58
|
+
subscribed_to_emails: MemberImportRow['subscribed'];
|
|
59
|
+
complimentary_plan: MemberImportRow['complimentary_plan'];
|
|
60
|
+
stripe_customer_id: MemberImportRow['stripe_customer_id'];
|
|
61
|
+
created_at: MemberImportRow['created_at'];
|
|
62
|
+
deleted_at: undefined;
|
|
63
|
+
labels: string;
|
|
64
|
+
tiers: '';
|
|
65
|
+
gift_id: string | null;
|
|
66
|
+
error: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function stringifyLabels(labels: Array<string | Label>): string {
|
|
70
|
+
return labels.map(label => (typeof label === 'string' ? label : label.name)).join(',');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The custom_fields.* cells a submitted row carried, echoed untouched so a manager can
|
|
74
|
+
// fix a failed row and re-upload the values they mapped.
|
|
75
|
+
function customFieldCells(row: ImportErrorRow): Record<string, unknown> {
|
|
76
|
+
return Object.fromEntries(Object.entries(row).filter(([column]) => isCustomFieldColumn(column)));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Shape a failed import row into its fixed error-report cells, with the raw ORM message
|
|
80
|
+
// rewritten into copy the member manager can act on. Custom field cells are merged on by
|
|
81
|
+
// buildErrorReport, which owns the dynamic column set.
|
|
82
|
+
function toErrorReportRow(row: ImportErrorRow): ErrorReportRow {
|
|
83
|
+
return {
|
|
84
|
+
id: row.id,
|
|
85
|
+
email: row.email,
|
|
86
|
+
name: row.name,
|
|
87
|
+
note: row.note,
|
|
88
|
+
subscribed_to_emails: row.subscribed,
|
|
89
|
+
complimentary_plan: row.complimentary_plan,
|
|
90
|
+
stripe_customer_id: row.stripe_customer_id,
|
|
91
|
+
created_at: row.created_at,
|
|
92
|
+
deleted_at: undefined,
|
|
93
|
+
labels: stringifyLabels(row.labels),
|
|
94
|
+
tiers: '',
|
|
95
|
+
gift_id: row.gift_id || null,
|
|
96
|
+
error: humaniseError(row.error)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The error report attached to the completion email: the failed rows as CSV. It shares
|
|
101
|
+
// the serialiser with the export but not the shaping -- the export writes db members,
|
|
102
|
+
// this echoes submitted rows. Member columns come from the shaper's keys (so the type
|
|
103
|
+
// stays the single source); the custom_fields.* columns across the rows are threaded in
|
|
104
|
+
// before the last error column, and each row's custom cells merged on.
|
|
105
|
+
function buildErrorReport(errors: ImportErrorRow[]): string {
|
|
106
|
+
if (errors.length === 0) {
|
|
107
|
+
return serialize([]);
|
|
108
|
+
}
|
|
109
|
+
const memberColumns = Object.keys(toErrorReportRow(errors[0])).filter(column => column !== 'error');
|
|
110
|
+
const customColumns = [...new Set(errors.flatMap(row => Object.keys(customFieldCells(row))))];
|
|
111
|
+
const columns = [...memberColumns, ...customColumns, 'error'];
|
|
112
|
+
const rows = errors.map(row => ({...toErrorReportRow(row), ...customFieldCells(row)}));
|
|
113
|
+
return serialize(rows, {columns});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Compose the completion email for a finished import: the summary and its links,
|
|
117
|
+
// plus the attached error report. Owns how the outcome is presented, so the
|
|
118
|
+
// importer yields only the result and never touches email or CSV formatting.
|
|
119
|
+
export default function buildCompletionEmail({result, recipient, labelName, importLabel, urlFor}: CompletionEmailInput): EmailPayload {
|
|
120
|
+
const siteUrl = new URL(urlFor('home', null, true));
|
|
121
|
+
const membersUrl = new URL('members', urlFor('admin', null, true));
|
|
122
|
+
if (importLabel) {
|
|
123
|
+
membersUrl.searchParams.set('label', importLabel.slug);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const html = emailTemplate({result, siteUrl, membersUrl, emailRecipient: recipient, importLabel});
|
|
127
|
+
const subject = result.imported > 0 ? 'Your member import is complete' : 'Your member import was unsuccessful';
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
to: recipient,
|
|
131
|
+
subject,
|
|
132
|
+
html,
|
|
133
|
+
forceTextContent: true,
|
|
134
|
+
attachments: [{
|
|
135
|
+
filename: `${labelName} - Errors.csv`,
|
|
136
|
+
content: buildErrorReport(result.errors),
|
|
137
|
+
contentType: 'text/csv',
|
|
138
|
+
contentDisposition: 'attachment'
|
|
139
|
+
}]
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
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
|
+
const moment_timezone_1 = __importDefault(require("moment-timezone"));
|
|
7
|
+
const completion_email_1 = __importDefault(require("./completion-email"));
|
|
8
|
+
const csv_1 = require("../csv");
|
|
9
|
+
const csv_2 = require("@tryghost/custom-field-types/csv");
|
|
10
|
+
const metrics = require('@tryghost/metrics');
|
|
11
|
+
const errors = require('@tryghost/errors');
|
|
12
|
+
const tpl = require('@tryghost/tpl');
|
|
13
|
+
const logging = require('@tryghost/logging');
|
|
14
|
+
const messages = {
|
|
15
|
+
freeMemberNotAllowedImportTier: 'You cannot import a free member with a specified tier.',
|
|
16
|
+
invalidImportTier: '"{tier}" is not a valid tier.',
|
|
17
|
+
giftCannotCombineWithImportTier: 'Cannot specify both gift_id and import_tier.',
|
|
18
|
+
giftCannotCombineWithComplimentary: 'Cannot specify both gift_id and complimentary_plan.',
|
|
19
|
+
giftReassignFailed: 'Failed to reassign gift to member.'
|
|
20
|
+
};
|
|
21
|
+
// Columns whose presence makes a row slow to import (they reach out to Stripe), so
|
|
22
|
+
// a file carrying any of them is deferred regardless of size. The routing decision
|
|
23
|
+
// only ever sees the boolean this produces, never the column names themselves.
|
|
24
|
+
const EXPENSIVE_COLUMNS = ['stripe_customer_id'];
|
|
25
|
+
// The context each repository call runs under: the import identifies itself, and a
|
|
26
|
+
// created member is additionally flagged as imported so downstream hooks can tell.
|
|
27
|
+
const IMPORT_CONTEXT = { importer: true };
|
|
28
|
+
const CREATE_CONTEXT = { import: true };
|
|
29
|
+
// The import label members are tagged with, named for the request time. Shared by
|
|
30
|
+
// the kernel (the label) and the deferred email (the error CSV filename).
|
|
31
|
+
function buildImportLabelName(timezone) {
|
|
32
|
+
return `Import ${(0, moment_timezone_1.default)().tz(timezone).format('YYYY-MM-DD HH:mm')}`;
|
|
33
|
+
}
|
|
34
|
+
function hasExpensiveColumns(rows) {
|
|
35
|
+
return rows.some(row => EXPENSIVE_COLUMNS.some(column => !!row[column]));
|
|
36
|
+
}
|
|
37
|
+
// The routing decision, in terms it can reason about without knowing what a member
|
|
38
|
+
// or a Stripe customer is: an empty or small, cheap file imports inline (an empty
|
|
39
|
+
// one simply imports nothing); a large one, or one carrying expensive columns, is
|
|
40
|
+
// too slow to hold a request open, so it is deferred to a background job.
|
|
41
|
+
function canImportInline(rowCount, expensive, inlineThreshold) {
|
|
42
|
+
return rowCount <= inlineThreshold && !expensive;
|
|
43
|
+
}
|
|
44
|
+
class MembersCSVImporter {
|
|
45
|
+
_knex;
|
|
46
|
+
_readRows;
|
|
47
|
+
_spool;
|
|
48
|
+
_members;
|
|
49
|
+
_tiers;
|
|
50
|
+
_stripe;
|
|
51
|
+
_gifts;
|
|
52
|
+
_customFields;
|
|
53
|
+
_email;
|
|
54
|
+
_addJob;
|
|
55
|
+
_getTimezone;
|
|
56
|
+
_getInlineThreshold;
|
|
57
|
+
constructor({ knex, readRows, spool, members, tiers, stripe, gifts, customFields, email, addJob, getTimezone, getInlineThreshold }) {
|
|
58
|
+
this._knex = knex;
|
|
59
|
+
this._readRows = readRows;
|
|
60
|
+
this._spool = spool;
|
|
61
|
+
this._members = members;
|
|
62
|
+
this._tiers = tiers;
|
|
63
|
+
this._stripe = stripe;
|
|
64
|
+
this._gifts = gifts;
|
|
65
|
+
this._customFields = customFields;
|
|
66
|
+
this._email = email;
|
|
67
|
+
this._addJob = addJob;
|
|
68
|
+
this._getTimezone = getTimezone;
|
|
69
|
+
this._getInlineThreshold = getInlineThreshold;
|
|
70
|
+
}
|
|
71
|
+
async importCSV(request, verificationTrigger) {
|
|
72
|
+
const rows = await this._readRows(request.filePath, request.mapping);
|
|
73
|
+
const labelName = buildImportLabelName(this._getTimezone());
|
|
74
|
+
const extraLabels = request.extraLabels ?? [];
|
|
75
|
+
if (canImportInline(rows.length, hasExpensiveColumns(rows), this._getInlineThreshold())) {
|
|
76
|
+
const result = await this.importRows(rows, labelName, extraLabels);
|
|
77
|
+
await verificationTrigger.testImportThreshold();
|
|
78
|
+
return { deferred: false, originalImportSize: rows.length, result };
|
|
79
|
+
}
|
|
80
|
+
await this.deferImport(rows, { labelName, extraLabels, requestUserEmail: request.requestUserEmail }, verificationTrigger);
|
|
81
|
+
return { deferred: true, originalImportSize: rows.length };
|
|
82
|
+
}
|
|
83
|
+
async importInline(request, verificationTrigger) {
|
|
84
|
+
const rows = await this._readRows(request.filePath, request.mapping);
|
|
85
|
+
const result = await this.importRows(rows, buildImportLabelName(this._getTimezone()), request.extraLabels ?? []);
|
|
86
|
+
await verificationTrigger.testImportThreshold();
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
async deferImport(rows, { labelName, extraLabels, requestUserEmail }, verificationTrigger) {
|
|
90
|
+
// Resolved here, not at the API boundary, so the owner lookup only runs when
|
|
91
|
+
// a request without a user actually reaches the deferred path.
|
|
92
|
+
const emailRecipient = requestUserEmail ?? await this._email.getDefaultRecipient();
|
|
93
|
+
const spooled = await this._spool.write(rows);
|
|
94
|
+
this._addJob({
|
|
95
|
+
job: () => this.runImportJob(spooled, { labelName, extraLabels, emailRecipient }, verificationTrigger),
|
|
96
|
+
offloaded: false,
|
|
97
|
+
name: 'members-import'
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// Swallows its own errors: a failed import or email must not leave the queued job
|
|
101
|
+
// rejected, and the verification trigger must still run afterwards either way.
|
|
102
|
+
async runImportJob(spooled, { labelName, extraLabels, emailRecipient }, verificationTrigger) {
|
|
103
|
+
try {
|
|
104
|
+
const spooledRows = await spooled.read();
|
|
105
|
+
const result = await this.importRows(spooledRows, labelName, extraLabels);
|
|
106
|
+
const importLabel = result.importLabel ?? null;
|
|
107
|
+
await this._email.send((0, completion_email_1.default)({
|
|
108
|
+
result,
|
|
109
|
+
recipient: emailRecipient,
|
|
110
|
+
labelName,
|
|
111
|
+
importLabel,
|
|
112
|
+
urlFor: this._email.urlFor
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
logging.error('Error in members import job');
|
|
117
|
+
logging.error(e);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await spooled.remove();
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await verificationTrigger.testImportThreshold();
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
logging.error('Error in members import job when testing import threshold');
|
|
127
|
+
logging.error(e);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// The members import kernel, shared by the inline and deferred paths. Each row is
|
|
131
|
+
// created or updated in its own transaction, so one row's failure rolls back only
|
|
132
|
+
// itself; failures are collected, and any Stripe prices an import tier created are
|
|
133
|
+
// archived at the end.
|
|
134
|
+
async importRows(rows, labelName, extraLabels) {
|
|
135
|
+
const importLabel = { name: labelName };
|
|
136
|
+
const globalLabels = [importLabel, ...extraLabels];
|
|
137
|
+
const performStart = Date.now();
|
|
138
|
+
const defaultTier = await this._tiers.getDefault();
|
|
139
|
+
// The field set a custom_fields.* column is read against; empty when the feature
|
|
140
|
+
// is off, so a carried-through column is dropped and the write boundary stays shut.
|
|
141
|
+
const activeCustomFields = await this._customFields.activeFields();
|
|
142
|
+
const tierIdCache = new Map();
|
|
143
|
+
const archivableStripePriceIds = [];
|
|
144
|
+
// Copied per row: the member model stamps ids and trims names onto these in
|
|
145
|
+
// place, and each row runs in its own transaction that can roll back. A
|
|
146
|
+
// caller can hand in a nameless label, which the model would drop anyway.
|
|
147
|
+
const cloneGlobalLabels = () => globalLabels
|
|
148
|
+
.map(label => ({ ...label }))
|
|
149
|
+
.filter(label => label.name);
|
|
150
|
+
let imported = 0;
|
|
151
|
+
const importErrors = [];
|
|
152
|
+
for (const row of rows) {
|
|
153
|
+
let trx;
|
|
154
|
+
try {
|
|
155
|
+
if (row.gift_id) {
|
|
156
|
+
if (row.import_tier) {
|
|
157
|
+
throw wrapGiftError(new errors.DataImportError({ message: tpl(messages.giftCannotCombineWithImportTier) }));
|
|
158
|
+
}
|
|
159
|
+
if (row.complimentary_plan) {
|
|
160
|
+
throw wrapGiftError(new errors.DataImportError({ message: tpl(messages.giftCannotCombineWithComplimentary) }));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Validate the row's custom field values before the transaction opens.
|
|
164
|
+
// planWrite only reads, and it throws on an invalid value to fail the row
|
|
165
|
+
// before any member write -- so there is no reason to hold a transaction
|
|
166
|
+
// across it, and doing so would deadlock the single-connection SQLite pool.
|
|
167
|
+
const customFieldPlan = activeCustomFields.length > 0
|
|
168
|
+
? await this._customFields.planWrite((0, csv_2.fieldValuesFromCsvRow)(activeCustomFields, row, csv_1.stripFormulaGuard))
|
|
169
|
+
: [];
|
|
170
|
+
trx = await this._knex.transaction(undefined, { doNotRejectOnRollback: false });
|
|
171
|
+
const options = { transacting: trx, context: IMPORT_CONTEXT };
|
|
172
|
+
const createdAt = (row.created_at && (0, moment_timezone_1.default)(row.created_at).isAfter((0, moment_timezone_1.default)())) ? (0, moment_timezone_1.default)().toDate() : row.created_at;
|
|
173
|
+
const memberValues = {
|
|
174
|
+
email: row.email,
|
|
175
|
+
name: row.name,
|
|
176
|
+
note: row.note,
|
|
177
|
+
subscribed: row.subscribed,
|
|
178
|
+
created_at: createdAt,
|
|
179
|
+
labels: [...row.labels, ...cloneGlobalLabels()]
|
|
180
|
+
};
|
|
181
|
+
const existingMember = row.email
|
|
182
|
+
? await this._members.get({ email: row.email }, { ...options, withRelated: ['labels', 'newsletters'] })
|
|
183
|
+
: null;
|
|
184
|
+
let member;
|
|
185
|
+
if (existingMember) {
|
|
186
|
+
const existingLabels = existingMember.related('labels').toJSON();
|
|
187
|
+
const existingNewsletters = existingMember.related('newsletters');
|
|
188
|
+
if (existingNewsletters.length > 0 && memberValues.subscribed) {
|
|
189
|
+
memberValues.newsletters = existingNewsletters.toJSON();
|
|
190
|
+
}
|
|
191
|
+
if (!existingNewsletters.length && memberValues.subscribed) {
|
|
192
|
+
memberValues.subscribed = false;
|
|
193
|
+
}
|
|
194
|
+
if (!row.name) {
|
|
195
|
+
memberValues.name = existingMember.name;
|
|
196
|
+
}
|
|
197
|
+
if (!row.note) {
|
|
198
|
+
memberValues.note = existingMember.note;
|
|
199
|
+
}
|
|
200
|
+
member = await this._members.update({
|
|
201
|
+
...memberValues,
|
|
202
|
+
labels: existingLabels.concat(memberValues.labels)
|
|
203
|
+
}, { ...options, id: existingMember.id });
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
member = await this._members.create(memberValues, Object.assign({}, options, { context: CREATE_CONTEXT }));
|
|
207
|
+
}
|
|
208
|
+
let importTierId;
|
|
209
|
+
if (row.import_tier) {
|
|
210
|
+
if (!tierIdCache.has(row.import_tier)) {
|
|
211
|
+
const tier = await this._tiers.getByName(row.import_tier);
|
|
212
|
+
tierIdCache.set(row.import_tier, tier ? tier.id.toString() : null);
|
|
213
|
+
}
|
|
214
|
+
importTierId = tierIdCache.get(row.import_tier);
|
|
215
|
+
if (!importTierId) {
|
|
216
|
+
throw new errors.DataImportError({ message: tpl(messages.invalidImportTier, { tier: row.import_tier }) });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (row.stripe_customer_id) {
|
|
220
|
+
let stripeCustomerId;
|
|
221
|
+
if (row.stripe_customer_id.toLowerCase() === 'auto') {
|
|
222
|
+
stripeCustomerId = row.email ? await this._members.getCustomerIdByEmail(row.email) : undefined;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
stripeCustomerId = row.stripe_customer_id;
|
|
226
|
+
}
|
|
227
|
+
if (stripeCustomerId) {
|
|
228
|
+
if (row.import_tier) {
|
|
229
|
+
const { isNewStripePrice, stripePriceId } = await this._stripe.forceStripeSubscriptionToProduct({
|
|
230
|
+
customer_id: stripeCustomerId,
|
|
231
|
+
product_id: importTierId
|
|
232
|
+
}, options);
|
|
233
|
+
if (isNewStripePrice) {
|
|
234
|
+
archivableStripePriceIds.push(stripePriceId);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
await this._members.linkStripeCustomer({ customer_id: stripeCustomerId, member_id: member.id }, options);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else if (row.complimentary_plan) {
|
|
241
|
+
const products = [];
|
|
242
|
+
if (row.import_tier) {
|
|
243
|
+
products.push({ id: importTierId });
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
products.push({ id: defaultTier.id.toString() });
|
|
247
|
+
}
|
|
248
|
+
await this._members.update({ products }, { ...options, id: member.id });
|
|
249
|
+
}
|
|
250
|
+
else if (row.import_tier) {
|
|
251
|
+
throw new errors.DataImportError({ message: tpl(messages.freeMemberNotAllowedImportTier) });
|
|
252
|
+
}
|
|
253
|
+
if (row.gift_id) {
|
|
254
|
+
try {
|
|
255
|
+
await this._gifts.reassignRedeemer(row.gift_id, member.id, { transacting: trx });
|
|
256
|
+
}
|
|
257
|
+
catch (giftError) {
|
|
258
|
+
throw wrapGiftError(giftError);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// On the row's transaction, so the values commit or roll back with the member.
|
|
262
|
+
await this._customFields.applyWrite(member.id, customFieldPlan, trx);
|
|
263
|
+
await trx.commit();
|
|
264
|
+
imported += 1;
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
const errorList = Array.isArray(error) ? error : [error];
|
|
268
|
+
const errorMessage = errorList
|
|
269
|
+
.map(e => (typeof e === 'object' && e !== null && 'message' in e ? e.message : undefined))
|
|
270
|
+
.join(', ');
|
|
271
|
+
// trx is unset if the row failed before the transaction opened (a bad
|
|
272
|
+
// custom field value or gift combination).
|
|
273
|
+
if (trx) {
|
|
274
|
+
await trx.rollback();
|
|
275
|
+
}
|
|
276
|
+
importErrors.push({ ...row, error: errorMessage });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
await Promise.all(archivableStripePriceIds.map(id => this._stripe.archivePrice(id)));
|
|
280
|
+
metrics.metric('members-import', { imported, errors: importErrors.length, value: Date.now() - performStart });
|
|
281
|
+
if (imported > 0) {
|
|
282
|
+
// The import label exists now that members carry it, so fetch it to report.
|
|
283
|
+
// Guarded against a null lookup in case a row is ever counted without it.
|
|
284
|
+
const importLabelModel = await this._members.getImportLabel(labelName);
|
|
285
|
+
return { imported, errors: importErrors, importLabel: importLabelModel?.toJSON() };
|
|
286
|
+
}
|
|
287
|
+
return { imported: 0, errors: importErrors };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function wrapGiftError(error) {
|
|
291
|
+
const err = error;
|
|
292
|
+
const message = (err && typeof err.message === 'string' && err.message) || tpl(messages.giftReassignFailed);
|
|
293
|
+
return new errors.DataImportError({
|
|
294
|
+
message: `Member cannot be assigned to a gift: ${message}`
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
exports.default = MembersCSVImporter;
|