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
|
@@ -31,8 +31,8 @@ module.exports = class LinkClickRepository {
|
|
|
31
31
|
this.#DomainEvents = deps.DomainEvents;
|
|
32
32
|
|
|
33
33
|
// Memoize the findOne function
|
|
34
|
-
this.memoizedFindOne = _.memoize(async (uuid) => {
|
|
35
|
-
return await this.#Member.findOne({uuid});
|
|
34
|
+
this.memoizedFindOne = _.memoize(async (uuid, options) => {
|
|
35
|
+
return await this.#Member.findOne({uuid}, options);
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -54,15 +54,18 @@ module.exports = class LinkClickRepository {
|
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* @param {LinkClick} linkClick
|
|
57
|
-
* @
|
|
57
|
+
* @param {object} [options]
|
|
58
|
+
* @param {object} [options.transacting]
|
|
59
|
+
* @returns {Promise<string | undefined>}
|
|
58
60
|
*/
|
|
59
|
-
async save(linkClick) {
|
|
61
|
+
async save(linkClick, options = {}) {
|
|
60
62
|
let member;
|
|
63
|
+
const memberLookupOptions = options.transacting ? {transacting: options.transacting} : undefined;
|
|
61
64
|
|
|
62
65
|
if (config && config.get('linkClickTrackingCacheMemberUuid')) {
|
|
63
|
-
member = await this.memoizedFindOne(linkClick.member_uuid);
|
|
66
|
+
member = await this.memoizedFindOne(linkClick.member_uuid, memberLookupOptions);
|
|
64
67
|
} else {
|
|
65
|
-
member = await this.#Member.findOne({uuid: linkClick.member_uuid});
|
|
68
|
+
member = await this.#Member.findOne({uuid: linkClick.member_uuid}, memberLookupOptions);
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
if (!member) {
|
|
@@ -76,11 +79,29 @@ module.exports = class LinkClickRepository {
|
|
|
76
79
|
// Only store the pathname (no support for variable query strings)
|
|
77
80
|
redirect_id: linkClick.link_id.toHexString(),
|
|
78
81
|
member_id: member.id
|
|
79
|
-
},
|
|
82
|
+
}, options);
|
|
80
83
|
|
|
81
84
|
linkClick.event_id = ObjectID.createFromHexString(model.id);
|
|
85
|
+
const timestamp = linkClick.timestamp ?? new Date();
|
|
86
|
+
|
|
87
|
+
const event = this.#MemberLinkClickEvent.create({
|
|
88
|
+
memberId: member.id,
|
|
89
|
+
memberLastSeenAt: member.get('last_seen_at'),
|
|
90
|
+
linkId: linkClick.link_id.toHexString()
|
|
91
|
+
}, timestamp);
|
|
92
|
+
|
|
93
|
+
if (options.transacting) {
|
|
94
|
+
options.transacting.executionPromise.then(() => {
|
|
95
|
+
try {
|
|
96
|
+
this.#DomainEvents.dispatch(event);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
sentry.captureException(error);
|
|
99
|
+
}
|
|
100
|
+
}, () => undefined);
|
|
101
|
+
} else {
|
|
102
|
+
this.#DomainEvents.dispatch(event);
|
|
103
|
+
}
|
|
82
104
|
|
|
83
|
-
|
|
84
|
-
this.#DomainEvents.dispatch(this.#MemberLinkClickEvent.create({memberId: member.id, memberLastSeenAt: member.get('last_seen_at'), linkId: linkClick.link_id.toHexString()}, new Date()));
|
|
105
|
+
return member.id;
|
|
85
106
|
}
|
|
86
107
|
};
|
|
@@ -10,7 +10,7 @@ const moment = require('moment');
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* @typedef {object} ILinkClickRepository
|
|
13
|
-
* @prop {(event: LinkClick) => Promise<
|
|
13
|
+
* @prop {(event: LinkClick, options?: {transacting?: object}) => Promise<string | undefined>} save
|
|
14
14
|
* @prop {({filter: string}) => Promise<LinkClick[]>} getAll
|
|
15
15
|
*/
|
|
16
16
|
|
|
@@ -61,6 +61,10 @@ class LinkClickTrackingService {
|
|
|
61
61
|
#LinkRedirect;
|
|
62
62
|
/** @type {Object} */
|
|
63
63
|
#urlUtils;
|
|
64
|
+
/** @type {object} */
|
|
65
|
+
#automationsApi;
|
|
66
|
+
/** @type {(callback: (transacting: object) => Promise<void>) => Promise<void>} */
|
|
67
|
+
#runInTransaction;
|
|
64
68
|
|
|
65
69
|
/**
|
|
66
70
|
* @param {object} deps
|
|
@@ -69,6 +73,8 @@ class LinkClickTrackingService {
|
|
|
69
73
|
* @param {IPostLinkRepository} deps.postLinkRepository
|
|
70
74
|
* @param {DomainEvents} deps.DomainEvents
|
|
71
75
|
* @param {urlUtils} deps.urlUtils
|
|
76
|
+
* @param {object} deps.automationsApi
|
|
77
|
+
* @param {(callback: (transacting: object) => Promise<void>) => Promise<void>} deps.runInTransaction
|
|
72
78
|
*/
|
|
73
79
|
constructor(deps) {
|
|
74
80
|
this.#linkClickRepository = deps.linkClickRepository;
|
|
@@ -76,6 +82,8 @@ class LinkClickTrackingService {
|
|
|
76
82
|
this.#postLinkRepository = deps.postLinkRepository;
|
|
77
83
|
this.#DomainEvents = deps.DomainEvents;
|
|
78
84
|
this.#urlUtils = deps.urlUtils;
|
|
85
|
+
this.#automationsApi = deps.automationsApi;
|
|
86
|
+
this.#runInTransaction = deps.runInTransaction;
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
async init() {
|
|
@@ -233,6 +241,20 @@ class LinkClickTrackingService {
|
|
|
233
241
|
return url;
|
|
234
242
|
}
|
|
235
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Add first-party tracking to a URL in an automation email
|
|
246
|
+
* @param {Readonly<URL>} url
|
|
247
|
+
* @param {string} automationActionRevisionId
|
|
248
|
+
* @param {string} memberUuid
|
|
249
|
+
* @return {Promise<URL>}
|
|
250
|
+
*/
|
|
251
|
+
async addAutomationTrackingToUrl(url, automationActionRevisionId, memberUuid) {
|
|
252
|
+
const redirect = await this.#linkRedirectService.getOrAddAutomationRedirect(automationActionRevisionId, url);
|
|
253
|
+
const trackedUrl = new URL(redirect.from.href);
|
|
254
|
+
trackedUrl.searchParams.set('m', memberUuid);
|
|
255
|
+
return trackedUrl;
|
|
256
|
+
}
|
|
257
|
+
|
|
236
258
|
subscribe() {
|
|
237
259
|
this.#DomainEvents.subscribe(RedirectEvent, async (event) => {
|
|
238
260
|
const uuid = event.data.url.searchParams.get('m');
|
|
@@ -242,9 +264,28 @@ class LinkClickTrackingService {
|
|
|
242
264
|
|
|
243
265
|
const click = new LinkClick({
|
|
244
266
|
member_uuid: uuid,
|
|
245
|
-
link_id: event.data.link.link_id
|
|
267
|
+
link_id: event.data.link.link_id,
|
|
268
|
+
timestamp: event.timestamp
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const automationActionRevisionId = event.data.link.automationActionRevisionId;
|
|
272
|
+
if (!automationActionRevisionId) {
|
|
273
|
+
await this.#linkClickRepository.save(click);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
await this.#runInTransaction(async (transacting) => {
|
|
278
|
+
const memberId = await this.#linkClickRepository.save(click, {transacting});
|
|
279
|
+
if (!memberId) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
await this.#automationsApi.trackEmailClicked({
|
|
284
|
+
automationActionRevisionId,
|
|
285
|
+
memberId,
|
|
286
|
+
clickedAt: event.timestamp
|
|
287
|
+
}, {transacting});
|
|
246
288
|
});
|
|
247
|
-
await this.#linkClickRepository.save(click);
|
|
248
289
|
});
|
|
249
290
|
}
|
|
250
291
|
}
|
|
@@ -11,7 +11,9 @@ const blockedReferrerDomains = [
|
|
|
11
11
|
'www.chicagomag.com',
|
|
12
12
|
'bailii.org',
|
|
13
13
|
'www.bailii.org',
|
|
14
|
-
'*.doubleclick.net'
|
|
14
|
+
'*.doubleclick.net',
|
|
15
|
+
// Substack's client app throws on any non-empty ?ref= value and swaps the post for an error screen
|
|
16
|
+
'*.substack.com'
|
|
15
17
|
];
|
|
16
18
|
|
|
17
19
|
const isBlockedReferrerDomain = (domain) => {
|
|
@@ -2,11 +2,13 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const lexicalLib = require('../../lib/lexical');
|
|
4
4
|
const labs = require('../../../shared/labs');
|
|
5
|
+
const urlUtils = require('../../../shared/url-utils').default;
|
|
5
6
|
const {finalize} = require('../email-rendering/finalize');
|
|
6
7
|
const errors = require('@tryghost/errors');
|
|
7
8
|
const {MESSAGES} = require('./constants');
|
|
8
9
|
const {wrapReplacementStrings} = require('@tryghost/kg-default-nodes').utils.replacementStrings;
|
|
9
10
|
const linkReplacer = require('../lib/link-replacer');
|
|
11
|
+
const linkTracking = require('../link-tracking');
|
|
10
12
|
const emailDesign = require('../email-rendering/email-design');
|
|
11
13
|
const {registerHelpers} = require('../email-service/helpers/register-helpers');
|
|
12
14
|
|
|
@@ -115,9 +117,11 @@ class MemberWelcomeEmailRenderer {
|
|
|
115
117
|
* @param {Object} options.member - Member data (name, email)
|
|
116
118
|
* @param {Object} options.siteSettings - Site settings (title, url, accentColor)
|
|
117
119
|
* @param {string} [options.unsubscribeUrl] - When set, the footer shows an "Unsubscribe from these emails" link instead of "Manage your preferences"
|
|
120
|
+
* @param {boolean} [options.trackClicks]
|
|
121
|
+
* @param {string | null} [options.automationActionRevisionId]
|
|
118
122
|
* @returns {Promise<{html: string, text: string, subject: string}>}
|
|
119
123
|
*/
|
|
120
|
-
async render({lexical, subject, designSettings, member, siteSettings, unsubscribeUrl}) {
|
|
124
|
+
async render({lexical, subject, designSettings, member, siteSettings, unsubscribeUrl, trackClicks = false, automationActionRevisionId = null}) {
|
|
121
125
|
designSettings = designSettings || {};
|
|
122
126
|
|
|
123
127
|
const design = emailDesign.getEmailDesign({
|
|
@@ -138,7 +142,8 @@ class MemberWelcomeEmailRenderer {
|
|
|
138
142
|
|
|
139
143
|
let content;
|
|
140
144
|
try {
|
|
141
|
-
|
|
145
|
+
const absoluteLexical = urlUtils.transformReadyToAbsolute(lexical);
|
|
146
|
+
content = await lexicalLib.render(absoluteLexical, {target: 'email', design});
|
|
142
147
|
} catch (err) {
|
|
143
148
|
throw new errors.IncorrectUsageError({
|
|
144
149
|
message: MESSAGES.INVALID_LEXICAL_STRUCTURE,
|
|
@@ -158,8 +163,18 @@ class MemberWelcomeEmailRenderer {
|
|
|
158
163
|
const contentWithReplacements = this.#applyReplacements({definitions, text: content, escapeHtml: true});
|
|
159
164
|
const subjectWithReplacements = this.#applyReplacements({definitions, text: subject, escapeHtml: false});
|
|
160
165
|
|
|
161
|
-
// Resolve relative links
|
|
162
|
-
|
|
166
|
+
// Resolve relative links, including Ghost Portal routes such as #/portal/signup,
|
|
167
|
+
// while preserving document-local anchors such as #section.
|
|
168
|
+
const contentWithAbsoluteLinks = await linkReplacer.replace(contentWithReplacements, async (url, originalPath) => {
|
|
169
|
+
const isDocumentAnchor = originalPath.startsWith('#') && !originalPath.startsWith('#/');
|
|
170
|
+
if (isDocumentAnchor) {
|
|
171
|
+
return originalPath;
|
|
172
|
+
}
|
|
173
|
+
const isTrackable = ['http:', 'https:'].includes(url.protocol);
|
|
174
|
+
if (trackClicks && automationActionRevisionId && member.uuid && isTrackable) {
|
|
175
|
+
await linkTracking.init();
|
|
176
|
+
return await linkTracking.service.addAutomationTrackingToUrl(url, automationActionRevisionId, member.uuid);
|
|
177
|
+
}
|
|
163
178
|
return url;
|
|
164
179
|
}, {base: siteSettings.url});
|
|
165
180
|
|
|
@@ -368,10 +368,12 @@ 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]
|
|
371
373
|
* @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
374
|
* @returns {Promise<unknown>}
|
|
373
375
|
*/
|
|
374
|
-
async #sendEmail({member, memberStatus, email, emailType, trackOpens, unsubscribe = null}) {
|
|
376
|
+
async #sendEmail({member, memberStatus, email, emailType, trackOpens, trackClicks = false, automationActionRevisionId = null, unsubscribe = null}) {
|
|
375
377
|
if (!member.email) {
|
|
376
378
|
throw new errors.IncorrectUsageError({
|
|
377
379
|
message: MESSAGES.MISSING_RECIPIENT_EMAIL
|
|
@@ -396,7 +398,9 @@ class MemberWelcomeEmailService {
|
|
|
396
398
|
uuid: member.uuid
|
|
397
399
|
},
|
|
398
400
|
siteSettings: this.#getSiteSettings(),
|
|
399
|
-
unsubscribeUrl: unsubscribe?.url
|
|
401
|
+
unsubscribeUrl: unsubscribe?.url,
|
|
402
|
+
trackClicks,
|
|
403
|
+
automationActionRevisionId
|
|
400
404
|
});
|
|
401
405
|
|
|
402
406
|
const senderOptions = await this.#getEffectiveSenderOptions(
|
|
@@ -474,9 +478,11 @@ class MemberWelcomeEmailService {
|
|
|
474
478
|
* @param {string} options.member.uuid
|
|
475
479
|
* @param {'free' | 'paid'} options.memberStatus
|
|
476
480
|
* @param {boolean} options.trackOpens
|
|
481
|
+
* @param {boolean} options.trackClicks
|
|
482
|
+
* @param {string} options.automationActionRevisionId
|
|
477
483
|
* @returns {Promise<unknown>}
|
|
478
484
|
*/
|
|
479
|
-
async sendAutomationEmail({email, member, memberStatus, trackOpens}) {
|
|
485
|
+
async sendAutomationEmail({email, member, memberStatus, trackOpens, trackClicks, automationActionRevisionId}) {
|
|
480
486
|
const designSettings = email.designSettingId ?
|
|
481
487
|
await EmailDesignSetting.findOne({id: email.designSettingId}) :
|
|
482
488
|
null;
|
|
@@ -502,6 +508,8 @@ class MemberWelcomeEmailService {
|
|
|
502
508
|
designSettings: designSettingsJson
|
|
503
509
|
},
|
|
504
510
|
trackOpens,
|
|
511
|
+
trackClicks,
|
|
512
|
+
automationActionRevisionId,
|
|
505
513
|
unsubscribe
|
|
506
514
|
});
|
|
507
515
|
}
|
|
@@ -2,15 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resolveInlineThreshold = resolveInlineThreshold;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* higher layer — an operator's config file, an environment variable, argv — can
|
|
10
|
-
* replace it with anything at all. Turning that into something usable is a
|
|
11
|
-
* config concern, so it happens here, at the boundary. The importer is handed an
|
|
12
|
-
* answer and never learns where it came from.
|
|
13
|
-
*/
|
|
5
|
+
// Config arrives untyped: defaults.json ships a value, and any higher layer -- an
|
|
6
|
+
// operator's config file, an environment variable, argv -- can replace it with anything.
|
|
7
|
+
// Validating it into a usable number is a config concern, so it happens here at the
|
|
8
|
+
// boundary; the importer is handed a resolved value.
|
|
14
9
|
// The row count at or below which an import is performed while the request is
|
|
15
10
|
// still open. Zero is allowed and means every import is deferred. A numeric
|
|
16
11
|
// string is accepted because a value supplied through an environment variable
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import {z} from 'zod';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* higher layer — an operator's config file, an environment variable, argv — can
|
|
8
|
-
* replace it with anything at all. Turning that into something usable is a
|
|
9
|
-
* config concern, so it happens here, at the boundary. The importer is handed an
|
|
10
|
-
* answer and never learns where it came from.
|
|
11
|
-
*/
|
|
3
|
+
// Config arrives untyped: defaults.json ships a value, and any higher layer -- an
|
|
4
|
+
// operator's config file, an environment variable, argv -- can replace it with anything.
|
|
5
|
+
// Validating it into a usable number is a config concern, so it happens here at the
|
|
6
|
+
// boundary; the importer is handed a resolved value.
|
|
12
7
|
|
|
13
8
|
// The row count at or below which an import is performed while the request is
|
|
14
9
|
// still open. Zero is allowed and means every import is deferred. A numeric
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.stripFormulaGuard = exports.serialize = exports.parse = void 0;
|
|
7
|
+
var parse_1 = require("./parse");
|
|
8
|
+
Object.defineProperty(exports, "parse", { enumerable: true, get: function () { return __importDefault(parse_1).default; } });
|
|
9
|
+
var serialize_1 = require("./serialize");
|
|
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; } });
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = parse;
|
|
7
|
+
const node_stream_1 = require("node:stream");
|
|
8
|
+
const papaparse_1 = __importDefault(require("papaparse"));
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
// A column named after an Object.prototype member (toString, __proto__, ...) is unsafe
|
|
11
|
+
// as a key: writing it would shadow a method or reach the prototype.
|
|
12
|
+
function isSafeColumnName(name) {
|
|
13
|
+
return !(name in Object.prototype);
|
|
14
|
+
}
|
|
15
|
+
// headerMapping renames the CSV's headers to the columns they emit under; unmapped
|
|
16
|
+
// columns carry through under their own name.
|
|
17
|
+
function parse(path, headerMapping) {
|
|
18
|
+
return new Promise(function (resolve, reject) {
|
|
19
|
+
const csvFileStream = fs_extra_1.default.createReadStream(path);
|
|
20
|
+
const csvParserStream = papaparse_1.default.parse(papaparse_1.default.NODE_STREAM_INPUT, {
|
|
21
|
+
header: true
|
|
22
|
+
});
|
|
23
|
+
const rows = [];
|
|
24
|
+
const parsedCSVStream = (0, node_stream_1.pipeline)(csvFileStream, csvParserStream, (err) => {
|
|
25
|
+
if (err) {
|
|
26
|
+
return reject(err);
|
|
27
|
+
}
|
|
28
|
+
resolve(rows);
|
|
29
|
+
});
|
|
30
|
+
parsedCSVStream.on('data', (parsedRow) => {
|
|
31
|
+
// a throw here escapes as an uncaught exception and leaves this
|
|
32
|
+
// promise forever unsettled, so it has to become a rejection
|
|
33
|
+
try {
|
|
34
|
+
const row = {};
|
|
35
|
+
for (const [header, value] of Object.entries(parsedRow)) {
|
|
36
|
+
// papaparse gathers the overflow from a row carrying more
|
|
37
|
+
// fields than there are headers under __parsed_extra, as an
|
|
38
|
+
// array rather than a cell any mapping can name
|
|
39
|
+
if (typeof value !== 'string') {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// hasOwn, not `in`: a prototype-named header would otherwise match an
|
|
43
|
+
// inherited method on the mapping and take a function as its mapped name.
|
|
44
|
+
if (headerMapping && Object.hasOwn(headerMapping, header)) {
|
|
45
|
+
row[headerMapping[header]] = value;
|
|
46
|
+
}
|
|
47
|
+
else if (isSafeColumnName(header)) {
|
|
48
|
+
// Carry any unmapped column through untouched, so the import is not
|
|
49
|
+
// constrained to a known vocabulary: a custom_fields.* column
|
|
50
|
+
// survives parsing even though nothing consumes it yet.
|
|
51
|
+
row[header] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!Object.keys(row).length) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
rows.push(row);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
reject(err);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {pipeline} from 'node:stream';
|
|
2
|
+
import papaparse from 'papaparse';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
|
|
5
|
+
// A parsed CSV row: raw string cells, keyed by (renamed) column. Parsing is mechanical;
|
|
6
|
+
// giving the columns meaning and coercing their values is the domain's job (the import
|
|
7
|
+
// schema).
|
|
8
|
+
export type Row = Record<string, string>;
|
|
9
|
+
|
|
10
|
+
// A column named after an Object.prototype member (toString, __proto__, ...) is unsafe
|
|
11
|
+
// as a key: writing it would shadow a method or reach the prototype.
|
|
12
|
+
function isSafeColumnName(name: string): boolean {
|
|
13
|
+
return !(name in Object.prototype);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// headerMapping renames the CSV's headers to the columns they emit under; unmapped
|
|
17
|
+
// columns carry through under their own name.
|
|
18
|
+
export default function parse(path: string, headerMapping?: Record<string, string>): Promise<Row[]> {
|
|
19
|
+
return new Promise(function (resolve, reject) {
|
|
20
|
+
const csvFileStream = fs.createReadStream(path);
|
|
21
|
+
const csvParserStream = papaparse.parse(papaparse.NODE_STREAM_INPUT, {
|
|
22
|
+
header: true
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const rows: Row[] = [];
|
|
26
|
+
const parsedCSVStream = pipeline(csvFileStream, csvParserStream, (err) => {
|
|
27
|
+
if (err) {
|
|
28
|
+
return reject(err);
|
|
29
|
+
}
|
|
30
|
+
resolve(rows);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
parsedCSVStream.on('data', (parsedRow: Record<string, unknown>) => {
|
|
34
|
+
// a throw here escapes as an uncaught exception and leaves this
|
|
35
|
+
// promise forever unsettled, so it has to become a rejection
|
|
36
|
+
try {
|
|
37
|
+
const row: Row = {};
|
|
38
|
+
|
|
39
|
+
for (const [header, value] of Object.entries(parsedRow)) {
|
|
40
|
+
// papaparse gathers the overflow from a row carrying more
|
|
41
|
+
// fields than there are headers under __parsed_extra, as an
|
|
42
|
+
// array rather than a cell any mapping can name
|
|
43
|
+
if (typeof value !== 'string') {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// hasOwn, not `in`: a prototype-named header would otherwise match an
|
|
48
|
+
// inherited method on the mapping and take a function as its mapped name.
|
|
49
|
+
if (headerMapping && Object.hasOwn(headerMapping, header)) {
|
|
50
|
+
row[headerMapping[header]] = value;
|
|
51
|
+
} else if (isSafeColumnName(header)) {
|
|
52
|
+
// Carry any unmapped column through untouched, so the import is not
|
|
53
|
+
// constrained to a known vocabulary: a custom_fields.* column
|
|
54
|
+
// survives parsing even though nothing consumes it yet.
|
|
55
|
+
row[header] = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!Object.keys(row).length) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
rows.push(row);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
reject(err);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = serialize;
|
|
7
|
+
const papaparse_1 = __importDefault(require("papaparse"));
|
|
8
|
+
// The papaparse boundary: given rows already shaped into cells, it writes CSV and
|
|
9
|
+
// nothing else. escapeFormulae is forced on -- papaparse defaults it off, but this CSV
|
|
10
|
+
// is opened by a person, so a leading =, +, -, @ or tab must be escaped.
|
|
11
|
+
function serialize(rows, { columns, header = true } = {}) {
|
|
12
|
+
return papaparse_1.default.unparse(rows, { columns, header, escapeFormulae: true });
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import papaparse from 'papaparse';
|
|
2
|
+
|
|
3
|
+
interface SerializeOptions {
|
|
4
|
+
// Omitted, papaparse derives the columns (and their order) from the first row's
|
|
5
|
+
// keys -- so a typed shaper's row can define them, with no separate list to drift.
|
|
6
|
+
columns?: string[];
|
|
7
|
+
header?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// The papaparse boundary: given rows already shaped into cells, it writes CSV and
|
|
11
|
+
// nothing else. escapeFormulae is forced on -- papaparse defaults it off, but this CSV
|
|
12
|
+
// is opened by a person, so a leading =, +, -, @ or tab must be escaped.
|
|
13
|
+
export default function serialize(
|
|
14
|
+
rows: Array<Record<string, unknown>>,
|
|
15
|
+
{columns, header = true}: SerializeOptions = {}
|
|
16
|
+
): string {
|
|
17
|
+
return papaparse.unparse(rows, {columns, header, escapeFormulae: true});
|
|
18
|
+
}
|