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
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const {createAddColumnMigration} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = createAddColumnMigration('automated_email_recipients', 'automation_run_step_id', {
|
|
4
|
+
type: 'string',
|
|
5
|
+
maxlength: 24,
|
|
6
|
+
nullable: true,
|
|
7
|
+
references: 'automation_run_steps.id'
|
|
8
|
+
}, {algorithm: 'auto'});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const {combineTransactionalMigrations, removePermissionFromRole} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = combineTransactionalMigrations(
|
|
4
|
+
removePermissionFromRole({
|
|
5
|
+
permission: 'Add notifications',
|
|
6
|
+
role: 'Editor'
|
|
7
|
+
}),
|
|
8
|
+
removePermissionFromRole({
|
|
9
|
+
permission: 'Add notifications',
|
|
10
|
+
role: 'Super Editor'
|
|
11
|
+
})
|
|
12
|
+
);
|
|
@@ -25,6 +25,11 @@ function addTableColumn(tableName, tableBuilder, columnName, columnSpec = schema
|
|
|
25
25
|
// creation distinguishes between text with fieldtype, string with maxlength and all others
|
|
26
26
|
if (columnSpec.type === 'text' && Object.prototype.hasOwnProperty.call(columnSpec, 'fieldtype')) {
|
|
27
27
|
column = tableBuilder[columnSpec.type](columnName, columnSpec.fieldtype);
|
|
28
|
+
} else if (columnSpec.type === 'binary' && Object.prototype.hasOwnProperty.call(columnSpec, 'maxlength')) {
|
|
29
|
+
// knex emits an unbounded `blob` for a length-less binary column, which
|
|
30
|
+
// MySQL can only index with a prefix. Passing the length gives us
|
|
31
|
+
// `varbinary(N)`, which is indexable in full.
|
|
32
|
+
column = tableBuilder[columnSpec.type](columnName, columnSpec.maxlength);
|
|
28
33
|
} else if (columnSpec.type === 'string') {
|
|
29
34
|
if (Object.prototype.hasOwnProperty.call(columnSpec, 'maxlength')) {
|
|
30
35
|
column = tableBuilder[columnSpec.type](columnName, columnSpec.maxlength);
|
|
@@ -1066,7 +1066,7 @@
|
|
|
1066
1066
|
"gift_link": "manage"
|
|
1067
1067
|
},
|
|
1068
1068
|
"Super Editor": {
|
|
1069
|
-
"notification": "
|
|
1069
|
+
"notification": ["browse", "destroy"],
|
|
1070
1070
|
"post": "all",
|
|
1071
1071
|
"setting": ["browse", "read"],
|
|
1072
1072
|
"slug": "all",
|
|
@@ -1091,7 +1091,7 @@
|
|
|
1091
1091
|
"gift_link": "manage"
|
|
1092
1092
|
},
|
|
1093
1093
|
"Editor": {
|
|
1094
|
-
"notification": "
|
|
1094
|
+
"notification": ["browse", "destroy"],
|
|
1095
1095
|
"post": "all",
|
|
1096
1096
|
"setting": ["browse", "read"],
|
|
1097
1097
|
"slug": "all",
|
|
@@ -1263,8 +1263,12 @@ module.exports = {
|
|
|
1263
1263
|
to: {type: 'string', maxlength: 2000, nullable: false},
|
|
1264
1264
|
post_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'posts.id', setNullDelete: true},
|
|
1265
1265
|
automation_action_revision_id: {type: 'string', maxlength: 24, nullable: true, references: 'automation_action_revisions.id', setNullDelete: true},
|
|
1266
|
+
to_hash: {type: 'binary', maxlength: 32, nullable: true},
|
|
1266
1267
|
created_at: {type: 'dateTime', nullable: false},
|
|
1267
|
-
updated_at: {type: 'dateTime', nullable: true}
|
|
1268
|
+
updated_at: {type: 'dateTime', nullable: true},
|
|
1269
|
+
'@@UNIQUE_CONSTRAINTS@@': [
|
|
1270
|
+
['automation_action_revision_id', 'to_hash']
|
|
1271
|
+
]
|
|
1268
1272
|
},
|
|
1269
1273
|
members_click_events: {
|
|
1270
1274
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
@@ -1335,6 +1339,7 @@ module.exports = {
|
|
|
1335
1339
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
1336
1340
|
automated_email_id: {type: 'string', maxlength: 24, nullable: true, references: 'welcome_email_automated_emails.id'},
|
|
1337
1341
|
automation_action_revision_id: {type: 'string', maxlength: 24, nullable: true, references: 'automation_action_revisions.id'},
|
|
1342
|
+
automation_run_step_id: {type: 'string', maxlength: 24, nullable: true, references: 'automation_run_steps.id'},
|
|
1338
1343
|
member_id: {type: 'string', maxlength: 24, nullable: false, index: true},
|
|
1339
1344
|
member_uuid: {type: 'string', maxlength: 36, nullable: false},
|
|
1340
1345
|
member_email: {type: 'string', maxlength: 191, nullable: false},
|
|
@@ -1346,7 +1346,16 @@ Post = ghostBookshelf.Model.extend({
|
|
|
1346
1346
|
destroy: function destroy(unfilteredOptions) {
|
|
1347
1347
|
let options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
|
|
1348
1348
|
|
|
1349
|
-
const destroyPost = () => {
|
|
1349
|
+
const destroyPost = async () => {
|
|
1350
|
+
// The `comments.in_reply_to_id` references form chains between a post's
|
|
1351
|
+
// comments, which MySQL cannot resolve while cascade-deleting them
|
|
1352
|
+
// alongside `comments.parent_id`. Clear the references first so the
|
|
1353
|
+
// `comments.post_id` cascade delete can do its job
|
|
1354
|
+
await ghostBookshelf.knex('comments')
|
|
1355
|
+
.where('post_id', options.id)
|
|
1356
|
+
.update('in_reply_to_id', null)
|
|
1357
|
+
.transacting(options.transacting);
|
|
1358
|
+
|
|
1350
1359
|
return ghostBookshelf.Model.destroy.call(this, options);
|
|
1351
1360
|
};
|
|
1352
1361
|
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.browse = browse;
|
|
7
7
|
exports.read = read;
|
|
8
|
+
exports.browseActionLinks = browseActionLinks;
|
|
8
9
|
exports.edit = edit;
|
|
9
10
|
exports.requestPoll = requestPoll;
|
|
10
11
|
exports.trigger = trigger;
|
|
@@ -15,6 +16,7 @@ exports.retryStep = retryStep;
|
|
|
15
16
|
exports.recordEmailSent = recordEmailSent;
|
|
16
17
|
exports.getAutomatedEmailRecipientsByMailgunIds = getAutomatedEmailRecipientsByMailgunIds;
|
|
17
18
|
exports.trackEmailDeliveredAndOpened = trackEmailDeliveredAndOpened;
|
|
19
|
+
exports.trackEmailClicked = trackEmailClicked;
|
|
18
20
|
const errors_1 = __importDefault(require("@tryghost/errors"));
|
|
19
21
|
const tpl_1 = __importDefault(require("@tryghost/tpl"));
|
|
20
22
|
const bson_objectid_1 = __importDefault(require("bson-objectid"));
|
|
@@ -30,6 +32,7 @@ const StartAutomationsPollEvent = require('./events/start-automations-poll-event
|
|
|
30
32
|
const MAX_AUTOMATION_ACTIONS = 20;
|
|
31
33
|
const messages = {
|
|
32
34
|
automationNotFound: 'Automation not found.',
|
|
35
|
+
automationActionNotFound: 'Automation action not found.',
|
|
33
36
|
invalidAutomationPayload: 'Automation edit payload must include status, actions, and edges.',
|
|
34
37
|
invalidAutomationStatus: 'Automation status must be one of: active, inactive.',
|
|
35
38
|
duplicateAutomationActionIdentity: 'Automation action identifiers must be unique.',
|
|
@@ -86,6 +89,15 @@ async function read(automationId) {
|
|
|
86
89
|
}
|
|
87
90
|
return automation;
|
|
88
91
|
}
|
|
92
|
+
async function browseActionLinks(automationId, actionId) {
|
|
93
|
+
const links = await repository.getAutomationActionLinks(automationId, actionId);
|
|
94
|
+
if (!links) {
|
|
95
|
+
throw new errors_1.default.NotFoundError({
|
|
96
|
+
message: (0, tpl_1.default)(messages.automationActionNotFound)
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return links;
|
|
100
|
+
}
|
|
89
101
|
async function edit(automationId, data) {
|
|
90
102
|
const parsedData = await validateEditData(data);
|
|
91
103
|
const automation = await repository.edit(automationId, parsedData);
|
|
@@ -311,3 +323,6 @@ async function getAutomatedEmailRecipientsByMailgunIds(...args) {
|
|
|
311
323
|
async function trackEmailDeliveredAndOpened(...args) {
|
|
312
324
|
return await repository.trackEmailDeliveredAndOpened(...args);
|
|
313
325
|
}
|
|
326
|
+
async function trackEmailClicked(...args) {
|
|
327
|
+
await repository.trackEmailClicked(...args);
|
|
328
|
+
}
|
|
@@ -20,6 +20,7 @@ const MAX_AUTOMATION_ACTIONS = 20;
|
|
|
20
20
|
|
|
21
21
|
const messages = {
|
|
22
22
|
automationNotFound: 'Automation not found.',
|
|
23
|
+
automationActionNotFound: 'Automation action not found.',
|
|
23
24
|
invalidAutomationPayload: 'Automation edit payload must include status, actions, and edges.',
|
|
24
25
|
invalidAutomationStatus: 'Automation status must be one of: active, inactive.',
|
|
25
26
|
duplicateAutomationActionIdentity: 'Automation action identifiers must be unique.',
|
|
@@ -87,6 +88,18 @@ export async function read(automationId: string) {
|
|
|
87
88
|
return automation;
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
export async function browseActionLinks(automationId: string, actionId: string) {
|
|
92
|
+
const links = await repository.getAutomationActionLinks(automationId, actionId);
|
|
93
|
+
|
|
94
|
+
if (!links) {
|
|
95
|
+
throw new errors.NotFoundError({
|
|
96
|
+
message: tpl(messages.automationActionNotFound)
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return links;
|
|
101
|
+
}
|
|
102
|
+
|
|
90
103
|
export async function edit(automationId: string, data: unknown) {
|
|
91
104
|
const parsedData = await validateEditData(data);
|
|
92
105
|
|
|
@@ -375,3 +388,9 @@ export async function trackEmailDeliveredAndOpened(
|
|
|
375
388
|
) {
|
|
376
389
|
return await repository.trackEmailDeliveredAndOpened(...args);
|
|
377
390
|
}
|
|
391
|
+
|
|
392
|
+
export async function trackEmailClicked(
|
|
393
|
+
...args: Parameters<AutomationsRepository['trackEmailClicked']>
|
|
394
|
+
) {
|
|
395
|
+
await repository.trackEmailClicked(...args);
|
|
396
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {ReadonlyDeep} from 'type-fest';
|
|
2
|
+
import type {Knex} from 'knex';
|
|
2
3
|
|
|
3
4
|
export interface Pagination {
|
|
4
5
|
page: number;
|
|
@@ -32,6 +33,11 @@ export interface AutomationEmailStats {
|
|
|
32
33
|
clicked_rate: number | null;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
export interface AutomationActionLink {
|
|
37
|
+
url: string;
|
|
38
|
+
clicked_count: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
export interface SendEmailAction {
|
|
36
42
|
id: string;
|
|
37
43
|
type: 'send_email';
|
|
@@ -84,6 +90,7 @@ export type AutomatedEmailEvents = {
|
|
|
84
90
|
|
|
85
91
|
export type RecordEmailSentOptions = Readonly<{
|
|
86
92
|
automationActionRevisionId: string;
|
|
93
|
+
automationRunStepId: string;
|
|
87
94
|
mailgunMessageId?: string;
|
|
88
95
|
memberEmail: string;
|
|
89
96
|
memberId: string;
|
|
@@ -131,6 +138,7 @@ export type AutomationStepTerminalStatus =
|
|
|
131
138
|
export interface AutomationsRepository {
|
|
132
139
|
browse(): Promise<Page<AutomationSummary>>;
|
|
133
140
|
getById(id: string): Promise<Automation | null>;
|
|
141
|
+
getAutomationActionLinks(automationId: string, actionId: string): Promise<AutomationActionLink[] | null>;
|
|
134
142
|
edit(id: string, data: EditAutomationData): Promise<Automation | null>;
|
|
135
143
|
trigger(options: {
|
|
136
144
|
memberEmail: string;
|
|
@@ -195,4 +203,16 @@ export interface AutomationsRepository {
|
|
|
195
203
|
trackEmailDeliveredAndOpened(
|
|
196
204
|
eventsByAutomatedEmailRecipientId: ReadonlyDeep<Map<string, AutomatedEmailEvents>>
|
|
197
205
|
): Promise<void>;
|
|
206
|
+
/**
|
|
207
|
+
* Record the first click timestamp for the automated email recipient identified by a run step.
|
|
208
|
+
*
|
|
209
|
+
*/
|
|
210
|
+
trackEmailClicked(options: {
|
|
211
|
+
automationActionRevisionId: string;
|
|
212
|
+
automationRunStepId: string;
|
|
213
|
+
memberId: string;
|
|
214
|
+
clickedAt: Readonly<Date>;
|
|
215
|
+
}, transactionOptions?: {
|
|
216
|
+
transacting?: Knex.Transaction;
|
|
217
|
+
}): Promise<void>;
|
|
198
218
|
}
|
|
@@ -9,11 +9,13 @@ const tpl_1 = __importDefault(require("@tryghost/tpl"));
|
|
|
9
9
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
10
10
|
const bson_objectid_1 = __importDefault(require("bson-objectid"));
|
|
11
11
|
const dequal_1 = require("dequal");
|
|
12
|
-
|
|
12
|
+
// @ts-expect-error This module currently lacks type definitions.
|
|
13
|
+
const lexical_1 = __importDefault(require("../../lib/lexical"));
|
|
14
|
+
const url_utils_1 = __importDefault(require("../../../shared/url-utils"));
|
|
13
15
|
const constants_1 = require("../member-welcome-emails/constants");
|
|
16
|
+
const database_date_1 = require("./database-date");
|
|
14
17
|
const stale_lock_cutoff_1 = require("./stale-lock-cutoff");
|
|
15
18
|
const HOUR_MS = 60 * 60 * 1000;
|
|
16
|
-
const DATABASE_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
|
17
19
|
const DEFAULT_WELCOME_EMAIL_AUTOMATIONS = [{
|
|
18
20
|
name: 'Free member welcome flow',
|
|
19
21
|
slug: constants_1.MEMBER_WELCOME_EMAIL_SLUGS.free
|
|
@@ -28,9 +30,6 @@ const messages = {
|
|
|
28
30
|
defaultEmailDesignSettingNotFound: 'Default automated email design setting not found.'
|
|
29
31
|
};
|
|
30
32
|
const DEFAULT_EMAIL_DESIGN_SETTING_REFERENCE = constants_1.DEFAULT_EMAIL_DESIGN_SETTING_SLUG;
|
|
31
|
-
function toDatabaseDate(date) {
|
|
32
|
-
return (0, moment_1.default)(date).format(DATABASE_DATE_FORMAT);
|
|
33
|
-
}
|
|
34
33
|
function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier }) {
|
|
35
34
|
return {
|
|
36
35
|
async browse() {
|
|
@@ -54,6 +53,33 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
|
|
|
54
53
|
return await buildAutomation(trx, automation);
|
|
55
54
|
});
|
|
56
55
|
},
|
|
56
|
+
async getAutomationActionLinks(automationId, actionId) {
|
|
57
|
+
const action = await knex('automation_actions')
|
|
58
|
+
.select('id')
|
|
59
|
+
.where({
|
|
60
|
+
id: actionId,
|
|
61
|
+
automation_id: automationId
|
|
62
|
+
})
|
|
63
|
+
.whereNull('deleted_at')
|
|
64
|
+
.first();
|
|
65
|
+
if (!action) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const rows = await knex('redirects as redirects')
|
|
69
|
+
.countDistinct({ clicked_count: 'members_click_events.member_id' })
|
|
70
|
+
.select(knex.raw('MIN(??) as ??', ['redirects.to', 'url']))
|
|
71
|
+
.innerJoin('automation_action_revisions as revisions', 'revisions.id', 'redirects.automation_action_revision_id')
|
|
72
|
+
.leftJoin('members_click_events', 'members_click_events.redirect_id', 'redirects.id')
|
|
73
|
+
.where('revisions.action_id', actionId)
|
|
74
|
+
.whereNotNull('redirects.to_hash')
|
|
75
|
+
.groupBy('redirects.to_hash')
|
|
76
|
+
.orderBy('clicked_count', 'desc')
|
|
77
|
+
.orderBy('url', 'asc');
|
|
78
|
+
return rows.map(row => ({
|
|
79
|
+
url: url_utils_1.default.transformReadyToAbsolute(row.url),
|
|
80
|
+
clicked_count: Number(row.clicked_count)
|
|
81
|
+
}));
|
|
82
|
+
},
|
|
57
83
|
async edit(id, data) {
|
|
58
84
|
return await knex.transaction(async (trx) => {
|
|
59
85
|
const automation = await loadAutomation(trx, id);
|
|
@@ -64,7 +90,7 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
|
|
|
64
90
|
const updatedAutomation = await updateAutomation(trx, {
|
|
65
91
|
...automation,
|
|
66
92
|
status: data.status,
|
|
67
|
-
updated_at: toDatabaseDate(now)
|
|
93
|
+
updated_at: (0, database_date_1.toDatabaseDate)(now)
|
|
68
94
|
});
|
|
69
95
|
await replaceAutomationGraph(trx, updatedAutomation.id, data.actions, data.edges);
|
|
70
96
|
if (updatedAutomation.status === 'inactive') {
|
|
@@ -101,7 +127,7 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
|
|
|
101
127
|
.update({
|
|
102
128
|
email_sent_count: trx.raw('COALESCE(??, 0) + ?', ['email_sent_count', 1])
|
|
103
129
|
});
|
|
104
|
-
const now = toDatabaseDate(new Date());
|
|
130
|
+
const now = (0, database_date_1.toDatabaseDate)(new Date());
|
|
105
131
|
await trx('automated_email_recipients').insert({
|
|
106
132
|
id: (0, bson_objectid_1.default)().toHexString(),
|
|
107
133
|
member_id: options.memberId,
|
|
@@ -109,6 +135,7 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
|
|
|
109
135
|
member_email: options.memberEmail,
|
|
110
136
|
member_name: options.memberName,
|
|
111
137
|
automation_action_revision_id: options.automationActionRevisionId,
|
|
138
|
+
automation_run_step_id: options.automationRunStepId,
|
|
112
139
|
...(options.mailgunMessageId ? { mailgun_message_id: options.mailgunMessageId } : {}),
|
|
113
140
|
track_clicks: options.trackClicks,
|
|
114
141
|
track_opens: options.trackOpens,
|
|
@@ -131,6 +158,13 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
|
|
|
131
158
|
return;
|
|
132
159
|
}
|
|
133
160
|
await knex.transaction(async (trx) => {
|
|
161
|
+
const revisionIds = new Set();
|
|
162
|
+
for (const { openedAt, automationActionRevisionId } of eventsByAutomatedEmailRecipientId.values()) {
|
|
163
|
+
if (openedAt) {
|
|
164
|
+
revisionIds.add(automationActionRevisionId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const orderedRevisionIds = await lockActionRevisions(trx, revisionIds);
|
|
134
168
|
const notYetOpened = await lockNotYetOpened(trx, eventsByAutomatedEmailRecipientId);
|
|
135
169
|
const newOpensPerRevision = new Map();
|
|
136
170
|
for (const [id, { deliveredAt, openedAt, automationActionRevisionId }] of eventsByAutomatedEmailRecipientId) {
|
|
@@ -151,10 +185,11 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
|
|
|
151
185
|
newOpensPerRevision.set(automationActionRevisionId, (newOpensPerRevision.get(automationActionRevisionId) ?? 0) + 1);
|
|
152
186
|
}
|
|
153
187
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
188
|
+
for (const id of orderedRevisionIds) {
|
|
189
|
+
const opens = newOpensPerRevision.get(id);
|
|
190
|
+
if (!opens) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
158
193
|
await trx('automation_action_revisions')
|
|
159
194
|
.where({ id })
|
|
160
195
|
.update({
|
|
@@ -162,9 +197,61 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
|
|
|
162
197
|
});
|
|
163
198
|
}
|
|
164
199
|
});
|
|
200
|
+
},
|
|
201
|
+
async trackEmailClicked({ automationActionRevisionId, automationRunStepId, memberId, clickedAt }, { transacting } = {}) {
|
|
202
|
+
const trackClick = async (trx) => {
|
|
203
|
+
await lockActionRevisions(trx, [automationActionRevisionId]);
|
|
204
|
+
const recipient = await trx('automated_email_recipients')
|
|
205
|
+
.select('id', 'clicked_at')
|
|
206
|
+
.where({
|
|
207
|
+
automation_action_revision_id: automationActionRevisionId,
|
|
208
|
+
automation_run_step_id: automationRunStepId,
|
|
209
|
+
member_id: memberId,
|
|
210
|
+
track_clicks: true
|
|
211
|
+
})
|
|
212
|
+
.forUpdate()
|
|
213
|
+
.first();
|
|
214
|
+
if (!recipient || recipient.clicked_at !== null) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const updated = await trx('automated_email_recipients')
|
|
218
|
+
.where({ id: recipient.id })
|
|
219
|
+
.whereNull('clicked_at')
|
|
220
|
+
.update({ clicked_at: clickedAt });
|
|
221
|
+
if (updated === 0) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
await trx('automation_action_revisions')
|
|
225
|
+
.where({ id: automationActionRevisionId })
|
|
226
|
+
.update({
|
|
227
|
+
email_clicked_count: trx.raw('COALESCE(email_clicked_count, 0) + 1')
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
if (transacting) {
|
|
231
|
+
await trackClick(transacting);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
await knex.transaction(trackClick);
|
|
165
235
|
}
|
|
166
236
|
};
|
|
167
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Lock revisions before recipients because inserting a recipient takes a shared
|
|
240
|
+
* foreign-key lock on its revision. Updating that revision later can deadlock
|
|
241
|
+
* with another transaction that has already locked the revision and is waiting
|
|
242
|
+
* for the recipient. Keep multi-revision lock acquisition deterministic.
|
|
243
|
+
*/
|
|
244
|
+
async function lockActionRevisions(trx, revisionIds) {
|
|
245
|
+
const sortedRevisionIds = [...new Set(revisionIds)]
|
|
246
|
+
.sort((left, right) => left.localeCompare(right));
|
|
247
|
+
if (sortedRevisionIds.length > 0) {
|
|
248
|
+
await trx('automation_action_revisions')
|
|
249
|
+
.select('id')
|
|
250
|
+
.whereIn('id', sortedRevisionIds)
|
|
251
|
+
.forUpdate();
|
|
252
|
+
}
|
|
253
|
+
return sortedRevisionIds;
|
|
254
|
+
}
|
|
168
255
|
/**
|
|
169
256
|
* Which of these recipients have yet to open, and so should count towards their
|
|
170
257
|
* revision's open count. Locks them for the transaction, so a worker racing on
|
|
@@ -194,7 +281,7 @@ async function ensureDefaultAutomations(trx) {
|
|
|
194
281
|
}
|
|
195
282
|
}
|
|
196
283
|
async function ensureAutomation(trx, defaults) {
|
|
197
|
-
const now = toDatabaseDate(new Date());
|
|
284
|
+
const now = (0, database_date_1.toDatabaseDate)(new Date());
|
|
198
285
|
const id = (0, bson_objectid_1.default)().toHexString();
|
|
199
286
|
await trx('automations')
|
|
200
287
|
.insert({
|
|
@@ -233,7 +320,7 @@ async function ensureWelcomeEmailAction(trx, automationId) {
|
|
|
233
320
|
if (!email) {
|
|
234
321
|
return;
|
|
235
322
|
}
|
|
236
|
-
const now = toDatabaseDate(new Date());
|
|
323
|
+
const now = (0, database_date_1.toDatabaseDate)(new Date());
|
|
237
324
|
const actionId = (0, bson_objectid_1.default)().toHexString();
|
|
238
325
|
await insertActions(trx, [{
|
|
239
326
|
id: actionId,
|
|
@@ -263,7 +350,7 @@ async function trigger(trx, options) {
|
|
|
263
350
|
return;
|
|
264
351
|
}
|
|
265
352
|
const now = new Date();
|
|
266
|
-
const nowString = toDatabaseDate(now);
|
|
353
|
+
const nowString = (0, database_date_1.toDatabaseDate)(now);
|
|
267
354
|
const readyAt = getReadyAtForAction(firstAction, now, fakeWaitHoursMultiplier);
|
|
268
355
|
const run = {
|
|
269
356
|
id: (0, bson_objectid_1.default)().toHexString(),
|
|
@@ -282,14 +369,14 @@ async function trigger(trx, options) {
|
|
|
282
369
|
});
|
|
283
370
|
}
|
|
284
371
|
async function insertRunStep(trx, { automationRunId, automationActionRevisionId, now, readyAt }) {
|
|
285
|
-
const nowString = toDatabaseDate(now);
|
|
372
|
+
const nowString = (0, database_date_1.toDatabaseDate)(now);
|
|
286
373
|
await trx('automation_run_steps').insert({
|
|
287
374
|
id: (0, bson_objectid_1.default)().toHexString(),
|
|
288
375
|
created_at: nowString,
|
|
289
376
|
updated_at: nowString,
|
|
290
377
|
automation_run_id: automationRunId,
|
|
291
378
|
automation_action_revision_id: automationActionRevisionId,
|
|
292
|
-
ready_at: toDatabaseDate(readyAt)
|
|
379
|
+
ready_at: (0, database_date_1.toDatabaseDate)(readyAt)
|
|
293
380
|
});
|
|
294
381
|
}
|
|
295
382
|
async function fetchAndLockSteps(trx, limit) {
|
|
@@ -304,9 +391,9 @@ async function fetchAndLockSteps(trx, limit) {
|
|
|
304
391
|
// 2. Try to lock those rows.
|
|
305
392
|
// 3. Select any rows we successfully locked.
|
|
306
393
|
const now = new Date();
|
|
307
|
-
const nowString = toDatabaseDate(now);
|
|
394
|
+
const nowString = (0, database_date_1.toDatabaseDate)(now);
|
|
308
395
|
const staleLockCutoff = (0, stale_lock_cutoff_1.getStaleLockCutoff)(now);
|
|
309
|
-
const staleLockCutoffString = toDatabaseDate(staleLockCutoff);
|
|
396
|
+
const staleLockCutoffString = (0, database_date_1.toDatabaseDate)(staleLockCutoff);
|
|
310
397
|
const lockId = node_crypto_1.default.randomUUID();
|
|
311
398
|
// 1. Select up to `limit` candidate rows.
|
|
312
399
|
const candidates = await trx('automation_run_steps')
|
|
@@ -374,7 +461,7 @@ async function findNextPendingReadyAt(trx, staleLockCutoff) {
|
|
|
374
461
|
.where((builder) => {
|
|
375
462
|
builder
|
|
376
463
|
.whereNull('locked_by')
|
|
377
|
-
.orWhere('locked_at', '<', toDatabaseDate(staleLockCutoff));
|
|
464
|
+
.orWhere('locked_at', '<', (0, database_date_1.toDatabaseDate)(staleLockCutoff));
|
|
378
465
|
})
|
|
379
466
|
.orderBy('ready_at')
|
|
380
467
|
.first();
|
|
@@ -479,7 +566,7 @@ async function findNextActionRevision(trx, sourceActionId) {
|
|
|
479
566
|
return row ?? null;
|
|
480
567
|
}
|
|
481
568
|
async function markStepTerminal(trx, step, status) {
|
|
482
|
-
const nowString = toDatabaseDate(new Date());
|
|
569
|
+
const nowString = (0, database_date_1.toDatabaseDate)(new Date());
|
|
483
570
|
return await updateStep(trx, step, {
|
|
484
571
|
status,
|
|
485
572
|
finished_at: nowString,
|
|
@@ -491,12 +578,12 @@ async function retryStep(trx, step, retryAt) {
|
|
|
491
578
|
await markStepTerminal(trx, step, 'automation disabled');
|
|
492
579
|
return false;
|
|
493
580
|
}
|
|
494
|
-
const nowString = toDatabaseDate(new Date());
|
|
581
|
+
const nowString = (0, database_date_1.toDatabaseDate)(new Date());
|
|
495
582
|
return await updateStep(trx, step, {
|
|
496
583
|
status: 'pending',
|
|
497
584
|
started_at: null,
|
|
498
585
|
finished_at: null,
|
|
499
|
-
ready_at: toDatabaseDate(retryAt),
|
|
586
|
+
ready_at: (0, database_date_1.toDatabaseDate)(retryAt),
|
|
500
587
|
updated_at: nowString
|
|
501
588
|
});
|
|
502
589
|
}
|
|
@@ -521,8 +608,8 @@ function getReadyAtForAction(action, now, fakeWaitHoursMultiplier) {
|
|
|
521
608
|
}
|
|
522
609
|
}
|
|
523
610
|
async function cancelCancelablePendingStepsForAutomation(trx, automationId, now) {
|
|
524
|
-
const nowString = toDatabaseDate(now);
|
|
525
|
-
const staleLockCutoff = toDatabaseDate((0, stale_lock_cutoff_1.getStaleLockCutoff)(now));
|
|
611
|
+
const nowString = (0, database_date_1.toDatabaseDate)(now);
|
|
612
|
+
const staleLockCutoff = (0, database_date_1.toDatabaseDate)((0, stale_lock_cutoff_1.getStaleLockCutoff)(now));
|
|
526
613
|
await trx('automation_run_steps')
|
|
527
614
|
.update({
|
|
528
615
|
status: 'automation disabled',
|
|
@@ -626,11 +713,11 @@ async function updateAutomation(trx, automation) {
|
|
|
626
713
|
async function replaceAutomationGraph(trx, automationId, submittedActions, edges) {
|
|
627
714
|
const existingActions = await loadAutomationActionRows(trx, automationId);
|
|
628
715
|
const existingActionById = new Map(existingActions.map(action => [action.id, action]));
|
|
629
|
-
const actions = await resolveEmailDesignSettingIds(trx, submittedActions);
|
|
716
|
+
const actions = (await resolveEmailDesignSettingIds(trx, submittedActions)).map(formatActionOnWrite);
|
|
630
717
|
const submittedActionIds = new Set(actions.map(action => action.id));
|
|
631
718
|
const actionIdsWithOwners = await loadActionIdsWithOwners(trx, [...submittedActionIds]);
|
|
632
719
|
const latestRevisionByActionId = new Map((await loadLatestActionRevisions(trx, [...submittedActionIds])).map(revision => [revision.action_id, revision]));
|
|
633
|
-
const now = toDatabaseDate(new Date());
|
|
720
|
+
const now = (0, database_date_1.toDatabaseDate)(new Date());
|
|
634
721
|
const actionsToInsert = [];
|
|
635
722
|
const revisionsToInsert = [];
|
|
636
723
|
for (const action of actions) {
|
|
@@ -680,6 +767,21 @@ async function replaceAutomationGraph(trx, automationId, submittedActions, edges
|
|
|
680
767
|
await deleteAutomationEdges(trx, automationId);
|
|
681
768
|
await insertActionEdges(trx, edges);
|
|
682
769
|
}
|
|
770
|
+
function formatActionOnWrite(action) {
|
|
771
|
+
if (action.type !== 'send_email') {
|
|
772
|
+
return action;
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
...action,
|
|
776
|
+
data: {
|
|
777
|
+
...action.data,
|
|
778
|
+
email_lexical: url_utils_1.default.lexicalToTransformReady(action.data.email_lexical, {
|
|
779
|
+
nodes: lexical_1.default.nodes,
|
|
780
|
+
transformMap: lexical_1.default.urlTransformMap
|
|
781
|
+
})
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
683
785
|
async function resolveEmailDesignSettingIds(trx, actions) {
|
|
684
786
|
if (!actions.some(action => action.type === 'send_email' && action.data.email_design_setting_id === DEFAULT_EMAIL_DESIGN_SETTING_REFERENCE)) {
|
|
685
787
|
return [...actions];
|
|
@@ -793,14 +895,14 @@ async function insertActionRevisions(trx, revisions) {
|
|
|
793
895
|
}
|
|
794
896
|
function getNextRevisionCreatedAt(latestCreatedAt, requestedCreatedAt) {
|
|
795
897
|
if (!latestCreatedAt) {
|
|
796
|
-
return toDatabaseDate(requestedCreatedAt);
|
|
898
|
+
return (0, database_date_1.toDatabaseDate)(requestedCreatedAt);
|
|
797
899
|
}
|
|
798
900
|
const requestedTime = new Date(requestedCreatedAt).getTime();
|
|
799
901
|
const latestTime = new Date(latestCreatedAt).getTime();
|
|
800
902
|
if (requestedTime > latestTime) {
|
|
801
|
-
return toDatabaseDate(requestedCreatedAt);
|
|
903
|
+
return (0, database_date_1.toDatabaseDate)(requestedCreatedAt);
|
|
802
904
|
}
|
|
803
|
-
return toDatabaseDate(new Date(latestTime + 1000));
|
|
905
|
+
return (0, database_date_1.toDatabaseDate)(new Date(latestTime + 1000));
|
|
804
906
|
}
|
|
805
907
|
function buildActionRevision(actionId, action, createdAt) {
|
|
806
908
|
switch (action.type) {
|
|
@@ -946,7 +1048,7 @@ function buildActionPayload(row, stats) {
|
|
|
946
1048
|
type: 'send_email',
|
|
947
1049
|
data: {
|
|
948
1050
|
email_subject: requireValue(row, 'email_subject'),
|
|
949
|
-
email_lexical: requireValue(row, 'email_lexical'),
|
|
1051
|
+
email_lexical: url_utils_1.default.transformReadyToAbsolute(requireValue(row, 'email_lexical')),
|
|
950
1052
|
email_design_setting_id: requireValue(row, 'email_design_setting_id')
|
|
951
1053
|
},
|
|
952
1054
|
stats: stats ?? EMPTY_EMAIL_STATS
|