ghost 6.57.1 → 6.58.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/LICENSE +22 -0
- package/bin/validate-adapters.js +1 -5
- 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-jobs-0.0.0.tgz +0 -0
- package/components/tryghost-adapter-base-redirects-0.0.0.tgz +0 -0
- package/components/tryghost-adapter-base-route-settings-0.0.0.tgz +0 -0
- package/components/tryghost-adapter-base-scheduling-0.2.2.tgz +0 -0
- package/components/tryghost-adapter-base-sso-0.1.2.tgz +0 -0
- package/components/tryghost-admin-api-schema-0.0.0.tgz +0 -0
- package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
- package/components/tryghost-i18n-0.0.0.tgz +0 -0
- package/components/tryghost-kg-card-factory-5.2.4.tgz +0 -0
- package/components/tryghost-kg-clean-basic-html-4.3.4.tgz +0 -0
- package/components/tryghost-kg-converters-1.2.5.tgz +0 -0
- package/components/tryghost-kg-default-cards-10.3.5.tgz +0 -0
- package/components/tryghost-kg-default-nodes-2.2.0.tgz +0 -0
- package/components/tryghost-kg-default-transforms-1.3.4.tgz +0 -0
- package/components/tryghost-kg-html-to-lexical-1.4.0.tgz +0 -0
- package/components/tryghost-kg-lexical-html-renderer-1.5.0.tgz +0 -0
- package/components/tryghost-kg-markdown-html-renderer-7.2.4.tgz +0 -0
- package/components/tryghost-kg-utils-1.1.4.tgz +0 -0
- package/components/tryghost-nql-string-0.0.0.tgz +0 -0
- package/components/tryghost-parse-email-address-0.0.0.tgz +0 -0
- package/core/boot.js +35 -3
- package/core/built/admin/assets/{about-BNojRaxf.js → about-B_BvSyLF.js} +1 -1
- package/core/built/admin/assets/{account-migration-DMS8JsM-.js → account-migration-oJUGxnzT.js} +1 -1
- package/core/built/admin/assets/activitypub/{account-migration-DHFbePl6.js → account-migration-BpDeIF3G.js} +8 -8
- package/core/built/admin/assets/activitypub/activitypub.js +4 -4
- package/core/built/admin/assets/activitypub/{avatar-CT4tS2_S.js → avatar-B0SpBPVL.js} +3 -3
- package/core/built/admin/assets/activitypub/{avatar-flipboard-ByYWKipo.js → avatar-flipboard-DKt1LGcE.js} +3 -3
- package/core/built/admin/assets/activitypub/{bluesky-sharing-srVnSHDz.js → bluesky-sharing-aPMtrS3L.js} +10 -10
- package/core/built/admin/assets/activitypub/{chunk-KS7C4IRE-D4HEcEwi.js → chunk-KS7C4IRE-CnTrsgmM.js} +26 -15
- package/core/built/admin/assets/activitypub/{content-formatters-C2sos5Ht.js → content-formatters-BORfuA9G.js} +318 -284
- package/core/built/admin/assets/activitypub/{copy-B1UBTOoZ.js → copy-D-YvNi2E.js} +2 -2
- package/core/built/admin/assets/activitypub/{dist-MPsb2WZ1.js → dist-Dqggkccr.js} +5 -5
- package/core/built/admin/assets/activitypub/{domain-DzcM9Dzo.js → domain-CPdiB27_.js} +10 -10
- package/core/built/admin/assets/activitypub/{edit-profile-CsQ_4xAk.js → edit-profile-BY2PawIe.js} +7 -7
- package/core/built/admin/assets/activitypub/{explore-CJe0DZBK.js → explore-Sp9bgAwq.js} +11 -11
- package/core/built/admin/assets/activitypub/{feed-CKV3qWON.js → feed-BjtF4ohL.js} +10 -10
- package/core/built/admin/assets/activitypub/{field-DckHfDJf.js → field-BDOYVTg6.js} +4 -4
- package/core/built/admin/assets/activitypub/{hash-Dlp8RM55.js → hash-DqXYHgzG.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-CsPy4R0M.js → inbox-CZ7IOAMn.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-DAsjzUUe.js → inbox-z1o2j1Tb.js} +11 -11
- package/core/built/admin/assets/activitypub/{label-DWyo9E5T.js → label-1jhN-qhD.js} +4 -4
- package/core/built/admin/assets/activitypub/layout-BRMezuty.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-B0Inc03M.js → moderation-CZudJi8N.js} +9 -9
- package/core/built/admin/assets/activitypub/{note-CSE54lER.js → note-nr_m_5yb.js} +8 -8
- package/core/built/admin/assets/activitypub/{notifications-RSwbKqdh.js → notifications-BdcyivbT.js} +10 -10
- package/core/built/admin/assets/activitypub/{onboarding-BFDkzeR2.js → onboarding-B4kwav9u.js} +2 -2
- package/core/built/admin/assets/activitypub/{onboarding-D6rcBdPe.js → onboarding-DZNElJYP.js} +3 -3
- package/core/built/admin/assets/activitypub/{preferences-CE231n2_.js → preferences-CNqy1x9C.js} +13 -13
- package/core/built/admin/assets/activitypub/{profile-aaQ1j1Eo.js → profile-DlY4rhbO.js} +15 -15
- package/core/built/admin/assets/activitypub/{reply-Cjb0Vs3m.js → reply-DASIN0Ec.js} +2 -2
- package/core/built/admin/assets/activitypub/{routes-Djb0mc9k.js → routes-CEHdEZzS.js} +31 -31
- package/core/built/admin/assets/activitypub/{separator-D8pROI5n.js → separator-B3k35sUV.js} +4 -4
- package/core/built/admin/assets/activitypub/{settings-DMc32IQl.js → settings-CrSV1cux.js} +6 -6
- package/core/built/admin/assets/activitypub/{site-DVIcsohX.js → site-CtBUCPBz.js} +6 -6
- package/core/built/admin/assets/activitypub/{step-1-9Yey9usF.js → step-1-D9jGWyAy.js} +7 -7
- package/core/built/admin/assets/activitypub/{step-2-DdYmKRXV.js → step-2-Jz_enuo9.js} +7 -7
- package/core/built/admin/assets/activitypub/{step-3-hdSkcjZU.js → step-3-KpUqpGWi.js} +12 -12
- package/core/built/admin/assets/activitypub/{tabs-CUqseYzQ.js → tabs-DvlsxTCI.js} +7 -7
- package/core/built/admin/assets/activitypub/{topic-filter-Coni6OXM.js → topic-filter-CIN1E2jg.js} +4 -4
- package/core/built/admin/assets/activitypub/{use-activity-pub-queries-MeHTPMQv.js → use-activity-pub-queries-CATFd6YV.js} +4 -4
- package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-B046AM38.js → use-navigate-with-base-path-iCkcbMGk.js} +2 -2
- package/core/built/admin/assets/activitypub/{use-reply-chain-data-1AO-Xob6.js → use-reply-chain-data-DJ0vhA8V.js} +6 -6
- package/core/built/admin/assets/activitypub/{users-BLeiOTHc.js → users-sgJ6GNZX.js} +12 -11
- package/core/built/admin/assets/activitypub/{x-BRfRYd16.js → x-Cy2jUiYM.js} +2 -2
- package/core/built/admin/assets/add-integration-modal-BW23PDYs.js +1 -0
- package/core/built/admin/assets/add-newsletter-modal-M0ls8BKO.js +1 -0
- package/core/built/admin/assets/{add-recommendation-modal-D-3LOrdd.js → add-recommendation-modal-B0NeBMzl.js} +1 -1
- package/core/built/admin/assets/{analytics-DTIMRhWw.js → analytics-BC2oR8ay.js} +1 -1
- package/core/built/admin/assets/announcement-bar-modal-pXBqjQE_.js +1 -0
- package/core/built/admin/assets/{api-keys-_iCT4dxr.js → api-keys-BgKr003d.js} +1 -1
- package/core/built/admin/assets/{api-keys-Dx4NeNpK.js → api-keys-Df6uv1dT.js} +1 -1
- package/core/built/admin/assets/{audience-DkknNqhZ.js → audience-BY8od8If.js} +1 -1
- package/core/built/admin/assets/{automated-emails-PayPCT8L.js → automated-emails-28c2Lbb3.js} +1 -1
- package/core/built/admin/assets/{automations-DvSU7QhW.js → automations-DCs6Wady.js} +1 -1
- package/core/built/admin/assets/{automations-162q5HjL.js → automations-Dzp_QnON.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-yZ9A_jL8.js → bluesky-sharing-D0P42Cws.js} +1 -1
- package/core/built/admin/assets/{breadcrumb-CjOH7LCU.js → breadcrumb-MTj_cKLb.js} +1 -1
- package/core/built/admin/assets/card-CiiE7QfM.js +1 -0
- package/core/built/admin/assets/{chart-helpers-DGxvWnBT.js → chart-helpers-DcO3k_vA.js} +1 -1
- package/core/built/admin/assets/{checkbox-BSUcnNq_.js → checkbox-LmClhqcS.js} +1 -1
- package/core/built/admin/assets/{chunk.424.4a27628efdade15c2dab.js → chunk.408.1a258af360eb8d905508.js} +40 -37
- package/core/built/admin/assets/{chunk.571.40f093ed6d883993714e.js → chunk.492.a9da46ca956b10116774.js} +1342 -1342
- package/core/built/admin/assets/{chunk.571.40f093ed6d883993714e.js.LICENSE.txt → chunk.492.a9da46ca956b10116774.js.LICENSE.txt} +1 -1
- package/core/built/admin/assets/chunk.524.4c693c3a00ac91df34fa.js +25 -0
- package/core/built/admin/assets/chunk.582.20b600e329e58d12d7c2.js +27 -0
- package/core/built/admin/assets/code-editor-view-DqTOUnbM.js +1 -0
- package/core/built/admin/assets/code-editor-view-bA0lfxUL.js +1 -0
- package/core/built/admin/assets/{debounce-Cx5B_9OI.js → color-picker-CBlJMv3y.js} +2 -2
- package/core/built/admin/assets/color-picker-field-Cv9GOJQZ.js +1 -0
- package/core/built/admin/assets/{combobox-5rdgDrDV.js → combobox-BbPJzjCp.js} +1 -1
- package/core/built/admin/assets/command-BrhXIlDr.js +1 -0
- package/core/built/admin/assets/comments-Cu4rD1aJ.js +1 -0
- package/core/built/admin/assets/confirmation-provider-BEdrnHns.js +1 -0
- package/core/built/admin/assets/{content-api-modal-BtW4Z1-1.js → content-api-modal-BOFWRUkC.js} +1 -1
- package/core/built/admin/assets/{copy-field-CEp_XieW.js → copy-field-8biQvNMD.js} +1 -1
- package/core/built/admin/assets/custom-field-type-option-CNhAgHbJ.js +1 -0
- package/core/built/admin/assets/custom-integration-modal-CrGOI8IB.js +1 -0
- package/core/built/admin/assets/{date-range-select-B3Ij1Rqg.js → date-range-select-Bsh4gIek.js} +1 -1
- package/core/built/admin/assets/debounce-pscB46sW.js +1 -0
- package/core/built/admin/assets/design-and-theme-modal-DWZguwWX.js +23 -0
- package/core/built/admin/assets/{dirty-confirm-dialog-YE9L21gY.js → dirty-confirm-dialog-DmdMpgyN.js} +1 -1
- package/core/built/admin/assets/{disabled-sources-indicator-uIzbWL7K.js → disabled-sources-indicator-BLnmyWgt.js} +1 -1
- package/core/built/admin/assets/dist-4m6pHdc0.js +23 -0
- package/core/built/admin/assets/{dist-DCk0UkGx.js → dist-B6bJuxL3.js} +1 -1
- package/core/built/admin/assets/{dist-CbHbGtQ-.js → dist-BtaTA7Ii.js} +1 -1
- package/core/built/admin/assets/{dist-Q1p9iXwH.js → dist-CZgIBXQN.js} +1 -1
- package/core/built/admin/assets/dist-CiHx7vA5.js +6 -0
- package/core/built/admin/assets/{dist-ARN6XTcR.js → dist-ECAUrdGc.js} +1 -1
- package/core/built/admin/assets/dist-Z97pCNqS.js +11 -0
- package/core/built/admin/assets/{dist-t6AXpf_4.js → dist-zwwGt8Yf.js} +1 -1
- package/core/built/admin/assets/{domain-DzrOAPY8.js → domain-Dp9TRrFp.js} +1 -1
- package/core/built/admin/assets/{dropzone-CzhP33wa.js → dropzone-B21Entqz.js} +1 -1
- package/core/built/admin/assets/{edit-profile-oufs4R4n.js → edit-profile-DG6lpMsr.js} +1 -1
- package/core/built/admin/assets/editor-CPQOrhrn.js +8 -0
- package/core/built/admin/assets/{editor-DtxX27Jz.css → editor-DLioOiRN.css} +1 -1
- package/core/built/admin/assets/{embed-signup-form-modal-Ch3s6lqA.js → embed-signup-form-modal-CgWniDha.js} +2 -2
- package/core/built/admin/assets/esm-CXo5SEPy.js +2 -0
- package/core/built/admin/assets/{explore-CA-R01Br.js → explore-C3rzONJv.js} +1 -1
- package/core/built/admin/assets/{feed-ClC8_Gld.js → feed-DpUwS1cU.js} +1 -1
- package/core/built/admin/assets/{field-B3OPenbP.js → field-BQqLTgHM.js} +1 -1
- package/core/built/admin/assets/{filter-sources-CiPLnG9A.js → filter-sources-FgqoUIL7.js} +1 -1
- package/core/built/admin/assets/{filters-yFvybRMe.js → filters-D0uIMD8n.js} +1 -1
- package/core/built/admin/assets/filters-DZJT29GW.js +1 -0
- package/core/built/admin/assets/{first-promoter-modal-BC0UpYCD.js → first-promoter-modal-DJhQQheG.js} +1 -1
- package/core/built/admin/assets/framework-provider-CFLwK5uS.js +4 -0
- package/core/built/admin/assets/{ghost-d38c30b866e59f2e06a357b5eb844fe5.css → ghost-68f447a1796b93a1f128b649bac8506d.css} +1 -1
- package/core/built/admin/assets/{ghost-d683fbeb945b13155ea669caa5d8db41.js → ghost-ba408063a5922b458b4a8f216cf23251.js} +124 -342
- package/core/built/admin/assets/{ghost-dark-1bffd5099ba7a749303133efacf8480b.css → ghost-dark-28ae4899c92022857f3584694bcb50ef.css} +1 -1
- package/core/built/admin/assets/{gift-link-modal-BKRuqo-k.js → gift-link-modal-sdFLjND9.js} +1 -1
- package/core/built/admin/assets/{gift-links-CVuqLhsZ.js → gift-links-D7BJ9qfe.js} +1 -1
- package/core/built/admin/assets/{global-data-provider-RzW6Bnlf.js → global-data-provider-CixmTX00.js} +1 -1
- package/core/built/admin/assets/{growth-ClNAldfL.js → growth-0YYkBEeS.js} +1 -1
- package/core/built/admin/assets/{growth-Dz8OSG06.js → growth-DpjAPeQT.js} +2 -2
- package/core/built/admin/assets/{history-modal-DIVuQ5Ct.js → history-modal-BWJ4-cJ0.js} +1 -1
- package/core/built/admin/assets/{hooks-D6EE7BIt.js → hooks-CJepWnbB.js} +1 -1
- package/core/built/admin/assets/{html-field-CVZ2sMxw.js → html-field-C-oa5DEl.js} +1 -1
- package/core/built/admin/assets/{iframe-buffering-e8vH4-Qn.js → iframe-buffering-Ba9sDpi2.js} +1 -1
- package/core/built/admin/assets/{images-Ok8FFJ3A.js → images-3cQgwfTD.js} +1 -1
- package/core/built/admin/assets/{inbox-CMUVI6qb.js → inbox-Dz_JblyJ.js} +6 -6
- package/core/built/admin/assets/index-BOJzlYiz.js +67 -0
- package/core/built/admin/assets/index-Bid6LAyc.css +1 -0
- package/core/built/admin/assets/{input-group-2XU-iuh_.js → input-group-DpogNUIt.js} +1 -1
- package/core/built/admin/assets/{integrations-BT6_iJvu.js → integrations-B9yYgci5.js} +1 -1
- package/core/built/admin/assets/{invite-user-modal-DrZ7xxrZ.js → invite-user-modal-DCiQSiuD.js} +1 -1
- package/core/built/admin/assets/{koenig-editor-base-BPj4S760.js → koenig-editor-base-BApy8Q7C.js} +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +8328 -8294
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +84 -84
- package/core/built/admin/assets/{koenig-lexical-Cdjv-m3_.js → koenig-lexical-2KuqRwZI.js} +33 -33
- package/core/built/admin/assets/{kpi-card-J8Mj1sDX.js → kpi-card-Ck3tT2Wg.js} +1 -1
- package/core/built/admin/assets/{kpi-card-awos0BMH.js → kpi-card-DDIxptaS.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-SEhvH-8E.js → kpi-helpers-ssbuy-hD.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-R6BjsKhd.js → kpi-tabs-BqoIM6bI.js} +1 -1
- package/core/built/admin/assets/{label-BMC3dzLI.js → label-DodDsMzn.js} +1 -1
- package/core/built/admin/assets/label-picker-Cvvbmzb5.js +1 -0
- package/core/built/admin/assets/{labels-DAPjKPy2.js → labels-DqvSR6JF.js} +1 -1
- package/core/built/admin/assets/layout-DkkGyev2.js +1 -0
- package/core/built/admin/assets/limit-service-BhiEvqcP.js +20 -0
- package/core/built/admin/assets/{link-helpers-Bzkk68GD.js → link-helpers-kpbU8TWh.js} +1 -1
- package/core/built/admin/assets/luxon-D1RVjjpq.js +1 -0
- package/core/built/admin/assets/member-custom-fields-CvGcpaiW.js +1 -0
- package/core/built/admin/assets/member-detail-CMCMZ47_.js +1 -0
- package/core/built/admin/assets/members-sRl6fNfq.js +11 -0
- package/core/built/admin/assets/{moderation-mPjYHgZW.js → moderation-KhKwGqI1.js} +1 -1
- package/core/built/admin/assets/{multi-select-combobox-CsoyHAiX.js → multi-select-combobox-nhCff0_K.js} +1 -1
- package/core/built/admin/assets/{navigation-modal-D_c0badj.js → navigation-modal-MDJExWDJ.js} +1 -1
- package/core/built/admin/assets/newsletter-BIBHumfV.js +1 -0
- package/core/built/admin/assets/newsletter-detail-modal-CfjBMH3v.js +1 -0
- package/core/built/admin/assets/{newsletter-emails-B-OLfrvr.js → newsletter-emails-ClDL-7dA.js} +1 -1
- package/core/built/admin/assets/newsletters-CBe-IPzK.js +1 -0
- package/core/built/admin/assets/{newsletters-Du0lzskS.js → newsletters-ILsexy-5.js} +1 -1
- package/core/built/admin/assets/{note-CSVsUKBm.js → note-HnNUOt4d.js} +1 -1
- package/core/built/admin/assets/{notifications-DV9FRtDw.js → notifications-Cj5e-258.js} +1 -1
- package/core/built/admin/assets/nql-lang-CaWrLesS.js +19 -0
- package/core/built/admin/assets/{offers-Um6ab71O.js → offers-DMxebaoK.js} +1 -1
- package/core/built/admin/assets/offers-container-modal-BHwHtixC.js +1 -0
- package/core/built/admin/assets/{onboarding-route--jL5KLPD.js → onboarding-route-D_4jp7Nq.js} +1 -1
- package/core/built/admin/assets/overview-CezDdg-F.js +1 -0
- package/core/built/admin/assets/overview-D5mM51Lj.js +1 -0
- package/core/built/admin/assets/{page-header-DAvDYKO9.js → page-header-VLPJfj_d.js} +1 -1
- package/core/built/admin/assets/{pages-BVMnpI2O.js → pages-Gchxp5c7.js} +1 -1
- package/core/built/admin/assets/{pintura-modal-jiUCxX0Q.js → pintura-modal-BXXJ8Iaa.js} +1 -1
- package/core/built/admin/assets/{portal-frame-D4D4nEO6.js → portal-frame-BLCvF8f-.js} +1 -1
- package/core/built/admin/assets/portal-modal-DN80tAcy.js +1 -0
- package/core/built/admin/assets/{post-analytics-BhAtiFJp.js → post-analytics-Gv81eN_g.js} +1 -1
- package/core/built/admin/assets/post-analytics-header-_ydSY8qO.js +1 -0
- package/core/built/admin/assets/{post-analytics-provider-DGg0FUqs.js → post-analytics-provider-O2YjZMhN.js} +1 -1
- package/core/built/admin/assets/{post-share-modal-DtFAC132.js → post-share-modal-CzYak_a5.js} +1 -1
- package/core/built/admin/assets/{posts-CxG7v0-M.js → posts-DgcUi2rc.js} +1 -1
- package/core/built/admin/assets/preferences-gneLb6LV.js +5 -0
- package/core/built/admin/assets/{preview-modal-DctYx20m.js → preview-modal-Bkq-tgVm.js} +1 -1
- package/core/built/admin/assets/profile-CzXmCNTC.js +1 -0
- package/core/built/admin/assets/{radio-group-CI5_Kf_d.js → radio-group-Dsq5ZEbV.js} +1 -1
- package/core/built/admin/assets/{referrers-B9QDAfYm.js → referrers-C3a2K8qB.js} +1 -1
- package/core/built/admin/assets/{router-provider-DSQfprxH.js → router-provider-CkB_ArLF.js} +1 -1
- package/core/built/admin/assets/{select-CkXqNJDJ.js → select-xuZAuDyS.js} +1 -1
- package/core/built/admin/assets/{settings-ZCj-W56o.js → settings-Ct8pabhr.js} +1 -1
- package/core/built/admin/assets/settings-KSWWQanQ.js +4 -0
- package/core/built/admin/assets/{settings-breadcrumbs-DIq31FwA.js → settings-breadcrumbs-DbPGu-Tu.js} +1 -1
- package/core/built/admin/assets/{share-modal-BNj14g79.js → share-modal-CdiUx9WM.js} +1 -1
- package/core/built/admin/assets/{slack-modal-C6SpMQ-q.js → slack-modal-BP--Z3jE.js} +1 -1
- package/core/built/admin/assets/{sort-button-B-k54ow2.js → sort-button-CjfKdlsL.js} +1 -1
- package/core/built/admin/assets/{source-icon-Cej3sZM6.js → source-icon-DHj1h6Ts.js} +1 -1
- package/core/built/admin/assets/{stats-B2KW8E2y.js → stats-C615kulJ.js} +1 -1
- package/core/built/admin/assets/{stats-view-CVzU2-p5.js → stats-view-BSMRdYiN.js} +1 -1
- package/core/built/admin/assets/{step-1-Dbn1iIMU.js → step-1-pSdGYtIF.js} +1 -1
- package/core/built/admin/assets/{step-2-BahnSuN6.js → step-2-T9Uy4eBD.js} +1 -1
- package/core/built/admin/assets/{step-3-CW04e2ZJ.js → step-3-D4uXe_XG.js} +2 -2
- package/core/built/admin/assets/{stripe-button-BkBZqaGG.js → stripe-button-Bh4p6Wud.js} +1 -1
- package/core/built/admin/assets/stripe-connect-modal-C2vTvepi.js +1 -0
- package/core/built/admin/assets/{table-BtLcEi1M.js → table-DghJxZMH.js} +1 -1
- package/core/built/admin/assets/{tabs-BaWRfg8j.js → tabs-l9c7FjzI.js} +1 -1
- package/core/built/admin/assets/tag-detail-DBw_M2-p.js +2 -0
- package/core/built/admin/assets/{tags-BbygUvNB.js → tags-DoWsAG6l.js} +1 -1
- package/core/built/admin/assets/{tags-BwCgmr5v.js → tags-EmEvJN75.js} +1 -1
- package/core/built/admin/assets/{testimonials-modal--bdzppyh.js → testimonials-modal-BZKePE2T.js} +1 -1
- package/core/built/admin/assets/tier-detail-modal-D65pVWsN.js +1 -0
- package/core/built/admin/assets/{tiers-Da6kxHQP.js → tiers-8iTFjJWg.js} +1 -1
- package/core/built/admin/assets/{toggle-group-CVl-Xigr.js → toggle-group-uJnl42w3.js} +1 -1
- package/core/built/admin/assets/{topic-filter-BoRHRbL1.js → topic-filter-BM7hz0kW.js} +1 -1
- package/core/built/admin/assets/transistor-modal-VZOGN3od.js +1 -0
- package/core/built/admin/assets/{unsplash-modal-CixoRAO2.js → unsplash-modal-CKGfACxF.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-BiWvcf1u.js → use-analytics-data-IxJcSjc9.js} +1 -1
- package/core/built/admin/assets/use-check-theme-limit-error-CGp3CGT5.js +1 -0
- package/core/built/admin/assets/use-feature-flag-CSq2Lwzt.js +1 -0
- package/core/built/admin/assets/use-filter-params-0hcJJuEJ.js +1 -0
- package/core/built/admin/assets/{use-filterable-api-iMpDZBxD.js → use-filterable-api-CQBtVG-l.js} +1 -1
- package/core/built/admin/assets/{use-growth-stats-Dom8xVzo.js → use-growth-stats-1NW-8eEZ.js} +1 -1
- package/core/built/admin/assets/use-limiter-ffwpsZPp.js +2 -0
- package/core/built/admin/assets/{use-pintura-config-Bj4QmiVk.js → use-pintura-config-BjFChXV1.js} +1 -1
- package/core/built/admin/assets/{use-pintura-editor-DvrslmPE.js → use-pintura-editor-Cb41oJyL.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-pxIHTXFs.js → use-post-referrers-Dp1BnTAK.js} +1 -1
- package/core/built/admin/assets/{use-reply-chain-data-CmFe0N6F.js → use-reply-chain-data-C0ZUTkfk.js} +1 -1
- package/core/built/admin/assets/{use-setting-group-Cmp_sy-d.js → use-setting-group-Bx8M6FL2.js} +1 -1
- package/core/built/admin/assets/{use-settings-navigation-BmROPrls.js → use-settings-navigation-BOZcat_V.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-D5rL3YwO.js → use-simple-pagination-DsEGSJhe.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-query-DP2dMaq7.js → use-tinybird-query-Ci_DiPkX.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-DrK_skPQ.js → use-tinybird-token-CsUwc722.js} +1 -1
- package/core/built/admin/assets/use-upgrade-route-B3_DUiuT.js +1 -0
- package/core/built/admin/assets/{use-url-input-ChrUZBFf.js → use-url-input-BEZJpVgN.js} +2 -2
- package/core/built/admin/assets/user-detail-modal-595z1N5r.js +1 -0
- package/core/built/admin/assets/{users-B6hIds6n.js → users-DmGMPfw7.js} +1 -1
- package/core/built/admin/assets/virtual-list-D5ugXyZY.js +1 -0
- package/core/built/admin/assets/{web-D71mjB2r.js → web-CAta3lRE.js} +1 -1
- package/core/built/admin/assets/web-Ds9_wWJK.js +2 -0
- package/core/built/admin/assets/zapier-modal-CcfDZY53.js +1 -0
- package/core/built/admin/index.html +12 -12
- package/core/frontend/apps/private-blogging/lib/middleware.js +5 -58
- package/core/frontend/helpers/ghost_head.js +52 -0
- package/core/frontend/meta/asset-url.js +20 -9
- package/core/frontend/public/cards.manifest.json +1 -0
- package/core/frontend/public/ghost-stats.min.js +1 -1
- package/core/frontend/services/assets-minification/card-assets.js +121 -46
- package/core/frontend/services/assets-minification/index.js +13 -4
- package/core/frontend/services/data/fetch-data.js +7 -14
- package/core/frontend/services/llms/service.js +27 -6
- package/core/frontend/services/proxy.js +1 -0
- package/core/frontend/services/routing/api-adapter.js +11 -5
- package/core/frontend/services/routing/collection-router.js +1 -2
- package/core/frontend/services/routing/controllers/entry/markdown.js +63 -5
- package/core/frontend/services/routing/controllers/entry.js +1 -1
- package/core/frontend/services/routing/controllers/previews.js +0 -3
- package/core/frontend/services/routing/events.js +2 -1
- package/core/frontend/services/routing/parent-router.js +0 -22
- package/core/frontend/services/routing/router-manager.js +2 -2
- package/core/frontend/services/routing/static-pages-router.js +0 -1
- package/core/frontend/services/sitemap/handler.js +3 -4
- package/core/frontend/services/sitemap/site-map-manager.js +1 -2
- package/core/frontend/services/theme-engine/i18n/i18n.js +0 -33
- package/core/frontend/web/middleware/llms-discovery.js +2 -2
- package/core/frontend/web/routers/serve-public-file.js +33 -3
- package/core/frontend/web/site.js +5 -4
- package/core/server/adapters/jobs/InMemoryJobsBackend.js +113 -0
- package/core/server/adapters/redirects/S3RedirectsStore.js +33 -13
- package/core/server/adapters/route-settings/S3RouteSettingsStore.js +33 -13
- package/core/server/adapters/scheduling/scheduling-default.js +6 -7
- package/core/server/adapters/storage/LocalStorageBase.js +1 -3
- package/core/server/api/endpoints/index.js +0 -4
- package/core/server/api/endpoints/member-custom-fields.js +15 -4
- package/core/server/api/endpoints/pages.js +3 -11
- package/core/server/api/endpoints/posts.js +3 -11
- package/core/server/api/endpoints/utils/api-filter-utils.js +15 -1
- package/core/server/api/endpoints/utils/serializers/input/settings.js +4 -0
- package/core/server/api/endpoints/utils/serializers/output/index.js +0 -4
- package/core/server/api/endpoints/utils/serializers/output/mappers/email-batches.js +1 -1
- package/core/server/api/endpoints/utils/serializers/output/mappers/snippets.js +0 -1
- package/core/server/api/endpoints/utils/serializers/output/member-custom-fields.js +1 -0
- package/core/server/data/exporter/table-lists.js +4 -1
- package/core/server/data/migrations/utils/schema.js +5 -3
- package/core/server/data/migrations/versions/6.57/2026-08-04-18-00-00-store-custom-field-values-as-leaf-rows.js +19 -7
- package/core/server/data/migrations/versions/6.58/2026-08-10-08-20-50-add-machine-payments-settings.js +41 -0
- package/core/server/data/migrations/versions/6.58/2026-08-10-08-21-00-add-machine-payment-events-table.js +18 -0
- package/core/server/data/migrations/versions/6.58/2026-08-10-15-24-09-reset-custom-field-definitions-for-underscored-keys.js +66 -0
- package/core/server/data/migrations/versions/6.58/2026-08-11-10-00-00-swap-custom-field-values-to-key-fk.js +65 -0
- package/core/server/data/migrations/versions/6.58/2026-08-11-10-34-25-remove-legacy-explore-integration.js +131 -0
- package/core/server/data/migrations/versions/6.58/2026-08-11-16-00-00-add-sort-order-to-members-custom-fields.js +13 -0
- package/core/server/data/migrations/versions/6.58/2026-08-11-21-22-48-rename-email-batches-provider-id.js +8 -0
- package/core/server/data/migrations/versions/6.58/2026-08-13-16-07-33-add-automation-runs-automation-id-created-at-index.js +28 -0
- package/core/server/data/schema/README.md +87 -0
- package/core/server/data/schema/commands.js +5 -2
- package/core/server/data/schema/default-settings/default-settings.json +27 -0
- package/core/server/data/schema/fixtures/fixtures.json +0 -21
- package/core/server/data/schema/schema.js +37 -9
- package/core/server/data/seeders/data-generator.js +3 -1
- package/core/server/data/seeders/importers/automation-action-edges-importer.js +38 -0
- package/core/server/data/seeders/importers/automation-action-revisions-importer.js +139 -0
- package/core/server/data/seeders/importers/automation-actions-importer.js +84 -0
- package/core/server/data/seeders/importers/automation-run-steps-importer.js +134 -0
- package/core/server/data/seeders/importers/automation-runs-importer.js +97 -0
- package/core/server/data/seeders/importers/automations-importer.js +78 -0
- package/core/server/data/seeders/importers/benefits-importer.js +1 -1
- package/core/server/data/seeders/importers/comment-reports-importer.js +5 -5
- package/core/server/data/seeders/importers/comments-importer.js +6 -6
- package/core/server/data/seeders/importers/email-batches-importer.js +9 -8
- package/core/server/data/seeders/importers/email-recipient-failures-importer.js +1 -1
- package/core/server/data/seeders/importers/email-recipients-importer.js +12 -12
- package/core/server/data/seeders/importers/emails-importer.js +7 -7
- package/core/server/data/seeders/importers/index.js +6 -0
- package/core/server/data/seeders/importers/labels-importer.js +4 -4
- package/core/server/data/seeders/importers/members-click-events-importer.js +4 -4
- package/core/server/data/seeders/importers/members-created-events-importer.js +5 -5
- package/core/server/data/seeders/importers/members-feedback-importer.js +7 -7
- package/core/server/data/seeders/importers/members-importer.js +5 -5
- package/core/server/data/seeders/importers/members-labels-importer.js +1 -1
- package/core/server/data/seeders/importers/members-login-events-importer.js +4 -4
- package/core/server/data/seeders/importers/members-newsletters-importer.js +1 -1
- package/core/server/data/seeders/importers/members-paid-subscription-events-importer.js +1 -1
- package/core/server/data/seeders/importers/members-products-importer.js +3 -3
- package/core/server/data/seeders/importers/members-status-events-importer.js +4 -4
- package/core/server/data/seeders/importers/members-stripe-customers-importer.js +3 -3
- package/core/server/data/seeders/importers/members-stripe-customers-subscriptions-importer.js +8 -8
- package/core/server/data/seeders/importers/members-subscribe-events-importer.js +3 -3
- package/core/server/data/seeders/importers/members-subscription-created-events-importer.js +4 -4
- package/core/server/data/seeders/importers/newsletters-importer.js +1 -1
- package/core/server/data/seeders/importers/offer-redemptions-importer.js +6 -6
- package/core/server/data/seeders/importers/offers-importer.js +4 -4
- package/core/server/data/seeders/importers/posts-authors-importer.js +1 -1
- package/core/server/data/seeders/importers/posts-importer.js +5 -5
- package/core/server/data/seeders/importers/posts-products-importer.js +1 -1
- package/core/server/data/seeders/importers/posts-tags-importer.js +1 -1
- package/core/server/data/seeders/importers/products-benefits-importer.js +1 -1
- package/core/server/data/seeders/importers/products-importer.js +1 -1
- package/core/server/data/seeders/importers/recommendation-click-events-importer.js +1 -1
- package/core/server/data/seeders/importers/recommendation-subscribe-events-importer.js +1 -1
- package/core/server/data/seeders/importers/recommendations-importer.js +1 -1
- package/core/server/data/seeders/importers/redirects-importer.js +1 -1
- package/core/server/data/seeders/importers/roles-users-importer.js +1 -1
- package/core/server/data/seeders/importers/stripe-prices-importer.js +1 -1
- package/core/server/data/seeders/importers/stripe-products-importer.js +1 -1
- package/core/server/data/seeders/importers/table-importer.js +92 -91
- package/core/server/data/seeders/importers/tags-importer.js +3 -3
- package/core/server/data/seeders/importers/users-importer.js +3 -3
- package/core/server/data/seeders/importers/web-mentions-importer.js +3 -3
- package/core/server/data/seeders/utils/database-date.js +14 -18
- package/core/server/data/seeders/utils/json-importer.js +1 -1
- package/core/server/data/seeders/utils/random.js +8 -7
- package/core/server/data/seeders/utils/topological-sort.js +5 -3
- package/core/server/data/tinybird/ARCHITECTURE.md +1 -2
- package/core/server/models/base/plugins/crud.js +0 -5
- package/core/server/models/index.js +2 -0
- package/core/server/models/machine-payment-event.js +18 -0
- package/core/server/models/member.js +24 -0
- package/core/server/models/settings.js +32 -3
- package/core/server/models/user.js +1 -1
- package/core/server/services/README.md +30 -0
- package/core/server/services/activitypub/activity-pub-service.js +2 -3
- package/core/server/services/adapter-manager/base-classes.js +3 -1
- package/core/server/services/auth/passwordreset.js +47 -51
- package/core/server/services/automations/database-automations-repository.js +19 -3
- package/core/server/services/automations/poll.js +2 -8
- package/core/server/services/comments/comments-service.js +4 -16
- package/core/server/services/comments/index.js +1 -3
- package/core/server/services/email-analytics/email-analytics-service-wrapper.js +68 -119
- package/core/server/services/email-analytics/email-analytics-service.js +73 -229
- package/core/server/services/email-analytics/event-processing-result.js +0 -10
- package/core/server/services/email-analytics/fetch-mailgun-events.js +25 -0
- package/core/server/services/email-analytics/index.js +35 -67
- package/core/server/services/email-analytics/lib/queries.js +88 -51
- package/core/server/services/email-analytics/newsletter-email-analytics-batch-processor.js +4 -1
- package/core/server/services/email-service/batch-sending-service.js +1 -1
- package/core/server/services/email-service/email-event-processor.js +12 -12
- package/core/server/services/email-service/events/email-bounced-event.js +8 -46
- package/core/server/services/email-service/events/email-delivered-event.js +8 -36
- package/core/server/services/email-service/events/email-opened-event.js +8 -36
- package/core/server/services/email-service/events/email-temporary-bounced-event.js +8 -46
- package/core/server/services/email-service/events/email-unsubscribed-event.js +8 -31
- package/core/server/services/email-service/events/spam-complaint-event.js +8 -31
- package/core/server/services/email-service/newsletter-email-event-storage.js +8 -7
- package/core/server/services/email-service/sending-service.js +1 -1
- package/core/server/services/email-suppression-list/mailgun-email-suppression-list.js +2 -2
- package/core/server/services/email-suppression-list/service.js +1 -3
- package/core/server/services/explore-ping/explore-ping-service.js +1 -6
- package/core/server/services/explore-ping/index.js +0 -2
- package/core/server/services/gifts/index.js +109 -3
- package/core/server/services/gifts/jobs/clean-gifts-job.js +1 -1
- package/core/server/services/gifts/jobs/send-gift-reminders-job.js +1 -1
- package/core/server/services/jobs-service/index.js +36 -0
- package/core/server/services/jobs-service/job.js +6 -0
- package/core/server/services/jobs-service/jobs-service.js +91 -0
- package/core/server/services/jobs-service/register-job-handlers.js +6 -0
- package/core/server/services/lib/mailgun-client.js +1 -1
- package/core/server/services/machine-payments/README.md +23 -0
- package/core/server/services/machine-payments/adapters/mpp-adapter.js +195 -0
- package/core/server/services/machine-payments/content-loader.js +89 -0
- package/core/server/services/machine-payments/eligibility.js +6 -0
- package/core/server/services/machine-payments/events/machine-payment-event-repository.js +58 -0
- package/core/server/services/machine-payments/events/machine-payment-event.js +46 -0
- package/core/server/services/machine-payments/index.js +87 -0
- package/core/server/services/machine-payments/pricing.js +54 -0
- package/core/server/services/machine-payments/service.js +275 -0
- package/core/server/services/machine-payments/stripe/deposit-address-store.js +157 -0
- package/core/server/services/machine-payments/stripe/payment-recorder.js +85 -0
- package/core/server/services/machine-payments/stripe/stripe-client-options.js +25 -0
- package/core/server/services/machine-payments/types.js +2 -0
- package/core/server/services/members/import-export/csv/parse.js +9 -1
- package/core/server/services/members/import-export/import/completion-email.js +61 -27
- package/core/server/services/members/import-export/import/email-template.js +28 -15
- package/core/server/services/members/import-export/import/importer.js +99 -51
- package/core/server/services/members/import-export/import/spool.js +1 -1
- package/core/server/services/members/import-export/index.js +24 -1
- package/core/server/services/members/members-api/services/member-bread-service.js +2 -2
- package/core/server/services/members/members-config-provider.js +12 -2
- package/core/server/services/members-custom-fields/actions.js +2 -1
- package/core/server/services/members-custom-fields/definitions-service.js +109 -27
- package/core/server/services/members-custom-fields/filter.js +199 -0
- package/core/server/services/members-custom-fields/key.js +37 -0
- package/core/server/services/members-custom-fields/queries.js +15 -0
- package/core/server/services/members-custom-fields/schema.js +1 -1
- package/core/server/services/members-custom-fields/values-service.js +18 -14
- package/core/server/services/members-events/last-seen-at-updater.js +1 -1
- package/core/server/services/oembed/oembed-service.js +8 -2
- package/core/server/services/permissions/can-this.js +4 -1
- package/core/server/services/posts/posts-service.js +18 -16
- package/core/server/services/public-config/config.js +1 -1
- package/core/server/services/route-settings/dynamic-routing-service.js +2 -0
- package/core/server/services/settings-helpers/settings-helpers.js +0 -1
- package/core/server/services/stats/posts-stats-service.js +0 -2
- package/core/server/services/stripe/stripe-api.js +2 -9
- package/core/server/web/api/endpoints/admin/middleware.js +0 -1
- package/core/server/web/api/endpoints/admin/routes.js +2 -3
- package/core/server/web/comments/routes.js +31 -8
- package/core/shared/config/defaults.json +17 -2
- package/core/shared/config/loader.js +11 -2
- package/core/shared/config/overrides.json +2 -0
- package/core/shared/config/secrets.js +71 -0
- package/core/shared/labs.js +4 -13
- package/core/shared/machine-payments.js +30 -0
- package/core/shared/private-site-access/index.js +68 -0
- package/package.json +48 -43
- package/pnpm-lock.yaml +1412 -1583
- package/pnpm-workspace.yaml +85 -48
- package/bin/validate-adapters.ts +0 -87
- package/components/tryghost-adapter-base-scheduling-0.2.1.tgz +0 -0
- package/components/tryghost-adapter-base-sso-0.1.1.tgz +0 -0
- package/components/tryghost-kg-card-factory-5.2.3.tgz +0 -0
- package/components/tryghost-kg-clean-basic-html-4.3.3.tgz +0 -0
- package/components/tryghost-kg-converters-1.2.4.tgz +0 -0
- package/components/tryghost-kg-default-cards-10.3.4.tgz +0 -0
- package/components/tryghost-kg-default-nodes-2.1.5.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-kg-markdown-html-renderer-7.2.3.tgz +0 -0
- package/components/tryghost-kg-utils-1.1.3.tgz +0 -0
- package/content/themes/casper/assets/built/casper.js.map +0 -1
- package/content/themes/casper/assets/built/global.css.map +0 -1
- package/content/themes/casper/assets/built/screen.css.map +0 -1
- package/content/themes/source/assets/built/screen.css.map +0 -1
- package/content/themes/source/assets/built/source.js.map +0 -1
- package/core/built/admin/assets/activitypub/layout-DM_3pqsU.js +0 -1
- package/core/built/admin/assets/add-integration-modal-DU9ghGLS.js +0 -1
- package/core/built/admin/assets/add-newsletter-modal-DFxuQrYu.js +0 -1
- package/core/built/admin/assets/announcement-bar-modal-DGCcezB6.js +0 -1
- package/core/built/admin/assets/card-aUHX0hyR.js +0 -1
- package/core/built/admin/assets/chunk.524.dce1c51e0bc30fb2718e.js +0 -25
- package/core/built/admin/assets/chunk.582.3a430d5c6a744fd475b6.js +0 -27
- package/core/built/admin/assets/code-editor-view-BcyKABrG.js +0 -1
- package/core/built/admin/assets/color-picker-field-8NoSnhNI.js +0 -1
- package/core/built/admin/assets/command-BOYMbZa_.js +0 -1
- package/core/built/admin/assets/comments-BhNM5CYl.js +0 -1
- package/core/built/admin/assets/confirmation-modal-CYD7x3UQ.js +0 -1
- package/core/built/admin/assets/custom-integration-modal-bk3lNEuQ.js +0 -1
- package/core/built/admin/assets/design-and-theme-modal-B2xYKS1I.js +0 -23
- package/core/built/admin/assets/dist-Bg8bSvLA.js +0 -6
- package/core/built/admin/assets/dist-BnFdlebk.js +0 -11
- package/core/built/admin/assets/dist-CsvXCJSe.js +0 -23
- package/core/built/admin/assets/editor-BBKrunMK.js +0 -8
- package/core/built/admin/assets/filters-Bz0HsRie.js +0 -1
- package/core/built/admin/assets/framework-provider-CmtnPXIT.js +0 -4
- package/core/built/admin/assets/index-DUXM0NlS.js +0 -67
- package/core/built/admin/assets/index-Ds2iUoF6.css +0 -1
- package/core/built/admin/assets/label-picker-Bnlru5tO.js +0 -1
- package/core/built/admin/assets/layout-yR9rlvEw.js +0 -1
- package/core/built/admin/assets/limit-modal-COmLevuY.js +0 -1
- package/core/built/admin/assets/limit-service-9fwD33lU.js +0 -20
- package/core/built/admin/assets/member-custom-fields-BvFKEpxJ.js +0 -1
- package/core/built/admin/assets/member-detail-CWdPAe3p.js +0 -1
- package/core/built/admin/assets/members-Bnt0ih2R.js +0 -11
- package/core/built/admin/assets/newsletter-DR49NyGS.js +0 -1
- package/core/built/admin/assets/newsletter-detail-modal-B45pL1pb.js +0 -1
- package/core/built/admin/assets/newsletters-DGs5BaKH.js +0 -1
- package/core/built/admin/assets/nql-lang-B7faUf8D.js +0 -19
- package/core/built/admin/assets/offers-container-modal-B0hFS8nk.js +0 -1
- package/core/built/admin/assets/overview-B-ZPNqMo.js +0 -1
- package/core/built/admin/assets/overview-D96iwoJi.js +0 -1
- package/core/built/admin/assets/portal-modal-BLFdYeDH.js +0 -1
- package/core/built/admin/assets/post-analytics-header-DK27VjJm.js +0 -1
- package/core/built/admin/assets/preferences-CuWLISHa.js +0 -5
- package/core/built/admin/assets/profile-BsKYYtuL.js +0 -1
- package/core/built/admin/assets/settings-B4vM-rgV.js +0 -4
- package/core/built/admin/assets/stripe-connect-modal-CYDDrSJm.js +0 -1
- package/core/built/admin/assets/tag-detail-CmqiC32L.js +0 -1
- package/core/built/admin/assets/tier-detail-modal-BZmAsIy4.js +0 -1
- package/core/built/admin/assets/transistor-modal-CBPDqEGy.js +0 -1
- package/core/built/admin/assets/use-check-theme-limit-error-B14lZukR.js +0 -2
- package/core/built/admin/assets/use-feature-flag-BmJoc5WN.js +0 -1
- package/core/built/admin/assets/use-filter-params-ChoV72Hw.js +0 -1
- package/core/built/admin/assets/use-limiter-Dwg8ivgQ.js +0 -2
- package/core/built/admin/assets/user-detail-modal-XODpOqNC.js +0 -1
- package/core/built/admin/assets/virtual-list-Dnoafqbk.js +0 -1
- package/core/built/admin/assets/web-z71lA1Y3.js +0 -2
- package/core/built/admin/assets/zapier-modal-Bp507Um7.js +0 -1
- package/core/frontend/meta/og-type.ts +0 -13
- package/core/frontend/services/routing/api-adapter.ts +0 -157
- package/core/frontend/services/routing/config.ts +0 -59
- package/core/frontend/services/routing/controllers/entry/canonical-url.ts +0 -14
- package/core/frontend/services/routing/controllers/entry/gift-links.ts +0 -56
- package/core/frontend/services/routing/controllers/entry/markdown.ts +0 -72
- package/core/frontend/services/routing/controllers/entry.ts +0 -120
- package/core/frontend/services/routing/controllers/rss.ts +0 -66
- package/core/frontend/services/routing/permalink-adapter.ts +0 -10
- package/core/frontend/src/utils/privacy.ts +0 -66
- package/core/server/adapters/redirects/FileStore.ts +0 -113
- package/core/server/adapters/redirects/S3RedirectsStore.ts +0 -185
- package/core/server/adapters/route-settings/FileStore.ts +0 -144
- package/core/server/adapters/route-settings/S3RouteSettingsStore.ts +0 -199
- package/core/server/adapters/route-settings/utils.ts +0 -17
- package/core/server/adapters/scheduling/deps.d.ts +0 -1
- package/core/server/adapters/scheduling/error-capture.ts +0 -91
- package/core/server/adapters/scheduling/scheduling-default.ts +0 -248
- package/core/server/adapters/scheduling/utils.ts +0 -36
- package/core/server/adapters/sso/DefaultSSOAdapter.ts +0 -15
- package/core/server/adapters/storage/LocalFilesStorage.ts +0 -24
- package/core/server/adapters/storage/LocalImagesStorage.ts +0 -24
- package/core/server/adapters/storage/LocalMediaStorage.ts +0 -24
- package/core/server/adapters/storage/LocalStorageBase.ts +0 -342
- package/core/server/adapters/storage/S3Storage.ts +0 -506
- package/core/server/adapters/storage/utils.ts +0 -60
- package/core/server/api/endpoints/automation-action-links.ts +0 -46
- package/core/server/api/endpoints/explore.js +0 -18
- package/core/server/api/endpoints/gift-links.ts +0 -87
- package/core/server/api/endpoints/libraries.d.ts +0 -2
- package/core/server/api/endpoints/member-commenting.ts +0 -92
- package/core/server/api/endpoints/member-custom-fields.ts +0 -106
- package/core/server/api/endpoints/utils/api-filter-utils.ts +0 -22
- package/core/server/api/endpoints/utils/gift-link-access.ts +0 -82
- package/core/server/api/endpoints/utils/public-cache-keys.ts +0 -47
- package/core/server/api/endpoints/utils/serializers/input/member-commenting.ts +0 -29
- package/core/server/api/endpoints/utils/serializers/output/explore.js +0 -11
- package/core/server/api/endpoints/utils/serializers/output/gift-links.ts +0 -21
- package/core/server/api/endpoints/utils/serializers/output/member-commenting.ts +0 -8
- package/core/server/api/endpoints/utils/serializers/output/member-custom-fields.ts +0 -25
- package/core/server/api/endpoints/utils/serializers/output/members.d.ts +0 -20
- package/core/server/api/endpoints/utils/serializers/output/posts-csv-transform.ts +0 -46
- package/core/server/api/endpoints/utils/serializers/output/stream-csv-response.ts +0 -47
- package/core/server/api/endpoints/utils/validate-email-sender-fields.ts +0 -42
- package/core/server/data/importer/email-template.ts +0 -213
- package/core/server/data/seeders/utils/topological-sort.ts +0 -48
- package/core/server/lib/auto-filling-map.ts +0 -23
- package/core/server/lib/bootstrap-socket.ts +0 -114
- package/core/server/lib/case-keys.ts +0 -30
- package/core/server/lib/common/to-plain.ts +0 -25
- package/core/server/lib/db-date.ts +0 -8
- package/core/server/lib/get-inbox-links.ts +0 -276
- package/core/server/lib/post-revisions.ts +0 -152
- package/core/server/lib/soonest-timer.ts +0 -57
- package/core/server/models/base/plugins/bulk-filters.ts +0 -35
- package/core/server/models/role-utils.ts +0 -56
- package/core/server/services/activitypub/activity-pub-service.ts +0 -216
- package/core/server/services/adapter-manager/adapter-manager.ts +0 -253
- package/core/server/services/adapter-manager/adapter-paths.ts +0 -26
- package/core/server/services/adapter-manager/base-classes.ts +0 -22
- package/core/server/services/adapter-manager/index.ts +0 -16
- package/core/server/services/adapter-manager/types.ts +0 -45
- package/core/server/services/adapter-manager/utils.ts +0 -308
- package/core/server/services/auth/reset-authentication.ts +0 -72
- package/core/server/services/automations/automations-api.ts +0 -396
- package/core/server/services/automations/automations-repository.ts +0 -218
- package/core/server/services/automations/constants.ts +0 -4
- package/core/server/services/automations/database-automations-repository.ts +0 -1513
- package/core/server/services/automations/database-date.ts +0 -26
- package/core/server/services/automations/fake-wait-hours-multiplier.ts +0 -12
- package/core/server/services/automations/get-scheduler-idempotency-key.ts +0 -8
- package/core/server/services/automations/mailgun-message-id.ts +0 -27
- package/core/server/services/automations/poll.ts +0 -325
- package/core/server/services/automations/scheduler-poll-time.ts +0 -38
- package/core/server/services/automations/service.ts +0 -131
- package/core/server/services/automations/stale-lock-cutoff.ts +0 -5
- package/core/server/services/comments/email-templates/new-comment-reply.txt.ts +0 -25
- package/core/server/services/comments/email-templates/new-comment.txt.ts +0 -25
- package/core/server/services/comments/email-templates/report.txt.ts +0 -34
- package/core/server/services/custom-redirects/redirect-config-parser.ts +0 -148
- package/core/server/services/custom-redirects/redirects-service.ts +0 -181
- package/core/server/services/custom-redirects/utils.ts +0 -7
- package/core/server/services/donations/donation-bookshelf-repository.ts +0 -63
- package/core/server/services/donations/donation-payment-event.ts +0 -51
- package/core/server/services/email-address/email-address-parser.d.ts +0 -12
- package/core/server/services/email-address/email-address-service.ts +0 -241
- package/core/server/services/email-analytics/automation-email-analytics-batch-processor.ts +0 -130
- package/core/server/services/email-analytics/batch-event-processor.ts +0 -18
- package/core/server/services/email-analytics/email-analytics-provider-mailgun.js +0 -57
- package/core/server/services/email-analytics/event-processing-result.ts +0 -66
- package/core/server/services/email-analytics/events/start-automation-email-analytics-job-event.ts +0 -14
- package/core/server/services/email-analytics/events/start-email-analytics-job-event.ts +0 -15
- package/core/server/services/email-analytics/index.ts +0 -109
- package/core/server/services/email-analytics/jobs/email-analytics-job-scheduler.ts +0 -126
- package/core/server/services/email-analytics/lib/queries.ts +0 -341
- package/core/server/services/email-service/domain-warming-service.ts +0 -91
- package/core/server/services/explore/explore-service.js +0 -59
- package/core/server/services/explore/index.js +0 -18
- package/core/server/services/explore-ping/explore-ping-service.ts +0 -198
- package/core/server/services/explore-ping/index.ts +0 -42
- package/core/server/services/files/file-type-utils.ts +0 -56
- package/core/server/services/gift-links/actions.ts +0 -51
- package/core/server/services/gift-links/codec.ts +0 -16
- package/core/server/services/gift-links/index.ts +0 -20
- package/core/server/services/gift-links/models.ts +0 -22
- package/core/server/services/gift-links/schema.ts +0 -33
- package/core/server/services/gift-links/serializers.ts +0 -20
- package/core/server/services/gift-links/service.ts +0 -95
- package/core/server/services/gifts/constants.ts +0 -3
- package/core/server/services/gifts/email-templates/gift-purchase-confirmation.ts +0 -43
- package/core/server/services/gifts/email-templates/gift-reminder.ts +0 -50
- package/core/server/services/gifts/gift-bookshelf-repository.ts +0 -366
- package/core/server/services/gifts/gift-checkout-offer.ts +0 -127
- package/core/server/services/gifts/gift-codec.ts +0 -62
- package/core/server/services/gifts/gift-controller.ts +0 -51
- package/core/server/services/gifts/gift-email-renderer.ts +0 -97
- package/core/server/services/gifts/gift-email-service.ts +0 -158
- package/core/server/services/gifts/gift-reminder-scheduler.ts +0 -107
- package/core/server/services/gifts/gift-schema.ts +0 -39
- package/core/server/services/gifts/gift-service-wrapper.js +0 -142
- package/core/server/services/gifts/gift-service.ts +0 -1034
- package/core/server/services/gifts/gift.ts +0 -234
- package/core/server/services/identity-tokens/identity-token-service.ts +0 -36
- package/core/server/services/indexnow-ping/constants.ts +0 -32
- package/core/server/services/indexnow-ping/index.ts +0 -35
- package/core/server/services/indexnow-ping/indexnow-ping-service.ts +0 -311
- package/core/server/services/internal-keys/index.ts +0 -53
- package/core/server/services/lib/dynamic-redirect-manager.d.ts +0 -20
- package/core/server/services/lib/in-memory-repository.ts +0 -78
- package/core/server/services/lib/member-signup-contexts.ts +0 -22
- package/core/server/services/link-redirection/link-redirect.ts +0 -24
- package/core/server/services/member-welcome-emails/constants.ts +0 -35
- package/core/server/services/members/commenting/index.ts +0 -2
- package/core/server/services/members/commenting/member-commenting-codec.ts +0 -73
- package/core/server/services/members/commenting/member-commenting.ts +0 -69
- package/core/server/services/members/create-paid-member-shim.ts +0 -40
- package/core/server/services/members/import-export/config.ts +0 -34
- package/core/server/services/members/import-export/csv/formula.ts +0 -14
- package/core/server/services/members/import-export/csv/index.ts +0 -3
- package/core/server/services/members/import-export/csv/parse.ts +0 -69
- package/core/server/services/members/import-export/csv/serialize.ts +0 -18
- package/core/server/services/members/import-export/export/exporter.ts +0 -325
- package/core/server/services/members/import-export/import/completion-email.ts +0 -146
- package/core/server/services/members/import-export/import/importer.ts +0 -489
- package/core/server/services/members/import-export/import/reader.ts +0 -33
- package/core/server/services/members/import-export/import/row.ts +0 -56
- package/core/server/services/members/import-export/import/spool.ts +0 -38
- package/core/server/services/members/import-export/index.ts +0 -143
- package/core/server/services/members/index.d.ts +0 -32
- package/core/server/services/members-custom-fields/actions.ts +0 -62
- package/core/server/services/members-custom-fields/codec.ts +0 -13
- package/core/server/services/members-custom-fields/config.ts +0 -38
- package/core/server/services/members-custom-fields/definitions-service.ts +0 -413
- package/core/server/services/members-custom-fields/index.ts +0 -50
- package/core/server/services/members-custom-fields/models.ts +0 -15
- package/core/server/services/members-custom-fields/queries.ts +0 -13
- package/core/server/services/members-custom-fields/schema.ts +0 -71
- package/core/server/services/members-custom-fields/serializers.ts +0 -22
- package/core/server/services/members-custom-fields/storage.ts +0 -122
- package/core/server/services/members-custom-fields/values-service.ts +0 -269
- package/core/server/services/notifications/notification-email.ts +0 -66
- package/core/server/services/notifications/sanitize-email-html.ts +0 -35
- package/core/server/services/post-scheduling/index.ts +0 -13
- package/core/server/services/post-scheduling/post-scheduling.ts +0 -133
- package/core/server/services/recommendations/service/bookshelf-click-event-repository.ts +0 -49
- package/core/server/services/recommendations/service/bookshelf-recommendation-repository.ts +0 -117
- package/core/server/services/recommendations/service/bookshelf-repository.ts +0 -192
- package/core/server/services/recommendations/service/bookshelf-subscribe-event-repository.ts +0 -49
- package/core/server/services/recommendations/service/click-event.ts +0 -32
- package/core/server/services/recommendations/service/in-memory-recommendation-repository.ts +0 -20
- package/core/server/services/recommendations/service/incoming-recommendation-controller.ts +0 -51
- package/core/server/services/recommendations/service/incoming-recommendation-email-renderer.ts +0 -37
- package/core/server/services/recommendations/service/incoming-recommendation-service.ts +0 -159
- package/core/server/services/recommendations/service/index.ts +0 -15
- package/core/server/services/recommendations/service/libraries.d.ts +0 -2
- package/core/server/services/recommendations/service/recommendation-controller.ts +0 -257
- package/core/server/services/recommendations/service/recommendation-metadata-service.ts +0 -132
- package/core/server/services/recommendations/service/recommendation-repository.ts +0 -13
- package/core/server/services/recommendations/service/recommendation-service.ts +0 -285
- package/core/server/services/recommendations/service/recommendation.ts +0 -200
- package/core/server/services/recommendations/service/subscribe-event.ts +0 -32
- package/core/server/services/recommendations/service/unsafe-data.ts +0 -217
- package/core/server/services/recommendations/service/well-known-service.ts +0 -47
- package/core/server/services/remote-flags/index.ts +0 -101
- package/core/server/services/remote-flags/remote-flags-service.ts +0 -236
- package/core/server/services/remote-flags/resolve.ts +0 -108
- package/core/server/services/route-settings/route-settings-parser.ts +0 -455
- package/core/server/services/route-settings/validation-errors.ts +0 -271
- package/core/server/services/route-settings/yaml-parser.d.ts +0 -4
- package/core/server/services/slack-ping/constants.ts +0 -15
- package/core/server/services/slack-ping/index.ts +0 -35
- package/core/server/services/slack-ping/slack-ping-service.ts +0 -295
- package/core/server/services/staff/email-templates/donation.txt.ts +0 -26
- package/core/server/services/staff/email-templates/gift.txt.ts +0 -27
- package/core/server/services/staff/email-templates/new-free-signup.txt.ts +0 -15
- package/core/server/services/staff/email-templates/new-gift-subscription.txt.ts +0 -26
- package/core/server/services/staff/email-templates/new-milestone-received.txt.ts +0 -20
- package/core/server/services/staff/email-templates/new-paid-cancellation.txt.ts +0 -15
- package/core/server/services/staff/email-templates/new-paid-started.txt.ts +0 -15
- package/core/server/services/staff/email-templates/recommendation-received.txt.ts +0 -23
- package/core/server/services/staff/email-templates/types.ts +0 -13
- package/core/server/services/url/lazy-find-resource.ts +0 -104
- package/core/server/services/url/lazy-url-service.ts +0 -647
- package/core/server/services/url/permalink-matcher.ts +0 -119
- package/core/server/services/url/router-filter.ts +0 -80
- package/core/server/services/verification/verification-webhook-service.ts +0 -137
- package/core/shared/config/helpers.ts +0 -133
- package/core/shared/config/index.ts +0 -12
- package/core/shared/config/loader.ts +0 -87
- package/core/shared/config/utils.ts +0 -104
- package/core/shared/errify.ts +0 -31
- package/core/shared/events-ts/index.ts +0 -8
- package/core/shared/events-ts/post-deleted-event.ts +0 -15
- package/core/shared/events-ts/posts-bulk-add-tags-event.ts +0 -13
- package/core/shared/events-ts/posts-bulk-destroyed-event.ts +0 -13
- package/core/shared/events-ts/posts-bulk-featured-event.ts +0 -13
- package/core/shared/events-ts/posts-bulk-unfeatured-event.ts +0 -13
- package/core/shared/events-ts/posts-bulk-unpublished-event.ts +0 -13
- package/core/shared/events-ts/posts-bulk-unscheduled-event.ts +0 -13
- package/core/shared/labs-flag-overrides.ts +0 -35
- package/core/shared/one-at-a-time.ts +0 -47
- package/core/shared/url-utils.ts +0 -20
|
@@ -1,1513 +0,0 @@
|
|
|
1
|
-
import errors from '@tryghost/errors';
|
|
2
|
-
import tpl from '@tryghost/tpl';
|
|
3
|
-
import crypto from 'node:crypto';
|
|
4
|
-
import ObjectId from 'bson-objectid';
|
|
5
|
-
import {dequal} from 'dequal';
|
|
6
|
-
import {type Knex} from 'knex';
|
|
7
|
-
// @ts-expect-error This module currently lacks type definitions.
|
|
8
|
-
import lexicalLib from '../../lib/lexical';
|
|
9
|
-
import urlUtils from '../../../shared/url-utils';
|
|
10
|
-
import {DEFAULT_EMAIL_DESIGN_SETTING_SLUG, MEMBER_WELCOME_EMAIL_SLUGS} from '../member-welcome-emails/constants';
|
|
11
|
-
import type {
|
|
12
|
-
AutomatedEmailEvents,
|
|
13
|
-
Automation,
|
|
14
|
-
AutomationAction,
|
|
15
|
-
AutomationEdge,
|
|
16
|
-
AutomationEmailStats,
|
|
17
|
-
AutomationSummary,
|
|
18
|
-
AutomationStepTerminalStatus,
|
|
19
|
-
AutomationStepToRun,
|
|
20
|
-
AutomationsRepository,
|
|
21
|
-
EditAutomationData,
|
|
22
|
-
Page
|
|
23
|
-
} from './automations-repository';
|
|
24
|
-
import {fromDatabaseDate, toDatabaseDate, type DatabaseDate} from './database-date';
|
|
25
|
-
import {getStaleLockCutoff} from './stale-lock-cutoff';
|
|
26
|
-
import type {ExclusifyUnion, ReadonlyDeep} from 'type-fest';
|
|
27
|
-
|
|
28
|
-
const HOUR_MS = 60 * 60 * 1000;
|
|
29
|
-
const DEFAULT_WELCOME_EMAIL_AUTOMATIONS = [{
|
|
30
|
-
name: 'Free member welcome flow',
|
|
31
|
-
slug: MEMBER_WELCOME_EMAIL_SLUGS.free
|
|
32
|
-
}, {
|
|
33
|
-
name: 'Paid member welcome flow',
|
|
34
|
-
slug: MEMBER_WELCOME_EMAIL_SLUGS.paid
|
|
35
|
-
}];
|
|
36
|
-
|
|
37
|
-
const messages = {
|
|
38
|
-
invalidAutomationActionRevision: 'Automation action "{actionId}" of type "{actionType}" is missing required revision field "{field}".',
|
|
39
|
-
conflictingAutomationActionId: 'Automation action "{actionId}" already exists and cannot be inserted.',
|
|
40
|
-
conflictingAutomationActionType: 'Automation action "{actionId}" already exists with a different type.',
|
|
41
|
-
defaultEmailDesignSettingNotFound: 'Default automated email design setting not found.'
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const DEFAULT_EMAIL_DESIGN_SETTING_REFERENCE = DEFAULT_EMAIL_DESIGN_SETTING_SLUG;
|
|
45
|
-
|
|
46
|
-
interface AutomationRow {
|
|
47
|
-
id: string;
|
|
48
|
-
slug: string;
|
|
49
|
-
name: string;
|
|
50
|
-
status: string;
|
|
51
|
-
created_at: DatabaseDate;
|
|
52
|
-
updated_at: DatabaseDate;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
interface ActionRow {
|
|
57
|
-
id: string;
|
|
58
|
-
type: 'wait' | 'send_email';
|
|
59
|
-
wait_hours: number | null;
|
|
60
|
-
email_subject: string | null;
|
|
61
|
-
email_lexical: string | null;
|
|
62
|
-
email_design_setting_id: string | null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
type ActionStatsRow = {
|
|
66
|
-
action_id: string;
|
|
67
|
-
email_clicked_count: number | null;
|
|
68
|
-
email_sent_count: number | null;
|
|
69
|
-
email_opened_count: number | null;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
type ActionLinkRow = {
|
|
73
|
-
url: string;
|
|
74
|
-
clicked_count: string | number;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
type ActionRevisionRow = {
|
|
78
|
-
action_id: string;
|
|
79
|
-
created_at: DatabaseDate;
|
|
80
|
-
wait_hours: number | null;
|
|
81
|
-
email_subject: string | null;
|
|
82
|
-
email_lexical: string | null;
|
|
83
|
-
email_design_setting_id: string | null;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
interface EdgeRow {
|
|
87
|
-
source_action_id: string;
|
|
88
|
-
target_action_id: string;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
type NextActionRevisionRow = {
|
|
92
|
-
automation_id: string;
|
|
93
|
-
action_id: string;
|
|
94
|
-
automation_action_revision_id: string;
|
|
95
|
-
type: 'wait' | 'send_email';
|
|
96
|
-
wait_hours: number | null;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
type StepToRunRow = {
|
|
100
|
-
id: string;
|
|
101
|
-
locked_by: string;
|
|
102
|
-
automation_run_id: string;
|
|
103
|
-
automation_id: string;
|
|
104
|
-
automation_slug: string;
|
|
105
|
-
automation_status: 'inactive' | 'active';
|
|
106
|
-
member_id: string | null;
|
|
107
|
-
member_email: string;
|
|
108
|
-
action_id: string;
|
|
109
|
-
automation_action_revision_id: string;
|
|
110
|
-
type: string;
|
|
111
|
-
ready_at: DatabaseDate;
|
|
112
|
-
step_attempts: number;
|
|
113
|
-
wait_hours: number | null;
|
|
114
|
-
email_subject: string | null;
|
|
115
|
-
email_lexical: string | null;
|
|
116
|
-
email_design_setting_id: string | null;
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
type WaitActionData = Extract<AutomationAction, {type: 'wait'}>['data'];
|
|
120
|
-
type SendEmailActionData = Extract<AutomationAction, {type: 'send_email'}>['data'];
|
|
121
|
-
type RevisionDataFor<ActionDataT> = {
|
|
122
|
-
[FieldT in keyof ActionDataT]: ActionRevisionRow[FieldT & keyof ActionRevisionRow];
|
|
123
|
-
};
|
|
124
|
-
type WaitRevisionData = RevisionDataFor<WaitActionData>;
|
|
125
|
-
type SendEmailRevisionData = RevisionDataFor<SendEmailActionData>;
|
|
126
|
-
|
|
127
|
-
type ActionToInsert = {
|
|
128
|
-
id: string;
|
|
129
|
-
created_at: string;
|
|
130
|
-
updated_at: string;
|
|
131
|
-
automation_id: string;
|
|
132
|
-
type: AutomationAction['type'];
|
|
133
|
-
};
|
|
134
|
-
type ActionRevisionToInsert = {
|
|
135
|
-
actionId: string;
|
|
136
|
-
action: AutomationAction;
|
|
137
|
-
createdAt: string;
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
export function createDatabaseAutomationsRepository({
|
|
141
|
-
knex,
|
|
142
|
-
fakeWaitHoursMultiplier
|
|
143
|
-
}: {
|
|
144
|
-
knex: Knex;
|
|
145
|
-
fakeWaitHoursMultiplier: number | null;
|
|
146
|
-
}): AutomationsRepository {
|
|
147
|
-
return {
|
|
148
|
-
async browse(): Promise<Page<AutomationSummary>> {
|
|
149
|
-
return await knex.transaction(async (trx) => {
|
|
150
|
-
await ensureDefaultAutomations(trx);
|
|
151
|
-
const rows = await loadAutomations(trx);
|
|
152
|
-
return {
|
|
153
|
-
data: rows.map(row => buildAutomationSummary(row)),
|
|
154
|
-
meta: {
|
|
155
|
-
pagination: buildPagination(rows.length)
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
});
|
|
159
|
-
},
|
|
160
|
-
|
|
161
|
-
async getById(id: string): Promise<Automation | null> {
|
|
162
|
-
return await knex.transaction(async (trx) => {
|
|
163
|
-
const automation = await loadAutomation(trx, id);
|
|
164
|
-
|
|
165
|
-
if (!automation) {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return await buildAutomation(trx, automation);
|
|
170
|
-
});
|
|
171
|
-
},
|
|
172
|
-
|
|
173
|
-
async getAutomationActionLinks(automationId, actionId) {
|
|
174
|
-
const action = await knex('automation_actions')
|
|
175
|
-
.select('id')
|
|
176
|
-
.where({
|
|
177
|
-
id: actionId,
|
|
178
|
-
automation_id: automationId
|
|
179
|
-
})
|
|
180
|
-
.whereNull('deleted_at')
|
|
181
|
-
.first();
|
|
182
|
-
|
|
183
|
-
if (!action) {
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const rows = await knex('redirects as redirects')
|
|
188
|
-
.countDistinct({clicked_count: 'members_click_events.member_id'})
|
|
189
|
-
.select<ActionLinkRow[]>(knex.raw('MIN(??) as ??', ['redirects.to', 'url']))
|
|
190
|
-
.innerJoin('automation_action_revisions as revisions', 'revisions.id', 'redirects.automation_action_revision_id')
|
|
191
|
-
.leftJoin('members_click_events', 'members_click_events.redirect_id', 'redirects.id')
|
|
192
|
-
.where('revisions.action_id', actionId)
|
|
193
|
-
.whereNotNull('redirects.to_hash')
|
|
194
|
-
.groupBy('redirects.to_hash')
|
|
195
|
-
.orderBy('clicked_count', 'desc')
|
|
196
|
-
.orderBy('url', 'asc');
|
|
197
|
-
|
|
198
|
-
return rows.map(row => ({
|
|
199
|
-
url: urlUtils.transformReadyToAbsolute(row.url),
|
|
200
|
-
clicked_count: Number(row.clicked_count)
|
|
201
|
-
}));
|
|
202
|
-
},
|
|
203
|
-
|
|
204
|
-
async edit(id: string, data: EditAutomationData): Promise<Automation | null> {
|
|
205
|
-
return await knex.transaction(async (trx) => {
|
|
206
|
-
const automation = await loadAutomation(trx, id);
|
|
207
|
-
|
|
208
|
-
if (!automation) {
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const now = new Date();
|
|
213
|
-
|
|
214
|
-
const updatedAutomation = await updateAutomation(trx, {
|
|
215
|
-
...automation,
|
|
216
|
-
status: data.status,
|
|
217
|
-
updated_at: toDatabaseDate(now)
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
await replaceAutomationGraph(trx, updatedAutomation.id, data.actions, data.edges);
|
|
221
|
-
|
|
222
|
-
if (updatedAutomation.status === 'inactive') {
|
|
223
|
-
await cancelCancelablePendingStepsForAutomation(trx, updatedAutomation.id, now);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return await buildAutomation(trx, updatedAutomation);
|
|
227
|
-
});
|
|
228
|
-
},
|
|
229
|
-
|
|
230
|
-
async trigger(options: {
|
|
231
|
-
memberEmail: string;
|
|
232
|
-
memberId: string;
|
|
233
|
-
memberStatus: 'free' | 'paid';
|
|
234
|
-
}): Promise<void> {
|
|
235
|
-
return await knex.transaction(trx => trigger(trx, {
|
|
236
|
-
...options,
|
|
237
|
-
fakeWaitHoursMultiplier
|
|
238
|
-
}));
|
|
239
|
-
},
|
|
240
|
-
|
|
241
|
-
async fetchAndLockSteps(limit: number): Promise<{
|
|
242
|
-
steps: AutomationStepToRun[],
|
|
243
|
-
nextStepReadyAt: null | Date;
|
|
244
|
-
}> {
|
|
245
|
-
return await knex.transaction(trx => fetchAndLockSteps(trx, limit));
|
|
246
|
-
},
|
|
247
|
-
|
|
248
|
-
async finishStepAndEnqueueNext(step: AutomationStepToRun): Promise<Date | null> {
|
|
249
|
-
return await knex.transaction(trx => finishStepAndEnqueueNext(trx, {
|
|
250
|
-
step,
|
|
251
|
-
fakeWaitHoursMultiplier
|
|
252
|
-
}));
|
|
253
|
-
},
|
|
254
|
-
|
|
255
|
-
async markStepTerminal(step: AutomationStepToRun, status: AutomationStepTerminalStatus): Promise<boolean> {
|
|
256
|
-
return await knex.transaction(trx => markStepTerminal(trx, step, status));
|
|
257
|
-
},
|
|
258
|
-
|
|
259
|
-
async retryStep(step: AutomationStepToRun, retryAt: Date): Promise<boolean> {
|
|
260
|
-
return await knex.transaction(trx => retryStep(trx, step, retryAt));
|
|
261
|
-
},
|
|
262
|
-
|
|
263
|
-
async recordEmailSent(options): Promise<void> {
|
|
264
|
-
await knex.transaction(async (trx) => {
|
|
265
|
-
await trx('automation_action_revisions')
|
|
266
|
-
.where('id', options.automationActionRevisionId)
|
|
267
|
-
.update({
|
|
268
|
-
email_sent_count: trx.raw('COALESCE(??, 0) + ?', ['email_sent_count', 1])
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
const now = toDatabaseDate(new Date());
|
|
272
|
-
await trx('automated_email_recipients').insert({
|
|
273
|
-
id: ObjectId().toHexString(),
|
|
274
|
-
member_id: options.memberId,
|
|
275
|
-
member_uuid: options.memberUuid,
|
|
276
|
-
member_email: options.memberEmail,
|
|
277
|
-
member_name: options.memberName,
|
|
278
|
-
automation_action_revision_id: options.automationActionRevisionId,
|
|
279
|
-
automation_run_step_id: options.automationRunStepId,
|
|
280
|
-
...(options.mailgunMessageId ? {mailgun_message_id: options.mailgunMessageId} : {}),
|
|
281
|
-
track_clicks: options.trackClicks,
|
|
282
|
-
track_opens: options.trackOpens,
|
|
283
|
-
created_at: now,
|
|
284
|
-
updated_at: now
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
},
|
|
288
|
-
|
|
289
|
-
async getAutomatedEmailRecipientsByMailgunIds(mailgunMessageIds) {
|
|
290
|
-
if (mailgunMessageIds.length === 0) {
|
|
291
|
-
return [];
|
|
292
|
-
}
|
|
293
|
-
return await knex('automated_email_recipients')
|
|
294
|
-
.select('id', 'mailgun_message_id', 'automation_action_revision_id')
|
|
295
|
-
.whereNotNull('automation_action_revision_id')
|
|
296
|
-
.whereIn('mailgun_message_id', mailgunMessageIds);
|
|
297
|
-
},
|
|
298
|
-
|
|
299
|
-
async trackEmailDeliveredAndOpened(eventsByAutomatedEmailRecipientId) {
|
|
300
|
-
if (eventsByAutomatedEmailRecipientId.size === 0) {
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
await knex.transaction(async (trx) => {
|
|
305
|
-
const revisionIds = new Set<string>();
|
|
306
|
-
for (const {openedAt, automationActionRevisionId} of eventsByAutomatedEmailRecipientId.values()) {
|
|
307
|
-
if (openedAt) {
|
|
308
|
-
revisionIds.add(automationActionRevisionId);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const orderedRevisionIds = await lockActionRevisions(trx, revisionIds);
|
|
313
|
-
|
|
314
|
-
const notYetOpened = await lockNotYetOpened(trx, eventsByAutomatedEmailRecipientId);
|
|
315
|
-
const newOpensPerRevision = new Map<string, number>();
|
|
316
|
-
|
|
317
|
-
for (const [id, {deliveredAt, openedAt, automationActionRevisionId}] of eventsByAutomatedEmailRecipientId) {
|
|
318
|
-
const updates: Record<string, Knex.Raw> = {};
|
|
319
|
-
if (deliveredAt) {
|
|
320
|
-
updates.delivered_at = trx.raw('CASE WHEN delivered_at IS NULL OR delivered_at > ? THEN ? ELSE delivered_at END', [deliveredAt, deliveredAt]);
|
|
321
|
-
}
|
|
322
|
-
if (openedAt) {
|
|
323
|
-
updates.opened_at = trx.raw('CASE WHEN opened_at IS NULL OR opened_at > ? THEN ? ELSE opened_at END', [openedAt, openedAt]);
|
|
324
|
-
}
|
|
325
|
-
if (Object.keys(updates).length === 0) {
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
await trx('automated_email_recipients')
|
|
329
|
-
.where({id})
|
|
330
|
-
.update(updates);
|
|
331
|
-
|
|
332
|
-
if (openedAt && notYetOpened.has(id)) {
|
|
333
|
-
newOpensPerRevision.set(
|
|
334
|
-
automationActionRevisionId,
|
|
335
|
-
(newOpensPerRevision.get(automationActionRevisionId) ?? 0) + 1
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
for (const id of orderedRevisionIds) {
|
|
341
|
-
const opens = newOpensPerRevision.get(id);
|
|
342
|
-
if (!opens) {
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
await trx('automation_action_revisions')
|
|
346
|
-
.where({id})
|
|
347
|
-
.update({
|
|
348
|
-
email_opened_count: trx.raw('COALESCE(email_opened_count, 0) + ?', [opens])
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
},
|
|
353
|
-
|
|
354
|
-
async trackEmailClicked({automationActionRevisionId, automationRunStepId, memberId, clickedAt}, {transacting} = {}) {
|
|
355
|
-
const trackClick = async (trx: Knex.Transaction) => {
|
|
356
|
-
await lockActionRevisions(trx, [automationActionRevisionId]);
|
|
357
|
-
|
|
358
|
-
const recipient = await trx('automated_email_recipients')
|
|
359
|
-
.select('id', 'clicked_at')
|
|
360
|
-
.where({
|
|
361
|
-
automation_action_revision_id: automationActionRevisionId,
|
|
362
|
-
automation_run_step_id: automationRunStepId,
|
|
363
|
-
member_id: memberId,
|
|
364
|
-
track_clicks: true
|
|
365
|
-
})
|
|
366
|
-
.forUpdate()
|
|
367
|
-
.first();
|
|
368
|
-
|
|
369
|
-
if (!recipient || recipient.clicked_at !== null) {
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const updated = await trx('automated_email_recipients')
|
|
374
|
-
.where({id: recipient.id})
|
|
375
|
-
.whereNull('clicked_at')
|
|
376
|
-
.update({clicked_at: clickedAt});
|
|
377
|
-
|
|
378
|
-
if (updated === 0) {
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
await trx('automation_action_revisions')
|
|
383
|
-
.where({id: automationActionRevisionId})
|
|
384
|
-
.update({
|
|
385
|
-
email_clicked_count: trx.raw('COALESCE(email_clicked_count, 0) + 1')
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
if (transacting) {
|
|
391
|
-
await trackClick(transacting);
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
await knex.transaction(trackClick);
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Lock revisions before recipients because inserting a recipient takes a shared
|
|
402
|
-
* foreign-key lock on its revision. Updating that revision later can deadlock
|
|
403
|
-
* with another transaction that has already locked the revision and is waiting
|
|
404
|
-
* for the recipient. Keep multi-revision lock acquisition deterministic.
|
|
405
|
-
*/
|
|
406
|
-
async function lockActionRevisions(
|
|
407
|
-
trx: Knex.Transaction,
|
|
408
|
-
revisionIds: Iterable<string>
|
|
409
|
-
): Promise<string[]> {
|
|
410
|
-
const sortedRevisionIds = [...new Set(revisionIds)]
|
|
411
|
-
.sort((left, right) => left.localeCompare(right));
|
|
412
|
-
|
|
413
|
-
if (sortedRevisionIds.length > 0) {
|
|
414
|
-
await trx('automation_action_revisions')
|
|
415
|
-
.select('id')
|
|
416
|
-
.whereIn('id', sortedRevisionIds)
|
|
417
|
-
.forUpdate();
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return sortedRevisionIds;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Which of these recipients have yet to open, and so should count towards their
|
|
425
|
-
* revision's open count. Locks them for the transaction, so a worker racing on
|
|
426
|
-
* the same open reads them as opened and doesn't count them a second time.
|
|
427
|
-
*/
|
|
428
|
-
async function lockNotYetOpened(
|
|
429
|
-
trx: Knex.Transaction,
|
|
430
|
-
eventsByAutomatedEmailRecipientId: ReadonlyDeep<Map<string, AutomatedEmailEvents>>
|
|
431
|
-
): Promise<Set<string>> {
|
|
432
|
-
const ids: string[] = [];
|
|
433
|
-
for (const [id, {openedAt}] of eventsByAutomatedEmailRecipientId) {
|
|
434
|
-
if (openedAt) {
|
|
435
|
-
ids.push(id);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (ids.length === 0) {
|
|
440
|
-
return new Set();
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const rows = await trx('automated_email_recipients')
|
|
444
|
-
.select('id')
|
|
445
|
-
.whereIn('id', ids)
|
|
446
|
-
.whereNull('opened_at')
|
|
447
|
-
.forUpdate();
|
|
448
|
-
|
|
449
|
-
return new Set(rows.map(row => row.id));
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
async function ensureDefaultAutomations(trx: Knex.Transaction): Promise<void> {
|
|
453
|
-
for (const defaults of DEFAULT_WELCOME_EMAIL_AUTOMATIONS) {
|
|
454
|
-
const automation = await ensureAutomation(trx, defaults);
|
|
455
|
-
await ensureWelcomeEmailAction(trx, automation.id);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
async function ensureAutomation(
|
|
460
|
-
trx: Knex.Transaction,
|
|
461
|
-
defaults: Readonly<{name: string; slug: string}>
|
|
462
|
-
): Promise<AutomationRow> {
|
|
463
|
-
const now = toDatabaseDate(new Date());
|
|
464
|
-
const id = ObjectId().toHexString();
|
|
465
|
-
|
|
466
|
-
await trx('automations')
|
|
467
|
-
.insert({
|
|
468
|
-
id,
|
|
469
|
-
status: 'inactive',
|
|
470
|
-
name: defaults.name,
|
|
471
|
-
slug: defaults.slug,
|
|
472
|
-
created_at: now,
|
|
473
|
-
updated_at: now
|
|
474
|
-
})
|
|
475
|
-
.onConflict('slug')
|
|
476
|
-
.ignore();
|
|
477
|
-
|
|
478
|
-
return requireAutomation(await loadAutomationBySlug(trx, defaults.slug), defaults.slug);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
async function ensureWelcomeEmailAction(trx: Knex.Transaction, automationId: string): Promise<void> {
|
|
482
|
-
const hasActions = await trx('automation_actions')
|
|
483
|
-
.where('automation_id', automationId)
|
|
484
|
-
.whereNull('deleted_at')
|
|
485
|
-
.first('id');
|
|
486
|
-
if (hasActions) {
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
await trx('automations')
|
|
491
|
-
.select('id')
|
|
492
|
-
.where('id', automationId)
|
|
493
|
-
.forUpdate()
|
|
494
|
-
.first();
|
|
495
|
-
|
|
496
|
-
const email = await trx('welcome_email_automated_emails')
|
|
497
|
-
.select('subject', 'lexical', 'email_design_setting_id')
|
|
498
|
-
.where('welcome_email_automation_id', automationId)
|
|
499
|
-
.orderBy([
|
|
500
|
-
'created_at',
|
|
501
|
-
'id'
|
|
502
|
-
])
|
|
503
|
-
.first();
|
|
504
|
-
|
|
505
|
-
if (!email) {
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const now = toDatabaseDate(new Date());
|
|
510
|
-
const actionId = ObjectId().toHexString();
|
|
511
|
-
|
|
512
|
-
await insertActions(trx, [{
|
|
513
|
-
id: actionId,
|
|
514
|
-
created_at: now,
|
|
515
|
-
updated_at: now,
|
|
516
|
-
automation_id: automationId,
|
|
517
|
-
type: 'send_email'
|
|
518
|
-
}]);
|
|
519
|
-
await insertActionRevisions(trx, [{
|
|
520
|
-
actionId,
|
|
521
|
-
action: {
|
|
522
|
-
id: actionId,
|
|
523
|
-
type: 'send_email',
|
|
524
|
-
data: {
|
|
525
|
-
email_subject: email.subject,
|
|
526
|
-
email_lexical: email.lexical ?? '',
|
|
527
|
-
email_design_setting_id: email.email_design_setting_id
|
|
528
|
-
}
|
|
529
|
-
},
|
|
530
|
-
createdAt: getNextRevisionCreatedAt(null, now)
|
|
531
|
-
}]);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
async function trigger(trx: Knex.Transaction, options: Readonly<{
|
|
535
|
-
memberEmail: string;
|
|
536
|
-
memberId: string;
|
|
537
|
-
memberStatus: 'free' | 'paid';
|
|
538
|
-
fakeWaitHoursMultiplier: number | null;
|
|
539
|
-
}>): Promise<void> {
|
|
540
|
-
const {
|
|
541
|
-
memberEmail,
|
|
542
|
-
memberId,
|
|
543
|
-
memberStatus,
|
|
544
|
-
fakeWaitHoursMultiplier
|
|
545
|
-
} = options;
|
|
546
|
-
|
|
547
|
-
const firstAction = await findFirstActionRevision(trx, memberStatus);
|
|
548
|
-
if (!firstAction) {
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const now = new Date();
|
|
553
|
-
const nowString = toDatabaseDate(now);
|
|
554
|
-
|
|
555
|
-
const readyAt = getReadyAtForAction(firstAction, now, fakeWaitHoursMultiplier);
|
|
556
|
-
|
|
557
|
-
const run = {
|
|
558
|
-
id: ObjectId().toHexString(),
|
|
559
|
-
created_at: nowString,
|
|
560
|
-
updated_at: nowString,
|
|
561
|
-
automation_id: firstAction.automation_id,
|
|
562
|
-
member_id: memberId,
|
|
563
|
-
member_email: memberEmail
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
await trx('automation_runs').insert(run);
|
|
567
|
-
await insertRunStep(trx, {
|
|
568
|
-
automationRunId: run.id,
|
|
569
|
-
automationActionRevisionId: firstAction.automation_action_revision_id,
|
|
570
|
-
now,
|
|
571
|
-
readyAt
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
async function insertRunStep(trx: Knex.Transaction, {
|
|
576
|
-
automationRunId,
|
|
577
|
-
automationActionRevisionId,
|
|
578
|
-
now,
|
|
579
|
-
readyAt
|
|
580
|
-
}: ReadonlyDeep<{
|
|
581
|
-
automationRunId: string;
|
|
582
|
-
automationActionRevisionId: string;
|
|
583
|
-
now: Date;
|
|
584
|
-
readyAt: Date;
|
|
585
|
-
}>): Promise<void> {
|
|
586
|
-
const nowString = toDatabaseDate(now);
|
|
587
|
-
|
|
588
|
-
await trx('automation_run_steps').insert({
|
|
589
|
-
id: ObjectId().toHexString(),
|
|
590
|
-
created_at: nowString,
|
|
591
|
-
updated_at: nowString,
|
|
592
|
-
automation_run_id: automationRunId,
|
|
593
|
-
automation_action_revision_id: automationActionRevisionId,
|
|
594
|
-
ready_at: toDatabaseDate(readyAt)
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
async function fetchAndLockSteps(trx: Knex.Transaction, limit: number): Promise<{
|
|
599
|
-
steps: AutomationStepToRun[],
|
|
600
|
-
nextStepReadyAt: null | Date;
|
|
601
|
-
}> {
|
|
602
|
-
// Two things make this tricky:
|
|
603
|
-
//
|
|
604
|
-
// - We want to do row-level locking, so multiple calls don't step on each other.
|
|
605
|
-
// - We can't `UPDATE` a fixed number of rows.
|
|
606
|
-
//
|
|
607
|
-
// To get around these problems, here's what we do:
|
|
608
|
-
//
|
|
609
|
-
// 1. Select up to `limit` candidate rows.
|
|
610
|
-
// 2. Try to lock those rows.
|
|
611
|
-
// 3. Select any rows we successfully locked.
|
|
612
|
-
|
|
613
|
-
const now = new Date();
|
|
614
|
-
const nowString = toDatabaseDate(now);
|
|
615
|
-
const staleLockCutoff = getStaleLockCutoff(now);
|
|
616
|
-
const staleLockCutoffString = toDatabaseDate(staleLockCutoff);
|
|
617
|
-
const lockId = crypto.randomUUID();
|
|
618
|
-
|
|
619
|
-
// 1. Select up to `limit` candidate rows.
|
|
620
|
-
const candidates: ReadonlyArray<{id: string}> = await trx('automation_run_steps')
|
|
621
|
-
.select('id')
|
|
622
|
-
.where('status', 'pending')
|
|
623
|
-
.where('ready_at', '<=', nowString)
|
|
624
|
-
.where((builder) => {
|
|
625
|
-
builder
|
|
626
|
-
.whereNull('locked_by')
|
|
627
|
-
.orWhere('locked_at', '<', staleLockCutoffString);
|
|
628
|
-
})
|
|
629
|
-
.orderBy([
|
|
630
|
-
'ready_at',
|
|
631
|
-
'created_at',
|
|
632
|
-
'id'
|
|
633
|
-
])
|
|
634
|
-
.limit(limit);
|
|
635
|
-
if (candidates.length === 0) {
|
|
636
|
-
return {
|
|
637
|
-
steps: [],
|
|
638
|
-
nextStepReadyAt: await findNextPendingReadyAt(trx, staleLockCutoff)
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
const candidateIds = candidates.map(candidate => candidate.id);
|
|
643
|
-
|
|
644
|
-
// 2. Try to lock those rows.
|
|
645
|
-
await trx('automation_run_steps')
|
|
646
|
-
.update({
|
|
647
|
-
locked_by: lockId,
|
|
648
|
-
locked_at: nowString,
|
|
649
|
-
started_at: nowString,
|
|
650
|
-
updated_at: nowString
|
|
651
|
-
})
|
|
652
|
-
.increment('step_attempts', 1)
|
|
653
|
-
.whereIn('id', candidateIds)
|
|
654
|
-
.where('status', 'pending')
|
|
655
|
-
.where('ready_at', '<=', nowString)
|
|
656
|
-
.where((builder) => {
|
|
657
|
-
builder
|
|
658
|
-
.whereNull('locked_by')
|
|
659
|
-
.orWhere('locked_at', '<', staleLockCutoffString);
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
// 3. Select any rows we successfully locked.
|
|
663
|
-
const rows: StepToRunRow[] = await trx('automation_run_steps as step')
|
|
664
|
-
.select(
|
|
665
|
-
'step.id as id',
|
|
666
|
-
'step.locked_by as locked_by',
|
|
667
|
-
'step.automation_run_id as automation_run_id',
|
|
668
|
-
'run.automation_id as automation_id',
|
|
669
|
-
'automation.slug as automation_slug',
|
|
670
|
-
'automation.status as automation_status',
|
|
671
|
-
'run.member_id as member_id',
|
|
672
|
-
'run.member_email as member_email',
|
|
673
|
-
'action.id as action_id',
|
|
674
|
-
'revision.id as automation_action_revision_id',
|
|
675
|
-
'action.type as type',
|
|
676
|
-
'step.ready_at as ready_at',
|
|
677
|
-
'step.step_attempts as step_attempts',
|
|
678
|
-
'revision.wait_hours as wait_hours',
|
|
679
|
-
'revision.email_subject as email_subject',
|
|
680
|
-
'revision.email_lexical as email_lexical',
|
|
681
|
-
'revision.email_design_setting_id as email_design_setting_id'
|
|
682
|
-
)
|
|
683
|
-
.innerJoin('automation_runs as run', 'run.id', 'step.automation_run_id')
|
|
684
|
-
.innerJoin('automations as automation', 'automation.id', 'run.automation_id')
|
|
685
|
-
.innerJoin('automation_action_revisions as revision', 'revision.id', 'step.automation_action_revision_id')
|
|
686
|
-
.innerJoin('automation_actions as action', 'action.id', 'revision.action_id')
|
|
687
|
-
.whereIn('step.id', candidateIds)
|
|
688
|
-
.where('step.locked_by', lockId)
|
|
689
|
-
.orderBy([
|
|
690
|
-
'step.ready_at',
|
|
691
|
-
'step.created_at',
|
|
692
|
-
'step.id'
|
|
693
|
-
]);
|
|
694
|
-
|
|
695
|
-
return {
|
|
696
|
-
steps: rows.map(row => buildStepToRun(row)),
|
|
697
|
-
nextStepReadyAt: await findNextPendingReadyAt(trx, staleLockCutoff)
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
async function findNextPendingReadyAt(trx: Knex.Transaction, staleLockCutoff: Readonly<Date>): Promise<Date | null> {
|
|
702
|
-
const row = await trx('automation_run_steps')
|
|
703
|
-
.select({next_ready_at: 'ready_at'})
|
|
704
|
-
.where('status', 'pending')
|
|
705
|
-
.where((builder) => {
|
|
706
|
-
builder
|
|
707
|
-
.whereNull('locked_by')
|
|
708
|
-
.orWhere('locked_at', '<', toDatabaseDate(staleLockCutoff));
|
|
709
|
-
})
|
|
710
|
-
.orderBy('ready_at')
|
|
711
|
-
.first();
|
|
712
|
-
return row?.next_ready_at ? fromDatabaseDate(row.next_ready_at) : null;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
function buildStepToRun(row: ReadonlyDeep<StepToRunRow>): AutomationStepToRun {
|
|
716
|
-
const base = {
|
|
717
|
-
id: row.id,
|
|
718
|
-
step_attempts: row.step_attempts,
|
|
719
|
-
ready_at: fromDatabaseDate(row.ready_at),
|
|
720
|
-
locked_by: row.locked_by,
|
|
721
|
-
automation_run_id: row.automation_run_id,
|
|
722
|
-
automation_id: row.automation_id,
|
|
723
|
-
automation_slug: row.automation_slug,
|
|
724
|
-
automation_status: row.automation_status,
|
|
725
|
-
member_id: row.member_id,
|
|
726
|
-
member_email: row.member_email,
|
|
727
|
-
action_id: row.action_id,
|
|
728
|
-
automation_action_revision_id: row.automation_action_revision_id
|
|
729
|
-
};
|
|
730
|
-
|
|
731
|
-
switch (row.type) {
|
|
732
|
-
case 'wait':
|
|
733
|
-
return {
|
|
734
|
-
...base,
|
|
735
|
-
type: 'wait',
|
|
736
|
-
wait_hours: requireValue(row, 'wait_hours')
|
|
737
|
-
};
|
|
738
|
-
case 'send_email':
|
|
739
|
-
return {
|
|
740
|
-
...base,
|
|
741
|
-
type: 'send_email',
|
|
742
|
-
email_subject: requireValue(row, 'email_subject'),
|
|
743
|
-
email_lexical: requireValue(row, 'email_lexical'),
|
|
744
|
-
email_design_setting_id: row.email_design_setting_id
|
|
745
|
-
};
|
|
746
|
-
default:
|
|
747
|
-
throw new errors.InternalServerError({
|
|
748
|
-
message: `Unexpected action type from database: ${row.type}`
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
async function findFirstActionRevision(trx: Knex.Transaction, memberStatus: 'free' | 'paid'): Promise<NextActionRevisionRow | null> {
|
|
754
|
-
const automationSlug: NonNullable<string> = MEMBER_WELCOME_EMAIL_SLUGS[memberStatus];
|
|
755
|
-
|
|
756
|
-
const row = await trx('automations as automation')
|
|
757
|
-
.select(
|
|
758
|
-
'automation.id as automation_id',
|
|
759
|
-
'actions.id as action_id',
|
|
760
|
-
'revisions.id as automation_action_revision_id',
|
|
761
|
-
'actions.type as type',
|
|
762
|
-
'revisions.wait_hours as wait_hours'
|
|
763
|
-
)
|
|
764
|
-
.innerJoin('automation_actions as actions', 'actions.automation_id', 'automation.id')
|
|
765
|
-
.innerJoin('automation_action_revisions as revisions', 'revisions.action_id', 'actions.id')
|
|
766
|
-
.where('automation.slug', automationSlug)
|
|
767
|
-
.where('automation.status', 'active')
|
|
768
|
-
.whereNull('actions.deleted_at')
|
|
769
|
-
.whereNotExists(trx('automation_action_edges as edge')
|
|
770
|
-
.select('edge.target_action_id')
|
|
771
|
-
.innerJoin('automation_actions as source_actions', 'source_actions.id', 'edge.source_action_id')
|
|
772
|
-
.whereNull('source_actions.deleted_at')
|
|
773
|
-
.where('edge.target_action_id', trx.ref('actions.id')))
|
|
774
|
-
.where('revisions.created_at', trx('automation_action_revisions')
|
|
775
|
-
.max('created_at')
|
|
776
|
-
.where('action_id', trx.ref('actions.id')))
|
|
777
|
-
.orderBy([
|
|
778
|
-
'actions.created_at',
|
|
779
|
-
'actions.id'
|
|
780
|
-
])
|
|
781
|
-
.first();
|
|
782
|
-
|
|
783
|
-
return row ?? null;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
async function finishStepAndEnqueueNext(
|
|
787
|
-
trx: Knex.Transaction,
|
|
788
|
-
options: Readonly<{
|
|
789
|
-
step: Pick<AutomationStepToRun, 'id' | 'locked_by' | 'action_id' | 'automation_run_id'>;
|
|
790
|
-
fakeWaitHoursMultiplier: number | null;
|
|
791
|
-
}>
|
|
792
|
-
): Promise<Date | null> {
|
|
793
|
-
const {
|
|
794
|
-
step,
|
|
795
|
-
fakeWaitHoursMultiplier
|
|
796
|
-
} = options;
|
|
797
|
-
|
|
798
|
-
const didFinish = await markStepTerminal(trx, step, 'finished');
|
|
799
|
-
if (!didFinish) {
|
|
800
|
-
return null;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
if (!await isRunAutomationActive(trx, step.automation_run_id)) {
|
|
804
|
-
return null;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
const next = await findNextActionRevision(trx, step.action_id);
|
|
808
|
-
|
|
809
|
-
if (!next) {
|
|
810
|
-
return null;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
const now = new Date();
|
|
814
|
-
const nextReadyAt = getReadyAtForAction(next, now, fakeWaitHoursMultiplier);
|
|
815
|
-
|
|
816
|
-
await insertRunStep(trx, {
|
|
817
|
-
automationRunId: step.automation_run_id,
|
|
818
|
-
automationActionRevisionId: next.automation_action_revision_id,
|
|
819
|
-
now,
|
|
820
|
-
readyAt: nextReadyAt
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
return nextReadyAt;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
async function findNextActionRevision(trx: Knex.Transaction, sourceActionId: string): Promise<NextActionRevisionRow | null> {
|
|
827
|
-
const row = await trx('automation_action_edges as edge')
|
|
828
|
-
.select(
|
|
829
|
-
'action.id as action_id',
|
|
830
|
-
'revision.id as automation_action_revision_id',
|
|
831
|
-
'action.type as type',
|
|
832
|
-
'revision.wait_hours as wait_hours'
|
|
833
|
-
)
|
|
834
|
-
.innerJoin('automation_actions as action', 'action.id', 'edge.target_action_id')
|
|
835
|
-
.innerJoin('automation_action_revisions as revision', 'revision.action_id', 'action.id')
|
|
836
|
-
.where('edge.source_action_id', sourceActionId)
|
|
837
|
-
.whereNull('action.deleted_at')
|
|
838
|
-
.where('revision.created_at', trx('automation_action_revisions')
|
|
839
|
-
.max('created_at')
|
|
840
|
-
.where('action_id', trx.ref('action.id')))
|
|
841
|
-
.orderBy('revision.created_at', 'desc')
|
|
842
|
-
.orderBy('revision.id', 'desc')
|
|
843
|
-
.first();
|
|
844
|
-
|
|
845
|
-
return row ?? null;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
async function markStepTerminal(
|
|
849
|
-
trx: Knex.Transaction,
|
|
850
|
-
step: Pick<AutomationStepToRun, 'id' | 'locked_by'>,
|
|
851
|
-
status: AutomationStepTerminalStatus
|
|
852
|
-
): Promise<boolean> {
|
|
853
|
-
const nowString = toDatabaseDate(new Date());
|
|
854
|
-
return await updateStep(trx, step, {
|
|
855
|
-
status,
|
|
856
|
-
finished_at: nowString,
|
|
857
|
-
updated_at: nowString
|
|
858
|
-
});
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
async function retryStep(
|
|
862
|
-
trx: Knex.Transaction,
|
|
863
|
-
step: Pick<AutomationStepToRun, 'id' | 'locked_by'>,
|
|
864
|
-
retryAt: Readonly<Date>
|
|
865
|
-
): Promise<boolean> {
|
|
866
|
-
if (!await isStepRunAutomationActive(trx, step.id)) {
|
|
867
|
-
await markStepTerminal(trx, step, 'automation disabled');
|
|
868
|
-
return false;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
const nowString = toDatabaseDate(new Date());
|
|
872
|
-
return await updateStep(trx, step, {
|
|
873
|
-
status: 'pending',
|
|
874
|
-
started_at: null,
|
|
875
|
-
finished_at: null,
|
|
876
|
-
ready_at: toDatabaseDate(retryAt),
|
|
877
|
-
updated_at: nowString
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
function getReadyAtForAction(
|
|
882
|
-
action: ReadonlyDeep<Pick<NextActionRevisionRow, 'action_id' | 'type' | 'wait_hours'>>,
|
|
883
|
-
now: Readonly<Date>,
|
|
884
|
-
fakeWaitHoursMultiplier: number | null
|
|
885
|
-
): Date {
|
|
886
|
-
switch (action.type) {
|
|
887
|
-
case 'wait': {
|
|
888
|
-
const waitHours = requireValue({
|
|
889
|
-
...action,
|
|
890
|
-
id: action.action_id
|
|
891
|
-
}, 'wait_hours');
|
|
892
|
-
const waitMs = waitHours * (fakeWaitHoursMultiplier || HOUR_MS);
|
|
893
|
-
return new Date(now.getTime() + waitMs);
|
|
894
|
-
}
|
|
895
|
-
case 'send_email':
|
|
896
|
-
return now;
|
|
897
|
-
default: {
|
|
898
|
-
const _exhaustive: never = action.type;
|
|
899
|
-
throw new errors.IncorrectUsageError({
|
|
900
|
-
message: `Unexpected action type ${_exhaustive}`
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
async function cancelCancelablePendingStepsForAutomation(
|
|
907
|
-
trx: Knex.Transaction,
|
|
908
|
-
automationId: string,
|
|
909
|
-
now: Readonly<Date>
|
|
910
|
-
): Promise<void> {
|
|
911
|
-
const nowString = toDatabaseDate(now);
|
|
912
|
-
const staleLockCutoff = toDatabaseDate(getStaleLockCutoff(now));
|
|
913
|
-
await trx('automation_run_steps')
|
|
914
|
-
.update({
|
|
915
|
-
status: 'automation disabled',
|
|
916
|
-
finished_at: nowString,
|
|
917
|
-
updated_at: nowString,
|
|
918
|
-
locked_by: null,
|
|
919
|
-
locked_at: null
|
|
920
|
-
})
|
|
921
|
-
.where('status', 'pending')
|
|
922
|
-
.whereIn('automation_run_id', trx('automation_runs')
|
|
923
|
-
.select('id')
|
|
924
|
-
.where('automation_id', automationId))
|
|
925
|
-
.where((builder) => {
|
|
926
|
-
builder
|
|
927
|
-
.whereNull('locked_by')
|
|
928
|
-
.orWhere('locked_at', '<', staleLockCutoff);
|
|
929
|
-
});
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
async function isStepRunAutomationActive(trx: Knex.Transaction, stepId: string): Promise<boolean> {
|
|
933
|
-
const query = trx('automation_run_steps as step')
|
|
934
|
-
.select(trx.raw('1'))
|
|
935
|
-
.innerJoin('automation_runs as run', 'run.id', 'step.automation_run_id')
|
|
936
|
-
.innerJoin('automations as automation', 'automation.id', 'run.automation_id')
|
|
937
|
-
.where('step.id', stepId)
|
|
938
|
-
.where('automation.status', 'active');
|
|
939
|
-
return await selectExists(trx, query);
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
async function isRunAutomationActive(trx: Knex.Transaction, automationRunId: string): Promise<boolean> {
|
|
943
|
-
const query = trx('automation_runs as run')
|
|
944
|
-
.select(trx.raw('1'))
|
|
945
|
-
.innerJoin('automations as automation', 'automation.id', 'run.automation_id')
|
|
946
|
-
.where('run.id', automationRunId)
|
|
947
|
-
.where('automation.status', 'active');
|
|
948
|
-
return await selectExists(trx, query);
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
async function selectExists(trx: Knex.Transaction, query: Knex.QueryBuilder): Promise<boolean> {
|
|
952
|
-
const row = await trx
|
|
953
|
-
.select<{exists: boolean | number | string}>(trx.raw('exists ? as `exists`', [query]))
|
|
954
|
-
.first();
|
|
955
|
-
return Boolean(Number(row?.exists));
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
/**
|
|
959
|
-
* Update a step. Returns whether the update succeeded.
|
|
960
|
-
*
|
|
961
|
-
* Should only update locked steps to avoid race conditions. Imagine the following scenario:
|
|
962
|
-
*
|
|
963
|
-
* 1. A step is locked by Worker A.
|
|
964
|
-
* 2. The lock expires.
|
|
965
|
-
* 3. The step is locked by Worker B.
|
|
966
|
-
* 4. Worker A finishes its work.
|
|
967
|
-
*
|
|
968
|
-
* Worker A has lost its lock, so it shouldn't be updating the step any more.
|
|
969
|
-
*/
|
|
970
|
-
async function updateStep(
|
|
971
|
-
trx: Knex.Transaction,
|
|
972
|
-
step: Pick<AutomationStepToRun, 'id' | 'locked_by'>,
|
|
973
|
-
attrs: {
|
|
974
|
-
status: string;
|
|
975
|
-
started_at?: string | null;
|
|
976
|
-
finished_at?: string | null;
|
|
977
|
-
ready_at?: string;
|
|
978
|
-
updated_at: string;
|
|
979
|
-
}
|
|
980
|
-
): Promise<boolean> {
|
|
981
|
-
/* eslint-disable camelcase */
|
|
982
|
-
const {started_at, finished_at, ready_at} = attrs;
|
|
983
|
-
const changes = await trx('automation_run_steps')
|
|
984
|
-
.update({
|
|
985
|
-
status: attrs.status,
|
|
986
|
-
updated_at: attrs.updated_at,
|
|
987
|
-
locked_by: null,
|
|
988
|
-
locked_at: null,
|
|
989
|
-
...(started_at === undefined ? {} : {started_at}),
|
|
990
|
-
...(finished_at === undefined ? {} : {finished_at}),
|
|
991
|
-
...(ready_at === undefined ? {} : {ready_at})
|
|
992
|
-
})
|
|
993
|
-
.where('id', step.id)
|
|
994
|
-
.where('status', 'pending')
|
|
995
|
-
.where('locked_by', step.locked_by);
|
|
996
|
-
/* eslint-enable camelcase */
|
|
997
|
-
return changes >= 1;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
async function loadAutomation(trx: Knex.Transaction, automationId: string): Promise<AutomationRow | null> {
|
|
1001
|
-
const row = await trx('automations')
|
|
1002
|
-
.select('id', 'slug', 'name', 'status', 'created_at', 'updated_at')
|
|
1003
|
-
.where('id', automationId)
|
|
1004
|
-
.first();
|
|
1005
|
-
return row ?? null;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
async function loadAutomationBySlug(trx: Knex.Transaction, slug: string): Promise<AutomationRow | null> {
|
|
1009
|
-
const row = await trx('automations')
|
|
1010
|
-
.select('id', 'slug', 'name', 'status', 'created_at', 'updated_at')
|
|
1011
|
-
.where('slug', slug)
|
|
1012
|
-
.first();
|
|
1013
|
-
return row ?? null;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
async function loadAutomations(trx: Knex.Transaction): Promise<AutomationRow[]> {
|
|
1017
|
-
return await trx('automations')
|
|
1018
|
-
.select('id', 'slug', 'name', 'status', 'created_at', 'updated_at')
|
|
1019
|
-
.orderBy('name');
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
async function updateAutomation(trx: Knex.Transaction, automation: AutomationRow): Promise<AutomationRow> {
|
|
1023
|
-
await trx('automations')
|
|
1024
|
-
.update({
|
|
1025
|
-
status: automation.status,
|
|
1026
|
-
updated_at: automation.updated_at
|
|
1027
|
-
})
|
|
1028
|
-
.where('id', automation.id);
|
|
1029
|
-
|
|
1030
|
-
return requireAutomation(await loadAutomation(trx, automation.id), automation.id);
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
async function replaceAutomationGraph(trx: Knex.Transaction, automationId: string, submittedActions: AutomationAction[], edges: AutomationEdge[]): Promise<void> {
|
|
1034
|
-
const existingActions = await loadAutomationActionRows(trx, automationId);
|
|
1035
|
-
const existingActionById = new Map(existingActions.map(action => [action.id, action]));
|
|
1036
|
-
const actions = (await resolveEmailDesignSettingIds(trx, submittedActions)).map(formatActionOnWrite);
|
|
1037
|
-
const submittedActionIds = new Set(actions.map(action => action.id));
|
|
1038
|
-
const actionIdsWithOwners = await loadActionIdsWithOwners(trx, [...submittedActionIds]);
|
|
1039
|
-
const latestRevisionByActionId = new Map((await loadLatestActionRevisions(trx, [...submittedActionIds])).map(revision => [revision.action_id, revision]));
|
|
1040
|
-
const now = toDatabaseDate(new Date());
|
|
1041
|
-
const actionsToInsert: ActionToInsert[] = [];
|
|
1042
|
-
const revisionsToInsert: ActionRevisionToInsert[] = [];
|
|
1043
|
-
|
|
1044
|
-
for (const action of actions) {
|
|
1045
|
-
const existingAction = existingActionById.get(action.id);
|
|
1046
|
-
if (existingAction) {
|
|
1047
|
-
if (existingAction.type !== action.type) {
|
|
1048
|
-
throw new errors.ValidationError({
|
|
1049
|
-
message: tpl(messages.conflictingAutomationActionType, {
|
|
1050
|
-
actionId: action.id
|
|
1051
|
-
}),
|
|
1052
|
-
property: 'actions.type'
|
|
1053
|
-
});
|
|
1054
|
-
}
|
|
1055
|
-
} else {
|
|
1056
|
-
if (actionIdsWithOwners.has(action.id)) {
|
|
1057
|
-
throw new errors.ValidationError({
|
|
1058
|
-
message: tpl(messages.conflictingAutomationActionId, {
|
|
1059
|
-
actionId: action.id
|
|
1060
|
-
}),
|
|
1061
|
-
property: 'actions.id'
|
|
1062
|
-
});
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
actionsToInsert.push({
|
|
1066
|
-
id: action.id,
|
|
1067
|
-
created_at: now,
|
|
1068
|
-
updated_at: now,
|
|
1069
|
-
automation_id: automationId,
|
|
1070
|
-
type: action.type
|
|
1071
|
-
});
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
const latestRevision = latestRevisionByActionId.get(action.id);
|
|
1075
|
-
if (shouldInsertActionRevision(action, latestRevision)) {
|
|
1076
|
-
revisionsToInsert.push({
|
|
1077
|
-
actionId: action.id,
|
|
1078
|
-
action,
|
|
1079
|
-
createdAt: getNextRevisionCreatedAt(latestRevision?.created_at ?? null, now)
|
|
1080
|
-
});
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
await insertActions(trx, actionsToInsert);
|
|
1085
|
-
await insertActionRevisions(trx, revisionsToInsert);
|
|
1086
|
-
|
|
1087
|
-
const actionIdsToSoftDelete = existingActions
|
|
1088
|
-
.filter(existingAction => !submittedActionIds.has(existingAction.id))
|
|
1089
|
-
.map(existingAction => existingAction.id);
|
|
1090
|
-
await softDeleteActions(trx, actionIdsToSoftDelete, now);
|
|
1091
|
-
|
|
1092
|
-
await deleteAutomationEdges(trx, automationId);
|
|
1093
|
-
|
|
1094
|
-
await insertActionEdges(trx, edges);
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
function formatActionOnWrite(action: AutomationAction): AutomationAction {
|
|
1098
|
-
if (action.type !== 'send_email') {
|
|
1099
|
-
return action;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
return {
|
|
1103
|
-
...action,
|
|
1104
|
-
data: {
|
|
1105
|
-
...action.data,
|
|
1106
|
-
email_lexical: urlUtils.lexicalToTransformReady(action.data.email_lexical, {
|
|
1107
|
-
nodes: lexicalLib.nodes,
|
|
1108
|
-
transformMap: lexicalLib.urlTransformMap
|
|
1109
|
-
})
|
|
1110
|
-
}
|
|
1111
|
-
};
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
async function resolveEmailDesignSettingIds(trx: Knex.Transaction, actions: ReadonlyArray<AutomationAction>): Promise<AutomationAction[]> {
|
|
1115
|
-
if (!actions.some(action => action.type === 'send_email' && action.data.email_design_setting_id === DEFAULT_EMAIL_DESIGN_SETTING_REFERENCE)) {
|
|
1116
|
-
return [...actions];
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
const defaultEmailDesignSettingId = await loadDefaultEmailDesignSettingId(trx);
|
|
1120
|
-
|
|
1121
|
-
return actions.map((action) => {
|
|
1122
|
-
if (action.type !== 'send_email' || action.data.email_design_setting_id !== DEFAULT_EMAIL_DESIGN_SETTING_REFERENCE) {
|
|
1123
|
-
return action;
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
return {
|
|
1127
|
-
...action,
|
|
1128
|
-
data: {
|
|
1129
|
-
...action.data,
|
|
1130
|
-
email_design_setting_id: defaultEmailDesignSettingId
|
|
1131
|
-
}
|
|
1132
|
-
};
|
|
1133
|
-
});
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
async function loadDefaultEmailDesignSettingId(trx: Knex.Transaction): Promise<string> {
|
|
1137
|
-
const row = await trx('email_design_settings')
|
|
1138
|
-
.select('id')
|
|
1139
|
-
.where('slug', DEFAULT_EMAIL_DESIGN_SETTING_SLUG)
|
|
1140
|
-
.first();
|
|
1141
|
-
|
|
1142
|
-
if (!row?.id) {
|
|
1143
|
-
throw new errors.InternalServerError({
|
|
1144
|
-
message: tpl(messages.defaultEmailDesignSettingNotFound)
|
|
1145
|
-
});
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
return row.id;
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
async function loadAutomationActionRows(trx: Knex.Transaction, automationId: string): Promise<Array<Pick<ActionRow, 'id' | 'type'>>> {
|
|
1152
|
-
return await trx('automation_actions')
|
|
1153
|
-
.select('id', 'type')
|
|
1154
|
-
.where('automation_id', automationId)
|
|
1155
|
-
.whereNull('deleted_at');
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
async function loadActionIdsWithOwners(trx: Knex.Transaction, actionIds: ReadonlyArray<string>): Promise<Set<string>> {
|
|
1159
|
-
if (actionIds.length === 0) {
|
|
1160
|
-
return new Set();
|
|
1161
|
-
}
|
|
1162
|
-
const rows = await trx('automation_actions')
|
|
1163
|
-
.select('id')
|
|
1164
|
-
.whereIn('id', actionIds);
|
|
1165
|
-
return new Set(rows.map(row => row.id));
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
async function insertActions(trx: Knex.Transaction, actions: ReadonlyArray<ActionToInsert>): Promise<void> {
|
|
1169
|
-
if (actions.length === 0) {
|
|
1170
|
-
return;
|
|
1171
|
-
}
|
|
1172
|
-
await trx('automation_actions').insert(actions);
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
function shouldInsertActionRevision(action: AutomationAction, latestRevision: ActionRevisionRow | undefined): boolean {
|
|
1176
|
-
if (!latestRevision) {
|
|
1177
|
-
return true;
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
return !dequal(buildRevisionActionData(action, latestRevision), action.data);
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
function buildRevisionActionData(action: AutomationAction, revision: ActionRevisionRow): ExclusifyUnion<WaitRevisionData | SendEmailRevisionData> {
|
|
1184
|
-
switch (action.type) {
|
|
1185
|
-
case 'wait':
|
|
1186
|
-
return {
|
|
1187
|
-
wait_hours: revision.wait_hours
|
|
1188
|
-
};
|
|
1189
|
-
case 'send_email':
|
|
1190
|
-
return {
|
|
1191
|
-
email_subject: revision.email_subject,
|
|
1192
|
-
email_lexical: revision.email_lexical,
|
|
1193
|
-
email_design_setting_id: revision.email_design_setting_id
|
|
1194
|
-
};
|
|
1195
|
-
default: {
|
|
1196
|
-
const _exhaustive: never = action;
|
|
1197
|
-
throw new errors.InternalServerError({
|
|
1198
|
-
message: `Unhandled action type: ${_exhaustive}`
|
|
1199
|
-
});
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
async function loadLatestActionRevisions(
|
|
1205
|
-
trx: Knex.Transaction,
|
|
1206
|
-
actionIds: ReadonlyArray<string>
|
|
1207
|
-
): Promise<ActionRevisionRow[]> {
|
|
1208
|
-
if (actionIds.length === 0) {
|
|
1209
|
-
return [];
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
const latestRevisionDates = trx('automation_action_revisions')
|
|
1213
|
-
.select('action_id')
|
|
1214
|
-
.max({created_at: 'created_at'})
|
|
1215
|
-
.whereIn('action_id', actionIds)
|
|
1216
|
-
.groupBy('action_id')
|
|
1217
|
-
.as('latest_revision_dates');
|
|
1218
|
-
|
|
1219
|
-
return await trx('automation_action_revisions')
|
|
1220
|
-
.select(
|
|
1221
|
-
'automation_action_revisions.action_id',
|
|
1222
|
-
'automation_action_revisions.created_at',
|
|
1223
|
-
'automation_action_revisions.wait_hours',
|
|
1224
|
-
'automation_action_revisions.email_subject',
|
|
1225
|
-
'automation_action_revisions.email_lexical',
|
|
1226
|
-
'automation_action_revisions.email_design_setting_id'
|
|
1227
|
-
)
|
|
1228
|
-
.innerJoin(latestRevisionDates, function () {
|
|
1229
|
-
this.on('automation_action_revisions.action_id', 'latest_revision_dates.action_id')
|
|
1230
|
-
.andOn('automation_action_revisions.created_at', 'latest_revision_dates.created_at');
|
|
1231
|
-
});
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
async function softDeleteActions(
|
|
1235
|
-
trx: Knex.Transaction,
|
|
1236
|
-
actionIds: ReadonlyArray<string>,
|
|
1237
|
-
deletedAt: string
|
|
1238
|
-
): Promise<void> {
|
|
1239
|
-
if (actionIds.length === 0) {
|
|
1240
|
-
return;
|
|
1241
|
-
}
|
|
1242
|
-
await trx('automation_actions')
|
|
1243
|
-
.update({
|
|
1244
|
-
deleted_at: deletedAt,
|
|
1245
|
-
updated_at: deletedAt
|
|
1246
|
-
})
|
|
1247
|
-
.whereIn('id', actionIds);
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
async function insertActionRevisions(
|
|
1251
|
-
trx: Knex.Transaction,
|
|
1252
|
-
revisions: ReadonlyArray<ActionRevisionToInsert>
|
|
1253
|
-
): Promise<void> {
|
|
1254
|
-
if (revisions.length === 0) {
|
|
1255
|
-
return;
|
|
1256
|
-
}
|
|
1257
|
-
await trx('automation_action_revisions').insert(
|
|
1258
|
-
revisions.map(({actionId, action, createdAt}) => buildActionRevision(actionId, action, createdAt))
|
|
1259
|
-
);
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
function getNextRevisionCreatedAt(latestCreatedAt: DatabaseDate | null, requestedCreatedAt: string) {
|
|
1263
|
-
if (!latestCreatedAt) {
|
|
1264
|
-
return toDatabaseDate(requestedCreatedAt);
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
const requestedTime = fromDatabaseDate(requestedCreatedAt).getTime();
|
|
1268
|
-
const latestTime = fromDatabaseDate(latestCreatedAt).getTime();
|
|
1269
|
-
|
|
1270
|
-
if (requestedTime > latestTime) {
|
|
1271
|
-
return toDatabaseDate(requestedCreatedAt);
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
return toDatabaseDate(new Date(latestTime + 1000));
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
function buildActionRevision(actionId: string, action: AutomationAction, createdAt: string) {
|
|
1278
|
-
switch (action.type) {
|
|
1279
|
-
case 'wait':
|
|
1280
|
-
return {
|
|
1281
|
-
id: ObjectId().toString(),
|
|
1282
|
-
created_at: createdAt,
|
|
1283
|
-
action_id: actionId,
|
|
1284
|
-
wait_hours: action.data.wait_hours,
|
|
1285
|
-
email_subject: null,
|
|
1286
|
-
email_lexical: null,
|
|
1287
|
-
email_design_setting_id: null
|
|
1288
|
-
};
|
|
1289
|
-
case 'send_email':
|
|
1290
|
-
return {
|
|
1291
|
-
id: ObjectId().toString(),
|
|
1292
|
-
created_at: createdAt,
|
|
1293
|
-
action_id: actionId,
|
|
1294
|
-
wait_hours: null,
|
|
1295
|
-
email_subject: action.data.email_subject,
|
|
1296
|
-
email_lexical: action.data.email_lexical,
|
|
1297
|
-
email_design_setting_id: action.data.email_design_setting_id
|
|
1298
|
-
};
|
|
1299
|
-
default: {
|
|
1300
|
-
const _exhaustive: never = action;
|
|
1301
|
-
throw new errors.InternalServerError({
|
|
1302
|
-
message: `Unexpected action type ${_exhaustive}`
|
|
1303
|
-
});
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
async function deleteAutomationEdges(trx: Knex.Transaction, automationId: string): Promise<void> {
|
|
1309
|
-
await trx('automation_action_edges')
|
|
1310
|
-
.delete()
|
|
1311
|
-
.whereIn('source_action_id', trx('automation_actions')
|
|
1312
|
-
.select('id')
|
|
1313
|
-
.where('automation_id', automationId));
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
async function insertActionEdges(trx: Knex.Transaction, edges: ReadonlyArray<AutomationEdge>): Promise<void> {
|
|
1317
|
-
if (edges.length === 0) {
|
|
1318
|
-
return;
|
|
1319
|
-
}
|
|
1320
|
-
await trx('automation_action_edges').insert(edges.map(edge => ({
|
|
1321
|
-
source_action_id: edge.source_action_id,
|
|
1322
|
-
target_action_id: edge.target_action_id
|
|
1323
|
-
})));
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
function requireAutomation(automation: AutomationRow | null, id: string): AutomationRow {
|
|
1327
|
-
if (!automation) {
|
|
1328
|
-
throw new errors.InternalServerError({
|
|
1329
|
-
message: `Updated automation "${id}" could not be loaded.`
|
|
1330
|
-
});
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
return automation;
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
async function buildAutomation(trx: Knex.Transaction, automation: AutomationRow): Promise<Automation> {
|
|
1337
|
-
const actionRows = await loadActionRows(trx, automation.id);
|
|
1338
|
-
const actionStats = await loadActionStats(trx, actionRows.map(row => row.id));
|
|
1339
|
-
const edgeRows = await loadEdgeRows(trx, automation.id);
|
|
1340
|
-
return {
|
|
1341
|
-
...buildAutomationSummary(automation),
|
|
1342
|
-
actions: actionRows.map(row => buildActionPayload(row, actionStats.get(row.id) ?? null)),
|
|
1343
|
-
edges: edgeRows.map(row => buildEdgePayload(row))
|
|
1344
|
-
};
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
function buildAutomationSummary(automation: AutomationRow): AutomationSummary {
|
|
1348
|
-
return {
|
|
1349
|
-
id: automation.id,
|
|
1350
|
-
slug: automation.slug,
|
|
1351
|
-
name: automation.name,
|
|
1352
|
-
status: automation.status,
|
|
1353
|
-
created_at: serializeDate(automation.created_at),
|
|
1354
|
-
updated_at: serializeDate(automation.updated_at)
|
|
1355
|
-
};
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
function serializeDate(date: DatabaseDate) {
|
|
1359
|
-
const normalizedDate = fromDatabaseDate(date);
|
|
1360
|
-
normalizedDate.setMilliseconds(0);
|
|
1361
|
-
return normalizedDate.toISOString();
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
async function loadActionRows(trx: Knex.Transaction, automationId: string): Promise<ActionRow[]> {
|
|
1365
|
-
return await trx('automation_actions as a')
|
|
1366
|
-
.select(
|
|
1367
|
-
'a.id as id',
|
|
1368
|
-
'a.type as type',
|
|
1369
|
-
'r.wait_hours as wait_hours',
|
|
1370
|
-
'r.email_subject as email_subject',
|
|
1371
|
-
'r.email_lexical as email_lexical',
|
|
1372
|
-
'r.email_design_setting_id as email_design_setting_id'
|
|
1373
|
-
)
|
|
1374
|
-
.innerJoin('automation_action_revisions as r', 'r.action_id', 'a.id')
|
|
1375
|
-
.where('a.automation_id', automationId)
|
|
1376
|
-
.whereNull('a.deleted_at')
|
|
1377
|
-
.where('r.created_at', trx('automation_action_revisions')
|
|
1378
|
-
.max('created_at')
|
|
1379
|
-
.where('action_id', trx.ref('a.id')))
|
|
1380
|
-
.orderBy([
|
|
1381
|
-
'a.created_at',
|
|
1382
|
-
'a.id'
|
|
1383
|
-
]);
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
async function loadActionStats(
|
|
1387
|
-
trx: Knex.Transaction,
|
|
1388
|
-
actionIds: ReadonlyArray<string>
|
|
1389
|
-
): Promise<Map<string, AutomationEmailStats>> {
|
|
1390
|
-
if (actionIds.length === 0) {
|
|
1391
|
-
return new Map();
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
const rows: ActionStatsRow[] = await trx('automation_action_revisions')
|
|
1395
|
-
.select('action_id')
|
|
1396
|
-
.sum({
|
|
1397
|
-
email_clicked_count: 'email_clicked_count',
|
|
1398
|
-
email_sent_count: 'email_sent_count',
|
|
1399
|
-
email_opened_count: 'email_opened_count'
|
|
1400
|
-
})
|
|
1401
|
-
.whereIn('action_id', actionIds)
|
|
1402
|
-
.groupBy('action_id');
|
|
1403
|
-
|
|
1404
|
-
return new Map(rows.map(row => [row.action_id, buildEmailStats(row)]));
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
async function loadEdgeRows(trx: Knex.Transaction, automationId: string): Promise<EdgeRow[]> {
|
|
1408
|
-
return await trx('automation_action_edges as e')
|
|
1409
|
-
.select('e.source_action_id', 'e.target_action_id')
|
|
1410
|
-
.innerJoin('automation_actions as source_action', (join) => {
|
|
1411
|
-
join
|
|
1412
|
-
.on('source_action.id', 'e.source_action_id')
|
|
1413
|
-
.onNull('source_action.deleted_at');
|
|
1414
|
-
})
|
|
1415
|
-
.innerJoin('automation_actions as target_action', (join) => {
|
|
1416
|
-
join
|
|
1417
|
-
.on('target_action.id', 'e.target_action_id')
|
|
1418
|
-
.onNull('target_action.deleted_at')
|
|
1419
|
-
.on('target_action.automation_id', 'source_action.automation_id');
|
|
1420
|
-
})
|
|
1421
|
-
.where('source_action.automation_id', automationId)
|
|
1422
|
-
.orderBy([
|
|
1423
|
-
'e.source_action_id',
|
|
1424
|
-
'e.target_action_id'
|
|
1425
|
-
]);
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
function buildActionPayload(row: ActionRow, stats: AutomationEmailStats | null): AutomationAction {
|
|
1429
|
-
switch (row.type) {
|
|
1430
|
-
case 'wait':
|
|
1431
|
-
return {
|
|
1432
|
-
id: row.id,
|
|
1433
|
-
type: 'wait',
|
|
1434
|
-
data: {
|
|
1435
|
-
wait_hours: requireValue(row, 'wait_hours')
|
|
1436
|
-
}
|
|
1437
|
-
};
|
|
1438
|
-
case 'send_email':
|
|
1439
|
-
return {
|
|
1440
|
-
id: row.id,
|
|
1441
|
-
type: 'send_email',
|
|
1442
|
-
data: {
|
|
1443
|
-
email_subject: requireValue(row, 'email_subject'),
|
|
1444
|
-
email_lexical: urlUtils.transformReadyToAbsolute(requireValue(row, 'email_lexical')),
|
|
1445
|
-
email_design_setting_id: requireValue(row, 'email_design_setting_id')
|
|
1446
|
-
},
|
|
1447
|
-
stats: stats ?? EMPTY_EMAIL_STATS
|
|
1448
|
-
};
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
const EMPTY_EMAIL_STATS: AutomationEmailStats = {
|
|
1453
|
-
email_clicked_count: 0,
|
|
1454
|
-
email_sent_count: 0,
|
|
1455
|
-
email_opened_count: 0,
|
|
1456
|
-
opened_rate: null,
|
|
1457
|
-
clicked_rate: null
|
|
1458
|
-
};
|
|
1459
|
-
|
|
1460
|
-
function buildEmailStats(row: ActionStatsRow): AutomationEmailStats {
|
|
1461
|
-
const emailClickedCount = row.email_clicked_count ?? 0;
|
|
1462
|
-
const emailSentCount = row.email_sent_count ?? 0;
|
|
1463
|
-
const emailOpenedCount = row.email_opened_count ?? 0;
|
|
1464
|
-
return {
|
|
1465
|
-
email_clicked_count: emailClickedCount,
|
|
1466
|
-
email_sent_count: emailSentCount,
|
|
1467
|
-
email_opened_count: emailOpenedCount,
|
|
1468
|
-
opened_rate: emailSentCount
|
|
1469
|
-
? Math.round(emailOpenedCount / emailSentCount * 100)
|
|
1470
|
-
: null,
|
|
1471
|
-
clicked_rate: emailSentCount
|
|
1472
|
-
? Math.round(emailClickedCount / emailSentCount * 100)
|
|
1473
|
-
: null
|
|
1474
|
-
};
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
function requireValue<
|
|
1478
|
-
RowT extends {id: string, type: string},
|
|
1479
|
-
FieldT extends keyof RowT
|
|
1480
|
-
>(
|
|
1481
|
-
row: RowT,
|
|
1482
|
-
field: FieldT
|
|
1483
|
-
): NonNullable<RowT[FieldT]> {
|
|
1484
|
-
const value = row[field];
|
|
1485
|
-
if ((value === null) || (value === undefined)) {
|
|
1486
|
-
throw new errors.InternalServerError({
|
|
1487
|
-
message: tpl(messages.invalidAutomationActionRevision, {
|
|
1488
|
-
actionId: row.id,
|
|
1489
|
-
actionType: row.type,
|
|
1490
|
-
field
|
|
1491
|
-
})
|
|
1492
|
-
});
|
|
1493
|
-
}
|
|
1494
|
-
return value;
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
function buildEdgePayload(edge: EdgeRow): AutomationEdge {
|
|
1498
|
-
return {
|
|
1499
|
-
source_action_id: edge.source_action_id,
|
|
1500
|
-
target_action_id: edge.target_action_id
|
|
1501
|
-
};
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
function buildPagination(total: number) {
|
|
1505
|
-
return {
|
|
1506
|
-
page: 1,
|
|
1507
|
-
pages: 1,
|
|
1508
|
-
limit: 'all' as const,
|
|
1509
|
-
total,
|
|
1510
|
-
prev: null,
|
|
1511
|
-
next: null
|
|
1512
|
-
};
|
|
1513
|
-
}
|