ghost 6.54.1 → 6.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
- package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
- package/components/tryghost-kg-default-nodes-2.1.4.tgz +0 -0
- package/components/tryghost-kg-default-transforms-1.3.3.tgz +0 -0
- package/components/tryghost-kg-html-to-lexical-1.3.3.tgz +0 -0
- package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
- package/components/tryghost-nql-string-0.0.0.tgz +0 -0
- package/core/boot.js +3 -6
- package/core/built/admin/assets/{Combination-CJG7Flp-.js → Combination-BCm9FL4o.js} +1 -1
- package/core/built/admin/assets/{PolarAngleAxis-BGjf7sBu.js → PolarAngleAxis-C4qtHy1B.js} +1 -1
- package/core/built/admin/assets/{RadialBarChart-zeFf9mBz.js → RadialBarChart-Bh09Stsj.js} +1 -1
- package/core/built/admin/assets/{account-migration-BklF2I_I.js → account-migration-D3O_yVck.js} +1 -1
- package/core/built/admin/assets/activitypub/{account-migration-CNovr6W-.js → account-migration-D8FbP-Z-.js} +7 -7
- package/core/built/admin/assets/activitypub/activitypub.js +6 -6
- package/core/built/admin/assets/activitypub/{avatar-B3DEDB1l.js → avatar-11EA1VSs.js} +2 -2
- package/core/built/admin/assets/activitypub/{avatar-flipboard-Cqrg0XMx.js → avatar-flipboard-jfbLG-xe.js} +2 -2
- package/core/built/admin/assets/activitypub/{bluesky-sharing-Ceslb1_M.js → bluesky-sharing-f1jTQe2P.js} +16 -16
- package/core/built/admin/assets/activitypub/{content-formatters-BpQARh28.js → content-formatters-nmTOPY9M.js} +3 -3
- package/core/built/admin/assets/activitypub/{copy-D9UWIAan.js → copy-BCwo9ep3.js} +2 -2
- package/core/built/admin/assets/activitypub/{dist-DHAjBcV3.js → dist-BEfX-0aW.js} +12 -12
- package/core/built/admin/assets/activitypub/{domain-C1qQvJ8C.js → domain-AaDlZlrA.js} +9 -9
- package/core/built/admin/assets/activitypub/{edit-profile-BcH-jEfM.js → edit-profile-DPi8Tl5M.js} +945 -945
- package/core/built/admin/assets/activitypub/{explore-AxwBh9dI.js → explore-CxTzjILQ.js} +8 -8
- package/core/built/admin/assets/activitypub/{feed-BaOlbh7a.js → feed-DPv5x2MX.js} +33 -33
- package/core/built/admin/assets/activitypub/{field-B7mYYQpN.js → field-DbgA8lat.js} +3 -3
- package/core/built/admin/assets/activitypub/{hash-BC9Q0N4c.js → hash-BWuZoYkA.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-DQ4ZnC1m.js → inbox-35VDQ2K2.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-vXez-5uJ.js → inbox-DSrF_i1O.js} +85 -85
- package/core/built/admin/assets/activitypub/{label-Chv5T8bw.js → label-CPjFBbo7.js} +3 -3
- package/core/built/admin/assets/activitypub/layout-CTgrezN7.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-DtSM2i1I.js → moderation-BkDC1XVZ.js} +11 -11
- package/core/built/admin/assets/activitypub/{note-Dckp4S-2.js → note-xEyH4xT-.js} +7 -7
- package/core/built/admin/assets/activitypub/{notifications-CG5T22vg.js → notifications-DClaL3ui.js} +9 -9
- package/core/built/admin/assets/activitypub/{preferences-D_799jdl.js → preferences-DpnOa-4Z.js} +20 -20
- package/core/built/admin/assets/activitypub/{profile-D2JfXBQl.js → profile-Dvi9mz-6.js} +237 -237
- package/core/built/admin/assets/activitypub/{reply-CpdMeAO5.js → reply-Cs9ySd6Z.js} +2 -2
- package/core/built/admin/assets/activitypub/{routes-CCHe2dQp.js → routes-DFBeTnfn.js} +1139 -1150
- package/core/built/admin/assets/activitypub/{separator-BI1FLsYM.js → separator-C5u_VjzY.js} +3 -3
- package/core/built/admin/assets/activitypub/{settings-kx-9Enkz.js → settings-CV2fFu6D.js} +13 -13
- package/core/built/admin/assets/activitypub/{site-x4_IjVJ0.js → site-_TgDd_EM.js} +81 -81
- package/core/built/admin/assets/activitypub/{step-1-DIDg4aHb.js → step-1-BfnNL2oF.js} +5 -5
- package/core/built/admin/assets/activitypub/{step-2-BfGe8BYt.js → step-2-DLndZkO-.js} +6 -6
- package/core/built/admin/assets/activitypub/{step-3-mE9spAqw.js → step-3-A-PZTtRE.js} +10 -10
- package/core/built/admin/assets/activitypub/{tabs-CyPRQQ1N.js → tabs-BUbz7tea.js} +321 -319
- package/core/built/admin/assets/activitypub/{topic-filter-DLHBZT93.js → topic-filter-BdX_xYYT.js} +3 -3
- package/core/built/admin/assets/activitypub/{use-activity-pub-queries-VzbN99u_.js → use-activity-pub-queries-EwuRemHh.js} +2 -2
- package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-CAwXTmqH.js → use-navigate-with-base-path-cy4ACBc4.js} +92 -90
- package/core/built/admin/assets/activitypub/{use-reply-chain-data-BAB6fNGt.js → use-reply-chain-data-CyFcGfan.js} +5 -5
- package/core/built/admin/assets/activitypub/{x-C8tAnXly.js → x-7zk8mV2p.js} +2 -2
- package/core/built/admin/assets/audience-CkBb2LlO.js +1 -0
- package/core/built/admin/assets/{automated-emails-of1MvVsv.js → automated-emails-6t2bIRWf.js} +2 -2
- package/core/built/admin/assets/{automations-B7-y3dXt.js → automations-DhKeS_U5.js} +1 -1
- package/core/built/admin/assets/{automations-DRFjW5fw.js → automations-cMllU545.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-BbbjWdzs.js → bluesky-sharing-DPGpyD-G.js} +1 -1
- package/core/built/admin/assets/{box-BWbhXh21.js → box-By_EgGtm.js} +1 -1
- package/core/built/admin/assets/{breadcrumb-B-M9Ip_2.js → breadcrumb-DIfcF5ow.js} +1 -1
- package/core/built/admin/assets/build-CsiH26uS.js +1 -0
- package/core/built/admin/assets/{card-DGoDJzrn.js → card-aUHX0hyR.js} +1 -1
- package/core/built/admin/assets/{chart-BuRT2Y6p.js → chart-BnXyfH11.js} +1 -1
- package/core/built/admin/assets/{chart-helpers-xiSsW8r0.js → chart-helpers-DV0FkdcD.js} +1 -1
- package/core/built/admin/assets/{checkbox-EhJKHFdZ.js → checkbox-BIl077a0.js} +1 -1
- package/core/built/admin/assets/{chunk.361.3cdb584b973959e52ccc.js → chunk.332.05991a101d67f732dec3.js} +16 -12
- package/core/built/admin/assets/chunk.524.87b52f4fada529df2c4c.js +25 -0
- package/core/built/admin/assets/chunk.582.340bcca873e806cbd4bb.js +27 -0
- package/core/built/admin/assets/{code-editor-view-CZtJFGhR.js → code-editor-view-C0aWNwdl.js} +1 -1
- package/core/built/admin/assets/comments-qwhIjH3c.js +1 -0
- package/core/built/admin/assets/{container-BYz2Ed39.js → container-LiZf3XdY.js} +1 -1
- package/core/built/admin/assets/{data-list-16rkJYYi.js → data-list-CupF7bjM.js} +1 -1
- package/core/built/admin/assets/{date-range-select-CTlyo8zW.js → date-range-select-dXc-GsVe.js} +1 -1
- package/core/built/admin/assets/{disabled-sources-indicator-qs2GfLvl.js → disabled-sources-indicator-DYAXW-6V.js} +1 -1
- package/core/built/admin/assets/dist-BUKmD51u.js +1 -0
- package/core/built/admin/assets/{dist-2Tnz2Yfq.js → dist-CNlMj7ML.js} +1 -1
- package/core/built/admin/assets/{dist-DGzPJ_bO.js → dist-Cb5RSHSg.js} +1 -1
- package/core/built/admin/assets/{dist-P_ohUlTi.js → dist-CqO2lWS0.js} +1 -1
- package/core/built/admin/assets/{domain-CPNXKVOV.js → domain-B5ogXUBa.js} +1 -1
- package/core/built/admin/assets/{dropzone-BmCXnw6J.js → dropzone-BnFG7dDP.js} +1 -1
- package/core/built/admin/assets/{edit-profile-SGrJve5M.js → edit-profile-B07jCZy9.js} +1 -1
- package/core/built/admin/assets/editor-DDYSOXGN.js +8 -0
- package/core/built/admin/assets/{empty-indicator-oX1u1Pkn.js → empty-indicator-WB1WgT9h.js} +1 -1
- package/core/built/admin/assets/{explore-CUYLv9G9.js → explore-Ca1y8HW_.js} +1 -1
- package/core/built/admin/assets/{feed-CPdJMttL.js → feed-x4iFMBP4.js} +1 -1
- package/core/built/admin/assets/field-ChCAabZg.js +1 -0
- package/core/built/admin/assets/filter-sources-LdtH1e-T.js +1 -0
- package/core/built/admin/assets/{filters-bXkkWro8.js → filters-CLehPo_D.js} +1 -1
- package/core/built/admin/assets/filters-CjVKRP8U.js +1 -0
- package/core/built/admin/assets/{ghost-05f9a7182ed56903dba05878a5b8422b.js → ghost-7f01131115186d1af84ead76e79646dd.js} +275 -255
- package/core/built/admin/assets/{ghost-3063eae29e6c9cc6d6e7131928a27da0.css → ghost-d38c30b866e59f2e06a357b5eb844fe5.css} +1 -1
- package/core/built/admin/assets/{ghost-dark-c8d0ab7f515b2010c173e23e9b32ea8a.css → ghost-dark-1bffd5099ba7a749303133efacf8480b.css} +1 -1
- package/core/built/admin/assets/gift-link-modal-Buqh2vXh.js +1 -0
- package/core/built/admin/assets/{gift-links-nBxJjZiQ.js → gift-links-CsUhefWG.js} +1 -1
- package/core/built/admin/assets/growth-DajOBdN0.js +1 -0
- package/core/built/admin/assets/growth-Dy2UOSay.js +3 -0
- package/core/built/admin/assets/{hooks-BApwwBE0.js → hooks-CixO01lL.js} +1 -1
- package/core/built/admin/assets/{inbox-Xx1CdnfO.js → inbox-BUOLDoji.js} +13 -13
- package/core/built/admin/assets/index-08n05-rc.js +67 -0
- package/core/built/admin/assets/index-DgUzgFvS.css +1 -0
- package/core/built/admin/assets/{inline-EMAm3okk.js → inline-Cw-gigDW.js} +1 -1
- package/core/built/admin/assets/input-group-uSK3XT7b.js +1 -0
- package/core/built/admin/assets/{input-surface-BpAjZXY5.js → input-surface-BdOe4KzO.js} +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +9132 -9087
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +121 -121
- package/core/built/admin/assets/koenig-lexical/style.css +1 -1
- package/core/built/admin/assets/{koenig-lexical-B61IALf5.js → koenig-lexical-BFc4yFed.js} +82 -82
- package/core/built/admin/assets/{kpi-card-DCdlyaSS.js → kpi-card-BN-E6WYX.js} +1 -1
- package/core/built/admin/assets/{kpi-card-BlZaG1Lo.js → kpi-card-CCt-qeWD.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-DpAuS1Nb.js → kpi-helpers-DlxTbTs4.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-Dme44bQS.js → kpi-tabs-BlcUttpn.js} +1 -1
- package/core/built/admin/assets/{label-dSs5aepE.js → label-AVrxwmsz.js} +1 -1
- package/core/built/admin/assets/label-picker-DVnpu_IF.js +1 -0
- package/core/built/admin/assets/labels-DAIqKed7.js +1 -0
- package/core/built/admin/assets/layout-B0YLh7R9.js +1 -0
- package/core/built/admin/assets/link-helpers-D9LsDOkG.js +1 -0
- package/core/built/admin/assets/{list-page-B6ANtzkd.js → list-page-CdsnQR9N.js} +1 -1
- package/core/built/admin/assets/member-custom-fields-C8NpZYza.js +1 -0
- package/core/built/admin/assets/member-detail-CIVAdKXf.js +1 -0
- package/core/built/admin/assets/members-oLPiSkXD.js +10 -0
- package/core/built/admin/assets/modals-CB7v-k3T.js +31 -0
- package/core/built/admin/assets/moderation-CQ3urTFE.js +1 -0
- package/core/built/admin/assets/newsletter-rbeDX9Wk.js +1 -0
- package/core/built/admin/assets/{newsletters-D1jbtV-h.js → newsletters-BXJbJgA-.js} +1 -1
- package/core/built/admin/assets/newsletters-vt3Ni1Ov.js +1 -0
- package/core/built/admin/assets/note-Dk877BTv.js +1 -0
- package/core/built/admin/assets/notifications-CPlLjPxO.js +1 -0
- package/core/built/admin/assets/{offers-CrY6nSRR.js → offers-DEniVRD5.js} +1 -1
- package/core/built/admin/assets/onboarding-route-DdStOtjy.js +1 -0
- package/core/built/admin/assets/overview--4V-kx9c.js +1 -0
- package/core/built/admin/assets/overview-DSLv30oz.js +1 -0
- package/core/built/admin/assets/{page-header-op0eN36I.js → page-header-pp4dWmOS.js} +1 -1
- package/core/built/admin/assets/{pages-BbNWFcGj.js → pages-B4goRPUn.js} +1 -1
- package/core/built/admin/assets/{post-analytics-WwRDEnrV.js → post-analytics-BMJrI81R.js} +1 -1
- package/core/built/admin/assets/post-analytics-header-pU2m9ZuV.js +1 -0
- package/core/built/admin/assets/post-analytics-provider-Btml5vJI.js +1 -0
- package/core/built/admin/assets/{post-share-modal-Dx-MEjhI.js → post-share-modal-AcUZkbfL.js} +1 -1
- package/core/built/admin/assets/{posts-EWpZIjkr.js → posts-DhXE7b6r.js} +1 -1
- package/core/built/admin/assets/preferences-Ru4CRPHC.js +5 -0
- package/core/built/admin/assets/profile-BNb1oj7D.js +1 -0
- package/core/built/admin/assets/{referrers-BCfFYSHc.js → referrers-pmJT4k3-.js} +1 -1
- package/core/built/admin/assets/{router-provider-C1R4AZ2o.js → router-provider-BmEZf6c6.js} +1 -1
- package/core/built/admin/assets/select-B0HteHrp.js +1 -0
- package/core/built/admin/assets/settings-B1C9Q3wo.js +4 -0
- package/core/built/admin/assets/{settings-H6HxFgfI.js → settings-BGgk7oCI.js} +1 -1
- package/core/built/admin/assets/{shade-app-B3heOAfz-CGXQwWeO.js → shade-app-B3heOAfz-BhQXxNkT.js} +1 -1
- package/core/built/admin/assets/{share-modal-BJCadJyp.js → share-modal-BeuydaBf.js} +1 -1
- package/core/built/admin/assets/social-urls-DA2lXrhW.js +8 -0
- package/core/built/admin/assets/{sort-button-DyYlfYp7.js → sort-button-Df4t2AL8.js} +1 -1
- package/core/built/admin/assets/{source-icon-_ubRDbg_.js → source-icon-ajoaKb41.js} +1 -1
- package/core/built/admin/assets/{stack-xiN0eQ6Z.js → stack-CIfOAAVf.js} +1 -1
- package/core/built/admin/assets/{stats-BQZMGMvn.js → stats-C_lfxZAw.js} +1 -1
- package/core/built/admin/assets/{stats-view-Doei-qCX.js → stats-view-Cxot1iD8.js} +1 -1
- package/core/built/admin/assets/{step-1-IKiLBPoC.js → step-1-ogAA4lDr.js} +1 -1
- package/core/built/admin/assets/{step-2-jJ41PsDN.js → step-2-C5Xini-7.js} +1 -1
- package/core/built/admin/assets/{step-3-CaqFAEqf.js → step-3-CZv1FV81.js} +2 -2
- package/core/built/admin/assets/{switch-t-WyENU2.js → switch-CyQcwMAv.js} +1 -1
- package/core/built/admin/assets/{table-DxUL78jU.js → table-DFj1wiYv.js} +1 -1
- package/core/built/admin/assets/tabs-D4y1gqUd.js +1 -0
- package/core/built/admin/assets/{tags-CNujGwkT.js → tags-Dmft-Y4O.js} +1 -1
- package/core/built/admin/assets/{text-DwtjwGkh.js → text-DU9Hv33_.js} +1 -1
- package/core/built/admin/assets/textarea-d9qEkMOP.js +1 -0
- package/core/built/admin/assets/{tiers-BgYLWRJv.js → tiers-DYsa4DCo.js} +1 -1
- package/core/built/admin/assets/{toggle-group-BeEpQ69p.js → toggle-group-R2QMFvXW.js} +1 -1
- package/core/built/admin/assets/{topic-filter-Bb5MOD-1.js → topic-filter-DGDxDalo.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-Cds6XDjg.js → use-analytics-data-C76v4LIp.js} +1 -1
- package/core/built/admin/assets/use-filter-params-WrdhAISv.js +1 -0
- package/core/built/admin/assets/{use-growth-stats-D8uf98-G.js → use-growth-stats-aNXJOmQr.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-CRtgyuXB.js → use-post-referrers-CG0SZioj.js} +1 -1
- package/core/built/admin/assets/{use-reply-chain-data-DbKYX50v.js → use-reply-chain-data-RLdPKRmS.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-DC01kpo0.js → use-simple-pagination-DgcF69KI.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-query-C7bQRJWw.js → use-tinybird-query-C1R_Kbmy.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-Az4tam7_.js → use-tinybird-token-C9fPjVM_.js} +1 -1
- package/core/built/admin/assets/{users-CcSCQFcQ.js → users-Dkv8NQZe.js} +1 -1
- package/core/built/admin/assets/{virtual-list-Dhuh9fGx.js → virtual-list-BmTUzeoF.js} +1 -1
- package/core/built/admin/assets/web-BqW0b0_C.js +1 -0
- package/core/built/admin/assets/web-Cy5GKMWM.js +2 -0
- package/core/built/admin/index.html +18 -18
- package/core/frontend/services/data/fetch-data.js +10 -5
- package/core/frontend/services/proxy.js +7 -8
- package/core/frontend/services/routing/api-adapter.js +115 -0
- package/core/frontend/services/routing/api-adapter.ts +157 -0
- package/core/frontend/services/routing/collection-router.js +5 -3
- package/core/frontend/services/routing/controllers/index.js +1 -1
- package/core/frontend/services/routing/controllers/rss.js +26 -31
- package/core/frontend/services/routing/controllers/rss.ts +66 -0
- package/core/frontend/services/routing/controllers/static.js +8 -7
- package/core/frontend/services/routing/parent-router.js +17 -4
- package/core/frontend/services/routing/permalink-adapter.js +26 -0
- package/core/frontend/services/routing/permalink-adapter.ts +23 -0
- package/core/frontend/services/routing/registry.js +59 -33
- package/core/frontend/services/routing/router-manager.js +14 -18
- package/core/frontend/services/routing/static-routes-router.js +11 -13
- package/core/frontend/services/routing/taxonomy-router.js +6 -2
- 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/index.js +4 -0
- package/core/server/api/endpoints/settings.js +0 -2
- package/core/server/api/endpoints/utils/serializers/output/utils/url.js +11 -1
- 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/migrations/versions/6.56/2026-08-03-19-27-51-add-automation-run-step-id-to-automated-email-recipients.js +8 -0
- package/core/server/data/migrations/versions/6.56/2026-08-04-14-42-39-restrict-notification-add-permission-to-administrators.js +12 -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/fixtures/fixtures.json +2 -2
- package/core/server/data/schema/schema.js +6 -1
- package/core/server/models/post.js +10 -1
- package/core/server/services/automations/automations-api.js +15 -0
- package/core/server/services/automations/automations-api.ts +19 -0
- package/core/server/services/automations/automations-repository.ts +20 -0
- package/core/server/services/automations/database-automations-repository.js +132 -30
- package/core/server/services/automations/database-automations-repository.ts +141 -12
- 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/get-scheduler-idempotency-key.js +13 -0
- package/core/server/services/automations/get-scheduler-idempotency-key.ts +8 -0
- package/core/server/services/automations/poll.js +7 -2
- package/core/server/services/automations/poll.ts +10 -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 +19 -3
- package/core/server/services/automations/service.ts +20 -3
- package/core/server/services/comments/comments-service-emails.js +10 -7
- 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/gift-links/CONTEXT.md +9 -0
- package/core/server/services/gifts/CONTEXT.md +55 -0
- package/core/server/services/gifts/README.md +28 -0
- package/core/server/services/gifts/gift-bookshelf-repository.js +65 -45
- package/core/server/services/gifts/gift-bookshelf-repository.ts +203 -74
- package/core/server/services/gifts/gift-checkout-adapter.js +59 -0
- package/core/server/services/gifts/gift-checkout-offer.js +74 -0
- package/core/server/services/gifts/gift-checkout-offer.ts +127 -0
- package/core/server/services/gifts/gift-codec.js +63 -0
- package/core/server/services/gifts/gift-codec.ts +62 -0
- package/core/server/services/gifts/gift-controller.js +6 -31
- package/core/server/services/gifts/gift-controller.ts +6 -69
- package/core/server/services/gifts/gift-email-service.ts +3 -2
- package/core/server/services/gifts/gift-schema.js +34 -0
- package/core/server/services/gifts/gift-schema.ts +39 -0
- package/core/server/services/gifts/gift-service-wrapper.js +13 -11
- package/core/server/services/gifts/gift-service.js +265 -52
- package/core/server/services/gifts/gift-service.ts +431 -97
- package/core/server/services/gifts/gift.ts +16 -39
- 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 +35 -9
- package/core/server/services/link-tracking/link-click-tracking-service.js +54 -4
- package/core/server/services/member-attribution/outbound-link-tagger.js +3 -1
- package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +20 -4
- package/core/server/services/member-welcome-emails/service.js +15 -3
- package/core/server/services/members/api.js +1 -2
- 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 +3 -1
- package/core/server/services/members/import-export/csv/index.ts +1 -0
- package/core/server/services/members/import-export/import/completion-email.js +22 -3
- package/core/server/services/members/import-export/import/completion-email.ts +29 -7
- package/core/server/services/members/import-export/import/importer.js +25 -4
- package/core/server/services/members/import-export/import/importer.ts +44 -5
- package/core/server/services/members/import-export/index.js +18 -1
- package/core/server/services/members/import-export/index.ts +32 -3
- package/core/server/services/members/members-api/controllers/router-controller.js +38 -36
- package/core/server/services/members/members-api/members-api.js +13 -7
- package/core/server/services/members/members-api/repositories/event-repository.js +4 -94
- package/core/server/services/members/members-api/services/member-bread-service.js +3 -3
- package/core/server/services/members/members-api/services/payments-service.js +6 -68
- package/core/server/services/members/service.js +8 -1
- package/core/server/services/members-custom-fields/schema.js +8 -1
- package/core/server/services/members-custom-fields/schema.ts +8 -0
- package/core/server/services/members-custom-fields/values-service.js +136 -8
- package/core/server/services/members-custom-fields/values-service.ts +152 -10
- package/core/server/services/mentions/service.js +5 -1
- package/core/server/services/oembed/oembed-service.js +44 -3
- package/core/server/services/posts/posts-service.js +11 -0
- package/core/server/services/route-settings/dynamic-routing-service.js +21 -52
- package/core/server/services/route-settings/index.js +1 -16
- package/core/server/services/route-settings/route-settings-parser.js +25 -25
- package/core/server/services/route-settings/route-settings-parser.ts +27 -27
- package/core/server/services/route-settings/validation-errors.js +68 -4
- package/core/server/services/route-settings/validation-errors.ts +79 -4
- package/core/server/services/settings/settings-service.js +0 -18
- package/core/server/services/stats/posts-stats-service.js +27 -23
- package/core/server/services/stripe/services/webhook/charge-refunded-event-service.js +1 -1
- package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +16 -4
- package/core/server/services/stripe/stripe-api.js +10 -0
- 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/server/web/gift-preview/controller.js +10 -25
- package/core/shared/labs.js +2 -2
- package/package.json +8 -7
- package/pnpm-lock.yaml +28 -18
- package/pnpm-workspace.yaml +6 -4
- package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
- package/core/built/admin/assets/Growth-BTP1LthE.js +0 -3
- package/core/built/admin/assets/Newsletters-CSEX0myc.js +0 -1
- package/core/built/admin/assets/Overview-BjACEsJF.js +0 -1
- package/core/built/admin/assets/Web-CxxxjCcv.js +0 -2
- package/core/built/admin/assets/activitypub/layout-CziktJXo.js +0 -1
- package/core/built/admin/assets/audience-X_XsZivI.js +0 -1
- package/core/built/admin/assets/chunk.524.8365cd9280405c0f6216.js +0 -25
- package/core/built/admin/assets/chunk.582.61d078937e6102769f2c.js +0 -27
- package/core/built/admin/assets/comments-CvyXxOyN.js +0 -1
- package/core/built/admin/assets/constants-c-JNF9c2.js +0 -1
- package/core/built/admin/assets/dist-CjY-f5Z1.js +0 -1
- package/core/built/admin/assets/editor-BOG2YPUm.js +0 -8
- package/core/built/admin/assets/field-D0XISCK2.js +0 -1
- package/core/built/admin/assets/filter-sources-Bieps75g.js +0 -1
- package/core/built/admin/assets/filters-CuB-A7ma.js +0 -1
- package/core/built/admin/assets/gift-link-modal-su8MsRdg.js +0 -1
- package/core/built/admin/assets/growth-Ba6S5FQX.js +0 -1
- package/core/built/admin/assets/index-D8PbzuT9.css +0 -1
- package/core/built/admin/assets/index-ukW_0Uie.js +0 -67
- package/core/built/admin/assets/input-group-CGOXcy4l.js +0 -1
- package/core/built/admin/assets/label-picker-Uq91atex.js +0 -1
- package/core/built/admin/assets/labels-Cyayfd-e.js +0 -1
- package/core/built/admin/assets/layout-WUcz6tu1.js +0 -1
- package/core/built/admin/assets/link-helpers-tOlxmuH5.js +0 -1
- package/core/built/admin/assets/member-custom-fields-NCzrMuba.js +0 -1
- package/core/built/admin/assets/member-detail-DPWDtYnN.js +0 -1
- package/core/built/admin/assets/members-B9rO7qr4.js +0 -10
- package/core/built/admin/assets/modals-CxRaan1J.js +0 -31
- package/core/built/admin/assets/moderation-D8zon60E.js +0 -1
- package/core/built/admin/assets/multi-select-combobox-1wYm3nle.js +0 -1
- package/core/built/admin/assets/newsletter-DbYMRxRS.js +0 -1
- package/core/built/admin/assets/note-PiWPnnrh.js +0 -1
- package/core/built/admin/assets/notifications-DEgqqB9G.js +0 -1
- package/core/built/admin/assets/nql-BsgFMd93.js +0 -1
- package/core/built/admin/assets/onboarding-route-BzF6HeVn.js +0 -1
- package/core/built/admin/assets/overview-DixLeIqY.js +0 -1
- package/core/built/admin/assets/post-analytics-header-DsMoLx0R.js +0 -1
- package/core/built/admin/assets/post-analytics-provider-BJCPLMIb.js +0 -1
- package/core/built/admin/assets/preferences-D7-j6A4C.js +0 -5
- package/core/built/admin/assets/profile-Dl69QPp0.js +0 -1
- package/core/built/admin/assets/select-C1qDZnW4.js +0 -1
- package/core/built/admin/assets/settings-BbmC_Iqq.js +0 -4
- package/core/built/admin/assets/social-urls-BPL1nADp.js +0 -8
- package/core/built/admin/assets/tabs-BQcJQ1ke.js +0 -1
- package/core/built/admin/assets/textarea-Bjccvh_E.js +0 -1
- package/core/built/admin/assets/use-filter-params-DjXkhO54.js +0 -1
- package/core/built/admin/assets/web-LtHNiNBV.js +0 -1
- package/core/server/services/gifts/gift-repository.js +0 -2
- package/core/server/services/gifts/gift-repository.ts +0 -29
- package/core/server/services/route-settings/activation-bridge.js +0 -164
- package/core/server/services/route-settings/activation-bridge.ts +0 -210
- 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
|
@@ -368,10 +368,13 @@ class MemberWelcomeEmailService {
|
|
|
368
368
|
* @param {null | object} options.email.designSettings
|
|
369
369
|
* @param {'welcome' | 'automation'} options.emailType
|
|
370
370
|
* @param {boolean} [options.trackOpens]
|
|
371
|
+
* @param {boolean} [options.trackClicks]
|
|
372
|
+
* @param {string | null} [options.automationActionRevisionId]
|
|
373
|
+
* @param {string | null} [options.automationRunStepId]
|
|
371
374
|
* @param {null | {url: string, oneClickUrl: string}} [options.unsubscribe] - When set, the footer links to an unsubscribe URL and the email carries one-click List-Unsubscribe headers
|
|
372
375
|
* @returns {Promise<unknown>}
|
|
373
376
|
*/
|
|
374
|
-
async #sendEmail({member, memberStatus, email, emailType, trackOpens, unsubscribe = null}) {
|
|
377
|
+
async #sendEmail({member, memberStatus, email, emailType, trackOpens, trackClicks = false, automationActionRevisionId = null, automationRunStepId = null, unsubscribe = null}) {
|
|
375
378
|
if (!member.email) {
|
|
376
379
|
throw new errors.IncorrectUsageError({
|
|
377
380
|
message: MESSAGES.MISSING_RECIPIENT_EMAIL
|
|
@@ -396,7 +399,10 @@ class MemberWelcomeEmailService {
|
|
|
396
399
|
uuid: member.uuid
|
|
397
400
|
},
|
|
398
401
|
siteSettings: this.#getSiteSettings(),
|
|
399
|
-
unsubscribeUrl: unsubscribe?.url
|
|
402
|
+
unsubscribeUrl: unsubscribe?.url,
|
|
403
|
+
trackClicks,
|
|
404
|
+
automationActionRevisionId,
|
|
405
|
+
automationRunStepId
|
|
400
406
|
});
|
|
401
407
|
|
|
402
408
|
const senderOptions = await this.#getEffectiveSenderOptions(
|
|
@@ -474,9 +480,12 @@ class MemberWelcomeEmailService {
|
|
|
474
480
|
* @param {string} options.member.uuid
|
|
475
481
|
* @param {'free' | 'paid'} options.memberStatus
|
|
476
482
|
* @param {boolean} options.trackOpens
|
|
483
|
+
* @param {boolean} options.trackClicks
|
|
484
|
+
* @param {string} options.automationActionRevisionId
|
|
485
|
+
* @param {string} options.automationRunStepId
|
|
477
486
|
* @returns {Promise<unknown>}
|
|
478
487
|
*/
|
|
479
|
-
async sendAutomationEmail({email, member, memberStatus, trackOpens}) {
|
|
488
|
+
async sendAutomationEmail({email, member, memberStatus, trackOpens, trackClicks, automationActionRevisionId, automationRunStepId}) {
|
|
480
489
|
const designSettings = email.designSettingId ?
|
|
481
490
|
await EmailDesignSetting.findOne({id: email.designSettingId}) :
|
|
482
491
|
null;
|
|
@@ -502,6 +511,9 @@ class MemberWelcomeEmailService {
|
|
|
502
511
|
designSettings: designSettingsJson
|
|
503
512
|
},
|
|
504
513
|
trackOpens,
|
|
514
|
+
trackClicks,
|
|
515
|
+
automationActionRevisionId,
|
|
516
|
+
automationRunStepId,
|
|
505
517
|
unsubscribe
|
|
506
518
|
});
|
|
507
519
|
}
|
|
@@ -244,8 +244,7 @@ function createApiInstance(config) {
|
|
|
244
244
|
EmailSpamComplaintEvent: models.EmailSpamComplaintEvent,
|
|
245
245
|
Automation: models.Automation,
|
|
246
246
|
WelcomeEmailAutomationRun: models.WelcomeEmailAutomationRun,
|
|
247
|
-
AutomatedEmailRecipient: models.AutomatedEmailRecipient
|
|
248
|
-
Gift: models.Gift
|
|
247
|
+
AutomatedEmailRecipient: models.AutomatedEmailRecipient
|
|
249
248
|
},
|
|
250
249
|
stripeAPIService: stripeService.api,
|
|
251
250
|
tiersService: tiersService,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stripFormulaGuard = stripFormulaGuard;
|
|
4
|
+
// The export writes with papaparse's escapeFormulae on (see serialize), which prefixes a
|
|
5
|
+
// cell starting with one of these with an apostrophe so a spreadsheet won't read it as a
|
|
6
|
+
// formula: `=SUM(A1)` is written as `'=SUM(A1)`.
|
|
7
|
+
const FORMULA_TRIGGERS = ['=', '+', '-', '@', '\t', '\r'];
|
|
8
|
+
// Strip that guard back off, so a re-imported value doesn't gain an apostrophe on every
|
|
9
|
+
// round trip. Only a lone apostrophe before a trigger is the guard; one before anything
|
|
10
|
+
// else is a character the value actually starts with, and is left alone.
|
|
11
|
+
function stripFormulaGuard(cell) {
|
|
12
|
+
if (cell.charAt(0) === '\'' && FORMULA_TRIGGERS.includes(cell.charAt(1))) {
|
|
13
|
+
return cell.slice(1);
|
|
14
|
+
}
|
|
15
|
+
return cell;
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The export writes with papaparse's escapeFormulae on (see serialize), which prefixes a
|
|
2
|
+
// cell starting with one of these with an apostrophe so a spreadsheet won't read it as a
|
|
3
|
+
// formula: `=SUM(A1)` is written as `'=SUM(A1)`.
|
|
4
|
+
const FORMULA_TRIGGERS = ['=', '+', '-', '@', '\t', '\r'];
|
|
5
|
+
|
|
6
|
+
// Strip that guard back off, so a re-imported value doesn't gain an apostrophe on every
|
|
7
|
+
// round trip. Only a lone apostrophe before a trigger is the guard; one before anything
|
|
8
|
+
// else is a character the value actually starts with, and is left alone.
|
|
9
|
+
export function stripFormulaGuard(cell: string): string {
|
|
10
|
+
if (cell.charAt(0) === '\'' && FORMULA_TRIGGERS.includes(cell.charAt(1))) {
|
|
11
|
+
return cell.slice(1);
|
|
12
|
+
}
|
|
13
|
+
return cell;
|
|
14
|
+
}
|
|
@@ -3,8 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.serialize = exports.parse = void 0;
|
|
6
|
+
exports.stripFormulaGuard = exports.serialize = exports.parse = void 0;
|
|
7
7
|
var parse_1 = require("./parse");
|
|
8
8
|
Object.defineProperty(exports, "parse", { enumerable: true, get: function () { return __importDefault(parse_1).default; } });
|
|
9
9
|
var serialize_1 = require("./serialize");
|
|
10
10
|
Object.defineProperty(exports, "serialize", { enumerable: true, get: function () { return __importDefault(serialize_1).default; } });
|
|
11
|
+
var formula_1 = require("./formula");
|
|
12
|
+
Object.defineProperty(exports, "stripFormulaGuard", { enumerable: true, get: function () { return formula_1.stripFormulaGuard; } });
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.default = buildCompletionEmail;
|
|
4
4
|
const csv_1 = require("../csv");
|
|
5
|
+
const csv_2 = require("@tryghost/custom-field-types/csv");
|
|
5
6
|
const emailTemplate = require('./email-template');
|
|
6
7
|
// Turn an ORM validation error into copy a member manager can act on. Presentation
|
|
7
8
|
// only: the API response keeps the raw messages, and just the emailed report is
|
|
@@ -17,6 +18,14 @@ function humaniseError(message) {
|
|
|
17
18
|
function stringifyLabels(labels) {
|
|
18
19
|
return labels.map(label => (typeof label === 'string' ? label : label.name)).join(',');
|
|
19
20
|
}
|
|
21
|
+
// The custom_fields.* cells a submitted row carried, echoed untouched so a manager can
|
|
22
|
+
// fix a failed row and re-upload the values they mapped.
|
|
23
|
+
function customFieldCells(row) {
|
|
24
|
+
return Object.fromEntries(Object.entries(row).filter(([column]) => (0, csv_2.isCustomFieldColumn)(column)));
|
|
25
|
+
}
|
|
26
|
+
// Shape a failed import row into its fixed error-report cells, with the raw ORM message
|
|
27
|
+
// rewritten into copy the member manager can act on. Custom field cells are merged on by
|
|
28
|
+
// buildErrorReport, which owns the dynamic column set.
|
|
20
29
|
function toErrorReportRow(row) {
|
|
21
30
|
return {
|
|
22
31
|
id: row.id,
|
|
@@ -34,10 +43,20 @@ function toErrorReportRow(row) {
|
|
|
34
43
|
error: humaniseError(row.error)
|
|
35
44
|
};
|
|
36
45
|
}
|
|
37
|
-
// The error report attached to the completion email: the failed rows as CSV.
|
|
38
|
-
//
|
|
46
|
+
// The error report attached to the completion email: the failed rows as CSV. It shares
|
|
47
|
+
// the serialiser with the export but not the shaping -- the export writes db members,
|
|
48
|
+
// this echoes submitted rows. Member columns come from the shaper's keys (so the type
|
|
49
|
+
// stays the single source); the custom_fields.* columns across the rows are threaded in
|
|
50
|
+
// before the last error column, and each row's custom cells merged on.
|
|
39
51
|
function buildErrorReport(errors) {
|
|
40
|
-
|
|
52
|
+
if (errors.length === 0) {
|
|
53
|
+
return (0, csv_1.serialize)([]);
|
|
54
|
+
}
|
|
55
|
+
const memberColumns = Object.keys(toErrorReportRow(errors[0])).filter(column => column !== 'error');
|
|
56
|
+
const customColumns = [...new Set(errors.flatMap(row => Object.keys(customFieldCells(row))))];
|
|
57
|
+
const columns = [...memberColumns, ...customColumns, 'error'];
|
|
58
|
+
const rows = errors.map(row => ({ ...toErrorReportRow(row), ...customFieldCells(row) }));
|
|
59
|
+
return (0, csv_1.serialize)(rows, { columns });
|
|
41
60
|
}
|
|
42
61
|
// Compose the completion email for a finished import: the summary and its links,
|
|
43
62
|
// plus the attached error report. Owns how the outcome is presented, so the
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {serialize} from '../csv';
|
|
2
|
+
import {isCustomFieldColumn} from '@tryghost/custom-field-types/csv';
|
|
2
3
|
import type {MemberImportRow, ImportErrorRow, ImportLabel, Label} from './row';
|
|
3
4
|
|
|
4
5
|
const emailTemplate = require('./email-template');
|
|
@@ -43,10 +44,12 @@ function humaniseError(message: string): string {
|
|
|
43
44
|
.replace(/No such customer:[^,]*/, 'Could not find Stripe customer');
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
// One row of the
|
|
47
|
-
// these keys, so the report cannot drift from the
|
|
48
|
-
// here or the shaper stops compiling. tiers
|
|
49
|
-
// an import never fills; kept always-empty
|
|
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.
|
|
50
53
|
type ErrorReportRow = {
|
|
51
54
|
id: MemberImportRow['id'];
|
|
52
55
|
email: MemberImportRow['email'];
|
|
@@ -67,6 +70,15 @@ function stringifyLabels(labels: Array<string | Label>): string {
|
|
|
67
70
|
return labels.map(label => (typeof label === 'string' ? label : label.name)).join(',');
|
|
68
71
|
}
|
|
69
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.
|
|
70
82
|
function toErrorReportRow(row: ImportErrorRow): ErrorReportRow {
|
|
71
83
|
return {
|
|
72
84
|
id: row.id,
|
|
@@ -85,10 +97,20 @@ function toErrorReportRow(row: ImportErrorRow): ErrorReportRow {
|
|
|
85
97
|
};
|
|
86
98
|
}
|
|
87
99
|
|
|
88
|
-
// The error report attached to the completion email: the failed rows as CSV.
|
|
89
|
-
//
|
|
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.
|
|
90
105
|
function buildErrorReport(errors: ImportErrorRow[]): string {
|
|
91
|
-
|
|
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});
|
|
92
114
|
}
|
|
93
115
|
|
|
94
116
|
// Compose the completion email for a finished import: the summary and its links,
|
|
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const moment_timezone_1 = __importDefault(require("moment-timezone"));
|
|
7
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");
|
|
8
10
|
const metrics = require('@tryghost/metrics');
|
|
9
11
|
const errors = require('@tryghost/errors');
|
|
10
12
|
const tpl = require('@tryghost/tpl');
|
|
@@ -47,11 +49,12 @@ class MembersCSVImporter {
|
|
|
47
49
|
_tiers;
|
|
48
50
|
_stripe;
|
|
49
51
|
_gifts;
|
|
52
|
+
_customFields;
|
|
50
53
|
_email;
|
|
51
54
|
_addJob;
|
|
52
55
|
_getTimezone;
|
|
53
56
|
_getInlineThreshold;
|
|
54
|
-
constructor({ knex, readRows, spool, members, tiers, stripe, gifts, email, addJob, getTimezone, getInlineThreshold }) {
|
|
57
|
+
constructor({ knex, readRows, spool, members, tiers, stripe, gifts, customFields, email, addJob, getTimezone, getInlineThreshold }) {
|
|
55
58
|
this._knex = knex;
|
|
56
59
|
this._readRows = readRows;
|
|
57
60
|
this._spool = spool;
|
|
@@ -59,6 +62,7 @@ class MembersCSVImporter {
|
|
|
59
62
|
this._tiers = tiers;
|
|
60
63
|
this._stripe = stripe;
|
|
61
64
|
this._gifts = gifts;
|
|
65
|
+
this._customFields = customFields;
|
|
62
66
|
this._email = email;
|
|
63
67
|
this._addJob = addJob;
|
|
64
68
|
this._getTimezone = getTimezone;
|
|
@@ -132,6 +136,9 @@ class MembersCSVImporter {
|
|
|
132
136
|
const globalLabels = [importLabel, ...extraLabels];
|
|
133
137
|
const performStart = Date.now();
|
|
134
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();
|
|
135
142
|
const tierIdCache = new Map();
|
|
136
143
|
const archivableStripePriceIds = [];
|
|
137
144
|
// Copied per row: the member model stamps ids and trims names onto these in
|
|
@@ -143,8 +150,7 @@ class MembersCSVImporter {
|
|
|
143
150
|
let imported = 0;
|
|
144
151
|
const importErrors = [];
|
|
145
152
|
for (const row of rows) {
|
|
146
|
-
|
|
147
|
-
const options = { transacting: trx, context: IMPORT_CONTEXT };
|
|
153
|
+
let trx;
|
|
148
154
|
try {
|
|
149
155
|
if (row.gift_id) {
|
|
150
156
|
if (row.import_tier) {
|
|
@@ -154,6 +160,15 @@ class MembersCSVImporter {
|
|
|
154
160
|
throw wrapGiftError(new errors.DataImportError({ message: tpl(messages.giftCannotCombineWithComplimentary) }));
|
|
155
161
|
}
|
|
156
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 };
|
|
157
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;
|
|
158
173
|
const memberValues = {
|
|
159
174
|
email: row.email,
|
|
@@ -243,6 +258,8 @@ class MembersCSVImporter {
|
|
|
243
258
|
throw wrapGiftError(giftError);
|
|
244
259
|
}
|
|
245
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);
|
|
246
263
|
await trx.commit();
|
|
247
264
|
imported += 1;
|
|
248
265
|
}
|
|
@@ -251,7 +268,11 @@ class MembersCSVImporter {
|
|
|
251
268
|
const errorMessage = errorList
|
|
252
269
|
.map(e => (typeof e === 'object' && e !== null && 'message' in e ? e.message : undefined))
|
|
253
270
|
.join(', ');
|
|
254
|
-
|
|
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
|
+
}
|
|
255
276
|
importErrors.push({ ...row, error: errorMessage });
|
|
256
277
|
}
|
|
257
278
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import moment from 'moment-timezone';
|
|
2
2
|
import buildCompletionEmail from './completion-email';
|
|
3
|
+
import {stripFormulaGuard} from '../csv';
|
|
4
|
+
import {fieldValuesFromCsvRow, type CsvField} from '@tryghost/custom-field-types/csv';
|
|
3
5
|
import type {Knex} from 'knex';
|
|
4
6
|
import type {MemberImportRow, ImportErrorRow, ImportLabel, Label} from './row';
|
|
5
7
|
import type {RowSpool, SpooledRows} from './spool';
|
|
@@ -80,7 +82,7 @@ interface StripeSubscriptions {
|
|
|
80
82
|
|
|
81
83
|
// Gifts: reassigning a redeemed gift to the imported member.
|
|
82
84
|
export interface GiftService {
|
|
83
|
-
reassignRedeemer(giftId: string, memberId: string, options:
|
|
85
|
+
reassignRedeemer(giftId: string, memberId: string, options: {transacting?: Knex.Transaction}): Promise<void>;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
// The completion email concern: who it goes to, the links it carries, and sending
|
|
@@ -91,6 +93,20 @@ export interface EmailNotifications {
|
|
|
91
93
|
urlFor(type: string, data: unknown, absolute: boolean): string;
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
// Opaque to the import: planWrite produces it, applyWrite consumes it.
|
|
97
|
+
type CustomFieldPlan = unknown;
|
|
98
|
+
|
|
99
|
+
// The custom fields collaborator as the import needs it. activeFields is the field set a
|
|
100
|
+
// custom_fields.* column is read against, empty when the feature is off; planWrite
|
|
101
|
+
// validates a row's values (throwing so the row fails whole) and applyWrite persists
|
|
102
|
+
// them, merging a composite's sub-fields into whatever is already stored, both on the
|
|
103
|
+
// row's transaction.
|
|
104
|
+
export interface CustomFieldsImport {
|
|
105
|
+
activeFields(): Promise<CsvField[]>;
|
|
106
|
+
planWrite(values: Record<string, unknown>): Promise<CustomFieldPlan[]>;
|
|
107
|
+
applyWrite(memberId: string, plan: CustomFieldPlan[], executor: Knex): Promise<void>;
|
|
108
|
+
}
|
|
109
|
+
|
|
94
110
|
// The collaborators the import depends on, one per concern.
|
|
95
111
|
interface ImporterDeps {
|
|
96
112
|
knex: Knex;
|
|
@@ -100,6 +116,7 @@ interface ImporterDeps {
|
|
|
100
116
|
tiers: TiersRepository;
|
|
101
117
|
stripe: StripeSubscriptions;
|
|
102
118
|
gifts: GiftService;
|
|
119
|
+
customFields: CustomFieldsImport;
|
|
103
120
|
email: EmailNotifications;
|
|
104
121
|
addJob: (job: {job: () => Promise<void>; offloaded: boolean; name: string}) => void;
|
|
105
122
|
getTimezone: () => string;
|
|
@@ -183,12 +200,13 @@ class MembersCSVImporter {
|
|
|
183
200
|
private _tiers: TiersRepository;
|
|
184
201
|
private _stripe: StripeSubscriptions;
|
|
185
202
|
private _gifts: GiftService;
|
|
203
|
+
private _customFields: CustomFieldsImport;
|
|
186
204
|
private _email: EmailNotifications;
|
|
187
205
|
private _addJob: (job: {job: () => Promise<void>; offloaded: boolean; name: string}) => void;
|
|
188
206
|
private _getTimezone: () => string;
|
|
189
207
|
private _getInlineThreshold: () => number;
|
|
190
208
|
|
|
191
|
-
constructor({knex, readRows, spool, members, tiers, stripe, gifts, email, addJob, getTimezone, getInlineThreshold}: ImporterDeps) {
|
|
209
|
+
constructor({knex, readRows, spool, members, tiers, stripe, gifts, customFields, email, addJob, getTimezone, getInlineThreshold}: ImporterDeps) {
|
|
192
210
|
this._knex = knex;
|
|
193
211
|
this._readRows = readRows;
|
|
194
212
|
this._spool = spool;
|
|
@@ -196,6 +214,7 @@ class MembersCSVImporter {
|
|
|
196
214
|
this._tiers = tiers;
|
|
197
215
|
this._stripe = stripe;
|
|
198
216
|
this._gifts = gifts;
|
|
217
|
+
this._customFields = customFields;
|
|
199
218
|
this._email = email;
|
|
200
219
|
this._addJob = addJob;
|
|
201
220
|
this._getTimezone = getTimezone;
|
|
@@ -283,6 +302,9 @@ class MembersCSVImporter {
|
|
|
283
302
|
const globalLabels: Label[] = [importLabel, ...extraLabels];
|
|
284
303
|
const performStart = Date.now();
|
|
285
304
|
const defaultTier = await this._tiers.getDefault();
|
|
305
|
+
// The field set a custom_fields.* column is read against; empty when the feature
|
|
306
|
+
// is off, so a carried-through column is dropped and the write boundary stays shut.
|
|
307
|
+
const activeCustomFields = await this._customFields.activeFields();
|
|
286
308
|
const tierIdCache = new Map();
|
|
287
309
|
const archivableStripePriceIds: string[] = [];
|
|
288
310
|
// Copied per row: the member model stamps ids and trims names onto these in
|
|
@@ -295,8 +317,7 @@ class MembersCSVImporter {
|
|
|
295
317
|
let imported = 0;
|
|
296
318
|
const importErrors: ImportErrorRow[] = [];
|
|
297
319
|
for (const row of rows) {
|
|
298
|
-
|
|
299
|
-
const options = {transacting: trx, context: IMPORT_CONTEXT};
|
|
320
|
+
let trx: Knex.Transaction | undefined;
|
|
300
321
|
try {
|
|
301
322
|
if (row.gift_id) {
|
|
302
323
|
if (row.import_tier) {
|
|
@@ -307,6 +328,17 @@ class MembersCSVImporter {
|
|
|
307
328
|
}
|
|
308
329
|
}
|
|
309
330
|
|
|
331
|
+
// Validate the row's custom field values before the transaction opens.
|
|
332
|
+
// planWrite only reads, and it throws on an invalid value to fail the row
|
|
333
|
+
// before any member write -- so there is no reason to hold a transaction
|
|
334
|
+
// across it, and doing so would deadlock the single-connection SQLite pool.
|
|
335
|
+
const customFieldPlan = activeCustomFields.length > 0
|
|
336
|
+
? await this._customFields.planWrite(fieldValuesFromCsvRow(activeCustomFields, row, stripFormulaGuard))
|
|
337
|
+
: [];
|
|
338
|
+
|
|
339
|
+
trx = await this._knex.transaction(undefined, {doNotRejectOnRollback: false});
|
|
340
|
+
const options = {transacting: trx, context: IMPORT_CONTEXT};
|
|
341
|
+
|
|
310
342
|
const createdAt = (row.created_at && moment(row.created_at).isAfter(moment())) ? moment().toDate() : row.created_at;
|
|
311
343
|
const memberValues: MemberImportValues = {
|
|
312
344
|
email: row.email,
|
|
@@ -394,6 +426,9 @@ class MembersCSVImporter {
|
|
|
394
426
|
}
|
|
395
427
|
}
|
|
396
428
|
|
|
429
|
+
// On the row's transaction, so the values commit or roll back with the member.
|
|
430
|
+
await this._customFields.applyWrite(member.id, customFieldPlan, trx);
|
|
431
|
+
|
|
397
432
|
await trx.commit();
|
|
398
433
|
imported += 1;
|
|
399
434
|
} catch (error) {
|
|
@@ -401,7 +436,11 @@ class MembersCSVImporter {
|
|
|
401
436
|
const errorMessage = errorList
|
|
402
437
|
.map(e => (typeof e === 'object' && e !== null && 'message' in e ? e.message : undefined))
|
|
403
438
|
.join(', ');
|
|
404
|
-
|
|
439
|
+
// trx is unset if the row failed before the transaction opened (a bad
|
|
440
|
+
// custom field value or gift combination).
|
|
441
|
+
if (trx) {
|
|
442
|
+
await trx.rollback();
|
|
443
|
+
}
|
|
405
444
|
importErrors.push({...row, error: errorMessage});
|
|
406
445
|
}
|
|
407
446
|
}
|
|
@@ -40,7 +40,23 @@ function makeImporter(deps) {
|
|
|
40
40
|
// Gifts is initialised at boot and always present at request time; the getter
|
|
41
41
|
// resolves it lazily so the ready service is picked up whenever a row uses it.
|
|
42
42
|
const gifts = {
|
|
43
|
-
reassignRedeemer: (giftId, memberId, options) => deps.getGiftService().reassignRedeemer(
|
|
43
|
+
reassignRedeemer: (giftId, memberId, options) => deps.getGiftService().reassignRedeemer({
|
|
44
|
+
giftId,
|
|
45
|
+
memberId,
|
|
46
|
+
transacting: options.transacting
|
|
47
|
+
})
|
|
48
|
+
};
|
|
49
|
+
// Gated by the same labs flag as the export, so the two halves round-trip or stay
|
|
50
|
+
// silent together: off, activeFields resolves empty and every custom_fields.* column
|
|
51
|
+
// is dropped.
|
|
52
|
+
const customFields = {
|
|
53
|
+
activeFields: async () => (labs.isSet('membersCustomFields') ? deps.customFields.definitions.browse() : []),
|
|
54
|
+
planWrite: values => deps.customFields.values.planWrite(values),
|
|
55
|
+
// mergeComposites: a sub-field is a field, so a row writes the sub-fields it fills
|
|
56
|
+
// and leaves the rest alone, the same rule a blank `name` column follows. Without
|
|
57
|
+
// it a file naming one address column would replace the whole address, clearing
|
|
58
|
+
// five sub-fields it never mentioned.
|
|
59
|
+
applyWrite: (memberId, plan, executor) => deps.customFields.values.applyWrite(memberId, plan, executor, { mergeComposites: true })
|
|
44
60
|
};
|
|
45
61
|
return new importer_1.default({
|
|
46
62
|
knex: deps.knex,
|
|
@@ -56,6 +72,7 @@ function makeImporter(deps) {
|
|
|
56
72
|
productRepository: deps.productRepository
|
|
57
73
|
}),
|
|
58
74
|
gifts,
|
|
75
|
+
customFields,
|
|
59
76
|
email,
|
|
60
77
|
addJob: deps.addJob,
|
|
61
78
|
getTimezone: deps.getTimezone,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {Knex} from 'knex';
|
|
2
|
-
import
|
|
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';
|
|
3
4
|
import readMemberRows from './import/reader';
|
|
4
5
|
import {createRowSpool} from './import/spool';
|
|
5
6
|
import MembersCSVExporter, {type ExportOptions, type CustomFieldDefinition} from './export/exporter';
|
|
@@ -17,7 +18,9 @@ interface ImporterServices {
|
|
|
17
18
|
getMembersRepository(): Promise<Omit<MembersRepository, 'getImportLabel'>>;
|
|
18
19
|
getDefaultTier(): Promise<Tier>;
|
|
19
20
|
getTierByName(name: string): Promise<Tier | null>;
|
|
20
|
-
getGiftService():
|
|
21
|
+
getGiftService(): {
|
|
22
|
+
reassignRedeemer(input: {giftId: string; memberId: string; transacting?: Knex.Transaction}): Promise<void>;
|
|
23
|
+
};
|
|
21
24
|
sendEmail: EmailNotifications['send'];
|
|
22
25
|
urlFor: EmailNotifications['urlFor'];
|
|
23
26
|
addJob(job: {job: () => Promise<void>; offloaded: boolean; name: string}): void;
|
|
@@ -25,6 +28,14 @@ interface ImporterServices {
|
|
|
25
28
|
getInlineThreshold(): number;
|
|
26
29
|
stripeAPIService: unknown;
|
|
27
30
|
productRepository: unknown;
|
|
31
|
+
// The custom fields services the members service hands the import composition root.
|
|
32
|
+
customFields: {
|
|
33
|
+
definitions: {browse(): Promise<CsvField[]>};
|
|
34
|
+
values: {
|
|
35
|
+
planWrite(values: Record<string, unknown>): Promise<unknown[]>;
|
|
36
|
+
applyWrite(memberId: string, plan: unknown[], executor: Knex, options?: {mergeComposites?: boolean}): Promise<void>;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
// The custom fields services the members service hands the export composition root.
|
|
@@ -63,7 +74,24 @@ export function makeImporter(deps: ImporterServices) {
|
|
|
63
74
|
// Gifts is initialised at boot and always present at request time; the getter
|
|
64
75
|
// resolves it lazily so the ready service is picked up whenever a row uses it.
|
|
65
76
|
const gifts: GiftService = {
|
|
66
|
-
reassignRedeemer: (giftId, memberId, options) => deps.getGiftService().reassignRedeemer(
|
|
77
|
+
reassignRedeemer: (giftId, memberId, options) => deps.getGiftService().reassignRedeemer({
|
|
78
|
+
giftId,
|
|
79
|
+
memberId,
|
|
80
|
+
transacting: options.transacting
|
|
81
|
+
})
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Gated by the same labs flag as the export, so the two halves round-trip or stay
|
|
85
|
+
// silent together: off, activeFields resolves empty and every custom_fields.* column
|
|
86
|
+
// is dropped.
|
|
87
|
+
const customFields: CustomFieldsImport = {
|
|
88
|
+
activeFields: async () => (labs.isSet('membersCustomFields') ? deps.customFields.definitions.browse() : []),
|
|
89
|
+
planWrite: values => deps.customFields.values.planWrite(values),
|
|
90
|
+
// mergeComposites: a sub-field is a field, so a row writes the sub-fields it fills
|
|
91
|
+
// and leaves the rest alone, the same rule a blank `name` column follows. Without
|
|
92
|
+
// it a file naming one address column would replace the whole address, clearing
|
|
93
|
+
// five sub-fields it never mentioned.
|
|
94
|
+
applyWrite: (memberId, plan, executor) => deps.customFields.values.applyWrite(memberId, plan, executor, {mergeComposites: true})
|
|
67
95
|
};
|
|
68
96
|
|
|
69
97
|
return new MembersCSVImporter({
|
|
@@ -80,6 +108,7 @@ export function makeImporter(deps: ImporterServices) {
|
|
|
80
108
|
productRepository: deps.productRepository
|
|
81
109
|
}),
|
|
82
110
|
gifts,
|
|
111
|
+
customFields,
|
|
83
112
|
email,
|
|
84
113
|
addJob: deps.addJob,
|
|
85
114
|
getTimezone: deps.getTimezone,
|