ghost 6.54.1 → 6.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (356) hide show
  1. package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
  2. package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
  3. package/components/tryghost-kg-default-nodes-2.1.4.tgz +0 -0
  4. package/components/tryghost-kg-default-transforms-1.3.3.tgz +0 -0
  5. package/components/tryghost-kg-html-to-lexical-1.3.3.tgz +0 -0
  6. package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
  7. package/components/tryghost-nql-string-0.0.0.tgz +0 -0
  8. package/core/boot.js +3 -6
  9. package/core/built/admin/assets/{Combination-CJG7Flp-.js → Combination-BCm9FL4o.js} +1 -1
  10. package/core/built/admin/assets/{PolarAngleAxis-BGjf7sBu.js → PolarAngleAxis-C4qtHy1B.js} +1 -1
  11. package/core/built/admin/assets/{RadialBarChart-zeFf9mBz.js → RadialBarChart-Bh09Stsj.js} +1 -1
  12. package/core/built/admin/assets/{account-migration-BklF2I_I.js → account-migration-D3O_yVck.js} +1 -1
  13. package/core/built/admin/assets/activitypub/{account-migration-CNovr6W-.js → account-migration-D8FbP-Z-.js} +7 -7
  14. package/core/built/admin/assets/activitypub/activitypub.js +6 -6
  15. package/core/built/admin/assets/activitypub/{avatar-B3DEDB1l.js → avatar-11EA1VSs.js} +2 -2
  16. package/core/built/admin/assets/activitypub/{avatar-flipboard-Cqrg0XMx.js → avatar-flipboard-jfbLG-xe.js} +2 -2
  17. package/core/built/admin/assets/activitypub/{bluesky-sharing-Ceslb1_M.js → bluesky-sharing-f1jTQe2P.js} +16 -16
  18. package/core/built/admin/assets/activitypub/{content-formatters-BpQARh28.js → content-formatters-nmTOPY9M.js} +3 -3
  19. package/core/built/admin/assets/activitypub/{copy-D9UWIAan.js → copy-BCwo9ep3.js} +2 -2
  20. package/core/built/admin/assets/activitypub/{dist-DHAjBcV3.js → dist-BEfX-0aW.js} +12 -12
  21. package/core/built/admin/assets/activitypub/{domain-C1qQvJ8C.js → domain-AaDlZlrA.js} +9 -9
  22. package/core/built/admin/assets/activitypub/{edit-profile-BcH-jEfM.js → edit-profile-DPi8Tl5M.js} +945 -945
  23. package/core/built/admin/assets/activitypub/{explore-AxwBh9dI.js → explore-CxTzjILQ.js} +8 -8
  24. package/core/built/admin/assets/activitypub/{feed-BaOlbh7a.js → feed-DPv5x2MX.js} +33 -33
  25. package/core/built/admin/assets/activitypub/{field-B7mYYQpN.js → field-DbgA8lat.js} +3 -3
  26. package/core/built/admin/assets/activitypub/{hash-BC9Q0N4c.js → hash-BWuZoYkA.js} +2 -2
  27. package/core/built/admin/assets/activitypub/{inbox-DQ4ZnC1m.js → inbox-35VDQ2K2.js} +2 -2
  28. package/core/built/admin/assets/activitypub/{inbox-vXez-5uJ.js → inbox-DSrF_i1O.js} +85 -85
  29. package/core/built/admin/assets/activitypub/{label-Chv5T8bw.js → label-CPjFBbo7.js} +3 -3
  30. package/core/built/admin/assets/activitypub/layout-CTgrezN7.js +1 -0
  31. package/core/built/admin/assets/activitypub/{moderation-DtSM2i1I.js → moderation-BkDC1XVZ.js} +11 -11
  32. package/core/built/admin/assets/activitypub/{note-Dckp4S-2.js → note-xEyH4xT-.js} +7 -7
  33. package/core/built/admin/assets/activitypub/{notifications-CG5T22vg.js → notifications-DClaL3ui.js} +9 -9
  34. package/core/built/admin/assets/activitypub/{preferences-D_799jdl.js → preferences-DpnOa-4Z.js} +20 -20
  35. package/core/built/admin/assets/activitypub/{profile-D2JfXBQl.js → profile-Dvi9mz-6.js} +237 -237
  36. package/core/built/admin/assets/activitypub/{reply-CpdMeAO5.js → reply-Cs9ySd6Z.js} +2 -2
  37. package/core/built/admin/assets/activitypub/{routes-CCHe2dQp.js → routes-DFBeTnfn.js} +1139 -1150
  38. package/core/built/admin/assets/activitypub/{separator-BI1FLsYM.js → separator-C5u_VjzY.js} +3 -3
  39. package/core/built/admin/assets/activitypub/{settings-kx-9Enkz.js → settings-CV2fFu6D.js} +13 -13
  40. package/core/built/admin/assets/activitypub/{site-x4_IjVJ0.js → site-_TgDd_EM.js} +81 -81
  41. package/core/built/admin/assets/activitypub/{step-1-DIDg4aHb.js → step-1-BfnNL2oF.js} +5 -5
  42. package/core/built/admin/assets/activitypub/{step-2-BfGe8BYt.js → step-2-DLndZkO-.js} +6 -6
  43. package/core/built/admin/assets/activitypub/{step-3-mE9spAqw.js → step-3-A-PZTtRE.js} +10 -10
  44. package/core/built/admin/assets/activitypub/{tabs-CyPRQQ1N.js → tabs-BUbz7tea.js} +321 -319
  45. package/core/built/admin/assets/activitypub/{topic-filter-DLHBZT93.js → topic-filter-BdX_xYYT.js} +3 -3
  46. package/core/built/admin/assets/activitypub/{use-activity-pub-queries-VzbN99u_.js → use-activity-pub-queries-EwuRemHh.js} +2 -2
  47. package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-CAwXTmqH.js → use-navigate-with-base-path-cy4ACBc4.js} +92 -90
  48. package/core/built/admin/assets/activitypub/{use-reply-chain-data-BAB6fNGt.js → use-reply-chain-data-CyFcGfan.js} +5 -5
  49. package/core/built/admin/assets/activitypub/{x-C8tAnXly.js → x-7zk8mV2p.js} +2 -2
  50. package/core/built/admin/assets/audience-CkBb2LlO.js +1 -0
  51. package/core/built/admin/assets/{automated-emails-of1MvVsv.js → automated-emails-6t2bIRWf.js} +2 -2
  52. package/core/built/admin/assets/{automations-B7-y3dXt.js → automations-DhKeS_U5.js} +1 -1
  53. package/core/built/admin/assets/{automations-DRFjW5fw.js → automations-cMllU545.js} +1 -1
  54. package/core/built/admin/assets/{bluesky-sharing-BbbjWdzs.js → bluesky-sharing-DPGpyD-G.js} +1 -1
  55. package/core/built/admin/assets/{box-BWbhXh21.js → box-By_EgGtm.js} +1 -1
  56. package/core/built/admin/assets/{breadcrumb-B-M9Ip_2.js → breadcrumb-DIfcF5ow.js} +1 -1
  57. package/core/built/admin/assets/build-CsiH26uS.js +1 -0
  58. package/core/built/admin/assets/{card-DGoDJzrn.js → card-aUHX0hyR.js} +1 -1
  59. package/core/built/admin/assets/{chart-BuRT2Y6p.js → chart-BnXyfH11.js} +1 -1
  60. package/core/built/admin/assets/{chart-helpers-xiSsW8r0.js → chart-helpers-DV0FkdcD.js} +1 -1
  61. package/core/built/admin/assets/{checkbox-EhJKHFdZ.js → checkbox-BIl077a0.js} +1 -1
  62. package/core/built/admin/assets/{chunk.361.3cdb584b973959e52ccc.js → chunk.332.05991a101d67f732dec3.js} +16 -12
  63. package/core/built/admin/assets/chunk.524.87b52f4fada529df2c4c.js +25 -0
  64. package/core/built/admin/assets/chunk.582.340bcca873e806cbd4bb.js +27 -0
  65. package/core/built/admin/assets/{code-editor-view-CZtJFGhR.js → code-editor-view-C0aWNwdl.js} +1 -1
  66. package/core/built/admin/assets/comments-qwhIjH3c.js +1 -0
  67. package/core/built/admin/assets/{container-BYz2Ed39.js → container-LiZf3XdY.js} +1 -1
  68. package/core/built/admin/assets/{data-list-16rkJYYi.js → data-list-CupF7bjM.js} +1 -1
  69. package/core/built/admin/assets/{date-range-select-CTlyo8zW.js → date-range-select-dXc-GsVe.js} +1 -1
  70. package/core/built/admin/assets/{disabled-sources-indicator-qs2GfLvl.js → disabled-sources-indicator-DYAXW-6V.js} +1 -1
  71. package/core/built/admin/assets/dist-BUKmD51u.js +1 -0
  72. package/core/built/admin/assets/{dist-2Tnz2Yfq.js → dist-CNlMj7ML.js} +1 -1
  73. package/core/built/admin/assets/{dist-DGzPJ_bO.js → dist-Cb5RSHSg.js} +1 -1
  74. package/core/built/admin/assets/{dist-P_ohUlTi.js → dist-CqO2lWS0.js} +1 -1
  75. package/core/built/admin/assets/{domain-CPNXKVOV.js → domain-B5ogXUBa.js} +1 -1
  76. package/core/built/admin/assets/{dropzone-BmCXnw6J.js → dropzone-BnFG7dDP.js} +1 -1
  77. package/core/built/admin/assets/{edit-profile-SGrJve5M.js → edit-profile-B07jCZy9.js} +1 -1
  78. package/core/built/admin/assets/editor-DDYSOXGN.js +8 -0
  79. package/core/built/admin/assets/{empty-indicator-oX1u1Pkn.js → empty-indicator-WB1WgT9h.js} +1 -1
  80. package/core/built/admin/assets/{explore-CUYLv9G9.js → explore-Ca1y8HW_.js} +1 -1
  81. package/core/built/admin/assets/{feed-CPdJMttL.js → feed-x4iFMBP4.js} +1 -1
  82. package/core/built/admin/assets/field-ChCAabZg.js +1 -0
  83. package/core/built/admin/assets/filter-sources-LdtH1e-T.js +1 -0
  84. package/core/built/admin/assets/{filters-bXkkWro8.js → filters-CLehPo_D.js} +1 -1
  85. package/core/built/admin/assets/filters-CjVKRP8U.js +1 -0
  86. package/core/built/admin/assets/{ghost-05f9a7182ed56903dba05878a5b8422b.js → ghost-7f01131115186d1af84ead76e79646dd.js} +275 -255
  87. package/core/built/admin/assets/{ghost-3063eae29e6c9cc6d6e7131928a27da0.css → ghost-d38c30b866e59f2e06a357b5eb844fe5.css} +1 -1
  88. package/core/built/admin/assets/{ghost-dark-c8d0ab7f515b2010c173e23e9b32ea8a.css → ghost-dark-1bffd5099ba7a749303133efacf8480b.css} +1 -1
  89. package/core/built/admin/assets/gift-link-modal-Buqh2vXh.js +1 -0
  90. package/core/built/admin/assets/{gift-links-nBxJjZiQ.js → gift-links-CsUhefWG.js} +1 -1
  91. package/core/built/admin/assets/growth-DajOBdN0.js +1 -0
  92. package/core/built/admin/assets/growth-Dy2UOSay.js +3 -0
  93. package/core/built/admin/assets/{hooks-BApwwBE0.js → hooks-CixO01lL.js} +1 -1
  94. package/core/built/admin/assets/{inbox-Xx1CdnfO.js → inbox-BUOLDoji.js} +13 -13
  95. package/core/built/admin/assets/index-08n05-rc.js +67 -0
  96. package/core/built/admin/assets/index-DgUzgFvS.css +1 -0
  97. package/core/built/admin/assets/{inline-EMAm3okk.js → inline-Cw-gigDW.js} +1 -1
  98. package/core/built/admin/assets/input-group-uSK3XT7b.js +1 -0
  99. package/core/built/admin/assets/{input-surface-BpAjZXY5.js → input-surface-BdOe4KzO.js} +1 -1
  100. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +9132 -9087
  101. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +121 -121
  102. package/core/built/admin/assets/koenig-lexical/style.css +1 -1
  103. package/core/built/admin/assets/{koenig-lexical-B61IALf5.js → koenig-lexical-BFc4yFed.js} +82 -82
  104. package/core/built/admin/assets/{kpi-card-DCdlyaSS.js → kpi-card-BN-E6WYX.js} +1 -1
  105. package/core/built/admin/assets/{kpi-card-BlZaG1Lo.js → kpi-card-CCt-qeWD.js} +1 -1
  106. package/core/built/admin/assets/{kpi-helpers-DpAuS1Nb.js → kpi-helpers-DlxTbTs4.js} +1 -1
  107. package/core/built/admin/assets/{kpi-tabs-Dme44bQS.js → kpi-tabs-BlcUttpn.js} +1 -1
  108. package/core/built/admin/assets/{label-dSs5aepE.js → label-AVrxwmsz.js} +1 -1
  109. package/core/built/admin/assets/label-picker-DVnpu_IF.js +1 -0
  110. package/core/built/admin/assets/labels-DAIqKed7.js +1 -0
  111. package/core/built/admin/assets/layout-B0YLh7R9.js +1 -0
  112. package/core/built/admin/assets/link-helpers-D9LsDOkG.js +1 -0
  113. package/core/built/admin/assets/{list-page-B6ANtzkd.js → list-page-CdsnQR9N.js} +1 -1
  114. package/core/built/admin/assets/member-custom-fields-C8NpZYza.js +1 -0
  115. package/core/built/admin/assets/member-detail-CIVAdKXf.js +1 -0
  116. package/core/built/admin/assets/members-oLPiSkXD.js +10 -0
  117. package/core/built/admin/assets/modals-CB7v-k3T.js +31 -0
  118. package/core/built/admin/assets/moderation-CQ3urTFE.js +1 -0
  119. package/core/built/admin/assets/newsletter-rbeDX9Wk.js +1 -0
  120. package/core/built/admin/assets/{newsletters-D1jbtV-h.js → newsletters-BXJbJgA-.js} +1 -1
  121. package/core/built/admin/assets/newsletters-vt3Ni1Ov.js +1 -0
  122. package/core/built/admin/assets/note-Dk877BTv.js +1 -0
  123. package/core/built/admin/assets/notifications-CPlLjPxO.js +1 -0
  124. package/core/built/admin/assets/{offers-CrY6nSRR.js → offers-DEniVRD5.js} +1 -1
  125. package/core/built/admin/assets/onboarding-route-DdStOtjy.js +1 -0
  126. package/core/built/admin/assets/overview--4V-kx9c.js +1 -0
  127. package/core/built/admin/assets/overview-DSLv30oz.js +1 -0
  128. package/core/built/admin/assets/{page-header-op0eN36I.js → page-header-pp4dWmOS.js} +1 -1
  129. package/core/built/admin/assets/{pages-BbNWFcGj.js → pages-B4goRPUn.js} +1 -1
  130. package/core/built/admin/assets/{post-analytics-WwRDEnrV.js → post-analytics-BMJrI81R.js} +1 -1
  131. package/core/built/admin/assets/post-analytics-header-pU2m9ZuV.js +1 -0
  132. package/core/built/admin/assets/post-analytics-provider-Btml5vJI.js +1 -0
  133. package/core/built/admin/assets/{post-share-modal-Dx-MEjhI.js → post-share-modal-AcUZkbfL.js} +1 -1
  134. package/core/built/admin/assets/{posts-EWpZIjkr.js → posts-DhXE7b6r.js} +1 -1
  135. package/core/built/admin/assets/preferences-Ru4CRPHC.js +5 -0
  136. package/core/built/admin/assets/profile-BNb1oj7D.js +1 -0
  137. package/core/built/admin/assets/{referrers-BCfFYSHc.js → referrers-pmJT4k3-.js} +1 -1
  138. package/core/built/admin/assets/{router-provider-C1R4AZ2o.js → router-provider-BmEZf6c6.js} +1 -1
  139. package/core/built/admin/assets/select-B0HteHrp.js +1 -0
  140. package/core/built/admin/assets/settings-B1C9Q3wo.js +4 -0
  141. package/core/built/admin/assets/{settings-H6HxFgfI.js → settings-BGgk7oCI.js} +1 -1
  142. package/core/built/admin/assets/{shade-app-B3heOAfz-CGXQwWeO.js → shade-app-B3heOAfz-BhQXxNkT.js} +1 -1
  143. package/core/built/admin/assets/{share-modal-BJCadJyp.js → share-modal-BeuydaBf.js} +1 -1
  144. package/core/built/admin/assets/social-urls-DA2lXrhW.js +8 -0
  145. package/core/built/admin/assets/{sort-button-DyYlfYp7.js → sort-button-Df4t2AL8.js} +1 -1
  146. package/core/built/admin/assets/{source-icon-_ubRDbg_.js → source-icon-ajoaKb41.js} +1 -1
  147. package/core/built/admin/assets/{stack-xiN0eQ6Z.js → stack-CIfOAAVf.js} +1 -1
  148. package/core/built/admin/assets/{stats-BQZMGMvn.js → stats-C_lfxZAw.js} +1 -1
  149. package/core/built/admin/assets/{stats-view-Doei-qCX.js → stats-view-Cxot1iD8.js} +1 -1
  150. package/core/built/admin/assets/{step-1-IKiLBPoC.js → step-1-ogAA4lDr.js} +1 -1
  151. package/core/built/admin/assets/{step-2-jJ41PsDN.js → step-2-C5Xini-7.js} +1 -1
  152. package/core/built/admin/assets/{step-3-CaqFAEqf.js → step-3-CZv1FV81.js} +2 -2
  153. package/core/built/admin/assets/{switch-t-WyENU2.js → switch-CyQcwMAv.js} +1 -1
  154. package/core/built/admin/assets/{table-DxUL78jU.js → table-DFj1wiYv.js} +1 -1
  155. package/core/built/admin/assets/tabs-D4y1gqUd.js +1 -0
  156. package/core/built/admin/assets/{tags-CNujGwkT.js → tags-Dmft-Y4O.js} +1 -1
  157. package/core/built/admin/assets/{text-DwtjwGkh.js → text-DU9Hv33_.js} +1 -1
  158. package/core/built/admin/assets/textarea-d9qEkMOP.js +1 -0
  159. package/core/built/admin/assets/{tiers-BgYLWRJv.js → tiers-DYsa4DCo.js} +1 -1
  160. package/core/built/admin/assets/{toggle-group-BeEpQ69p.js → toggle-group-R2QMFvXW.js} +1 -1
  161. package/core/built/admin/assets/{topic-filter-Bb5MOD-1.js → topic-filter-DGDxDalo.js} +1 -1
  162. package/core/built/admin/assets/{use-analytics-data-Cds6XDjg.js → use-analytics-data-C76v4LIp.js} +1 -1
  163. package/core/built/admin/assets/use-filter-params-WrdhAISv.js +1 -0
  164. package/core/built/admin/assets/{use-growth-stats-D8uf98-G.js → use-growth-stats-aNXJOmQr.js} +1 -1
  165. package/core/built/admin/assets/{use-post-referrers-CRtgyuXB.js → use-post-referrers-CG0SZioj.js} +1 -1
  166. package/core/built/admin/assets/{use-reply-chain-data-DbKYX50v.js → use-reply-chain-data-RLdPKRmS.js} +1 -1
  167. package/core/built/admin/assets/{use-simple-pagination-DC01kpo0.js → use-simple-pagination-DgcF69KI.js} +1 -1
  168. package/core/built/admin/assets/{use-tinybird-query-C7bQRJWw.js → use-tinybird-query-C1R_Kbmy.js} +1 -1
  169. package/core/built/admin/assets/{use-tinybird-token-Az4tam7_.js → use-tinybird-token-C9fPjVM_.js} +1 -1
  170. package/core/built/admin/assets/{users-CcSCQFcQ.js → users-Dkv8NQZe.js} +1 -1
  171. package/core/built/admin/assets/{virtual-list-Dhuh9fGx.js → virtual-list-BmTUzeoF.js} +1 -1
  172. package/core/built/admin/assets/web-BqW0b0_C.js +1 -0
  173. package/core/built/admin/assets/web-Cy5GKMWM.js +2 -0
  174. package/core/built/admin/index.html +18 -18
  175. package/core/frontend/services/data/fetch-data.js +10 -5
  176. package/core/frontend/services/proxy.js +7 -8
  177. package/core/frontend/services/routing/api-adapter.js +115 -0
  178. package/core/frontend/services/routing/api-adapter.ts +157 -0
  179. package/core/frontend/services/routing/collection-router.js +5 -3
  180. package/core/frontend/services/routing/controllers/index.js +1 -1
  181. package/core/frontend/services/routing/controllers/rss.js +26 -31
  182. package/core/frontend/services/routing/controllers/rss.ts +66 -0
  183. package/core/frontend/services/routing/controllers/static.js +8 -7
  184. package/core/frontend/services/routing/parent-router.js +17 -4
  185. package/core/frontend/services/routing/permalink-adapter.js +26 -0
  186. package/core/frontend/services/routing/permalink-adapter.ts +23 -0
  187. package/core/frontend/services/routing/registry.js +59 -33
  188. package/core/frontend/services/routing/router-manager.js +14 -18
  189. package/core/frontend/services/routing/static-routes-router.js +11 -13
  190. package/core/frontend/services/routing/taxonomy-router.js +6 -2
  191. package/core/server/api/endpoints/automation-action-links.js +68 -0
  192. package/core/server/api/endpoints/automation-action-links.ts +46 -0
  193. package/core/server/api/endpoints/index.js +4 -0
  194. package/core/server/api/endpoints/settings.js +0 -2
  195. package/core/server/api/endpoints/utils/serializers/output/utils/url.js +11 -1
  196. package/core/server/data/migrations/versions/6.55/2026-07-27-11-25-44-add-to-hash-to-redirects.js +51 -0
  197. package/core/server/data/migrations/versions/6.55/2026-07-28-12-08-00-remove-routes-hash-from-settings.js +5 -0
  198. package/core/server/data/migrations/versions/6.56/2026-08-03-19-27-51-add-automation-run-step-id-to-automated-email-recipients.js +8 -0
  199. package/core/server/data/migrations/versions/6.56/2026-08-04-14-42-39-restrict-notification-add-permission-to-administrators.js +12 -0
  200. package/core/server/data/schema/commands.js +5 -0
  201. package/core/server/data/schema/default-settings/default-settings.json +0 -4
  202. package/core/server/data/schema/fixtures/fixtures.json +2 -2
  203. package/core/server/data/schema/schema.js +6 -1
  204. package/core/server/models/post.js +10 -1
  205. package/core/server/services/automations/automations-api.js +15 -0
  206. package/core/server/services/automations/automations-api.ts +19 -0
  207. package/core/server/services/automations/automations-repository.ts +20 -0
  208. package/core/server/services/automations/database-automations-repository.js +132 -30
  209. package/core/server/services/automations/database-automations-repository.ts +141 -12
  210. package/core/server/services/automations/database-date.js +10 -0
  211. package/core/server/services/automations/database-date.ts +5 -0
  212. package/core/server/services/automations/get-scheduler-idempotency-key.js +13 -0
  213. package/core/server/services/automations/get-scheduler-idempotency-key.ts +8 -0
  214. package/core/server/services/automations/poll.js +7 -2
  215. package/core/server/services/automations/poll.ts +10 -2
  216. package/core/server/services/automations/scheduler-poll-time.js +40 -0
  217. package/core/server/services/automations/scheduler-poll-time.ts +38 -0
  218. package/core/server/services/automations/service.js +19 -3
  219. package/core/server/services/automations/service.ts +20 -3
  220. package/core/server/services/comments/comments-service-emails.js +10 -7
  221. package/core/server/services/comments/comments-service.js +9 -4
  222. package/core/server/services/email-rendering/partials/email-wrapper.hbs +3 -1
  223. package/core/server/services/email-service/email-service-wrapper.js +6 -1
  224. package/core/server/services/email-service/email-templates/partials/styles.hbs +8 -0
  225. package/core/server/services/email-service/newsletter-email-event-storage.js +41 -10
  226. package/core/server/services/gift-links/CONTEXT.md +9 -0
  227. package/core/server/services/gifts/CONTEXT.md +55 -0
  228. package/core/server/services/gifts/README.md +28 -0
  229. package/core/server/services/gifts/gift-bookshelf-repository.js +65 -45
  230. package/core/server/services/gifts/gift-bookshelf-repository.ts +203 -74
  231. package/core/server/services/gifts/gift-checkout-adapter.js +59 -0
  232. package/core/server/services/gifts/gift-checkout-offer.js +74 -0
  233. package/core/server/services/gifts/gift-checkout-offer.ts +127 -0
  234. package/core/server/services/gifts/gift-codec.js +63 -0
  235. package/core/server/services/gifts/gift-codec.ts +62 -0
  236. package/core/server/services/gifts/gift-controller.js +6 -31
  237. package/core/server/services/gifts/gift-controller.ts +6 -69
  238. package/core/server/services/gifts/gift-email-service.ts +3 -2
  239. package/core/server/services/gifts/gift-schema.js +34 -0
  240. package/core/server/services/gifts/gift-schema.ts +39 -0
  241. package/core/server/services/gifts/gift-service-wrapper.js +13 -11
  242. package/core/server/services/gifts/gift-service.js +265 -52
  243. package/core/server/services/gifts/gift-service.ts +431 -97
  244. package/core/server/services/gifts/gift.ts +16 -39
  245. package/core/server/services/lib/dynamic-redirect-manager.js +12 -3
  246. package/core/server/services/link-redirection/link-redirect-repository.js +41 -4
  247. package/core/server/services/link-redirection/link-redirect.js +2 -0
  248. package/core/server/services/link-redirection/link-redirect.ts +3 -1
  249. package/core/server/services/link-redirection/link-redirects-service.js +38 -2
  250. package/core/server/services/link-tracking/click-event.js +3 -0
  251. package/core/server/services/link-tracking/index.js +38 -6
  252. package/core/server/services/link-tracking/link-click-repository.js +35 -9
  253. package/core/server/services/link-tracking/link-click-tracking-service.js +54 -4
  254. package/core/server/services/member-attribution/outbound-link-tagger.js +3 -1
  255. package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +20 -4
  256. package/core/server/services/member-welcome-emails/service.js +15 -3
  257. package/core/server/services/members/api.js +1 -2
  258. package/core/server/services/members/import-export/csv/formula.js +16 -0
  259. package/core/server/services/members/import-export/csv/formula.ts +14 -0
  260. package/core/server/services/members/import-export/csv/index.js +3 -1
  261. package/core/server/services/members/import-export/csv/index.ts +1 -0
  262. package/core/server/services/members/import-export/import/completion-email.js +22 -3
  263. package/core/server/services/members/import-export/import/completion-email.ts +29 -7
  264. package/core/server/services/members/import-export/import/importer.js +25 -4
  265. package/core/server/services/members/import-export/import/importer.ts +44 -5
  266. package/core/server/services/members/import-export/index.js +18 -1
  267. package/core/server/services/members/import-export/index.ts +32 -3
  268. package/core/server/services/members/members-api/controllers/router-controller.js +38 -36
  269. package/core/server/services/members/members-api/members-api.js +13 -7
  270. package/core/server/services/members/members-api/repositories/event-repository.js +4 -94
  271. package/core/server/services/members/members-api/services/member-bread-service.js +3 -3
  272. package/core/server/services/members/members-api/services/payments-service.js +6 -68
  273. package/core/server/services/members/service.js +8 -1
  274. package/core/server/services/members-custom-fields/schema.js +8 -1
  275. package/core/server/services/members-custom-fields/schema.ts +8 -0
  276. package/core/server/services/members-custom-fields/values-service.js +136 -8
  277. package/core/server/services/members-custom-fields/values-service.ts +152 -10
  278. package/core/server/services/mentions/service.js +5 -1
  279. package/core/server/services/oembed/oembed-service.js +44 -3
  280. package/core/server/services/posts/posts-service.js +11 -0
  281. package/core/server/services/route-settings/dynamic-routing-service.js +21 -52
  282. package/core/server/services/route-settings/index.js +1 -16
  283. package/core/server/services/route-settings/route-settings-parser.js +25 -25
  284. package/core/server/services/route-settings/route-settings-parser.ts +27 -27
  285. package/core/server/services/route-settings/validation-errors.js +68 -4
  286. package/core/server/services/route-settings/validation-errors.ts +79 -4
  287. package/core/server/services/settings/settings-service.js +0 -18
  288. package/core/server/services/stats/posts-stats-service.js +27 -23
  289. package/core/server/services/stripe/services/webhook/charge-refunded-event-service.js +1 -1
  290. package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +16 -4
  291. package/core/server/services/stripe/stripe-api.js +10 -0
  292. package/core/server/services/url/url-service-facade.js +1 -1
  293. package/core/server/services/url/url-service-facade.ts +8 -1
  294. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  295. package/core/server/web/gift-preview/controller.js +10 -25
  296. package/core/shared/labs.js +2 -2
  297. package/package.json +8 -7
  298. package/pnpm-lock.yaml +28 -18
  299. package/pnpm-workspace.yaml +6 -4
  300. package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
  301. package/core/built/admin/assets/Growth-BTP1LthE.js +0 -3
  302. package/core/built/admin/assets/Newsletters-CSEX0myc.js +0 -1
  303. package/core/built/admin/assets/Overview-BjACEsJF.js +0 -1
  304. package/core/built/admin/assets/Web-CxxxjCcv.js +0 -2
  305. package/core/built/admin/assets/activitypub/layout-CziktJXo.js +0 -1
  306. package/core/built/admin/assets/audience-X_XsZivI.js +0 -1
  307. package/core/built/admin/assets/chunk.524.8365cd9280405c0f6216.js +0 -25
  308. package/core/built/admin/assets/chunk.582.61d078937e6102769f2c.js +0 -27
  309. package/core/built/admin/assets/comments-CvyXxOyN.js +0 -1
  310. package/core/built/admin/assets/constants-c-JNF9c2.js +0 -1
  311. package/core/built/admin/assets/dist-CjY-f5Z1.js +0 -1
  312. package/core/built/admin/assets/editor-BOG2YPUm.js +0 -8
  313. package/core/built/admin/assets/field-D0XISCK2.js +0 -1
  314. package/core/built/admin/assets/filter-sources-Bieps75g.js +0 -1
  315. package/core/built/admin/assets/filters-CuB-A7ma.js +0 -1
  316. package/core/built/admin/assets/gift-link-modal-su8MsRdg.js +0 -1
  317. package/core/built/admin/assets/growth-Ba6S5FQX.js +0 -1
  318. package/core/built/admin/assets/index-D8PbzuT9.css +0 -1
  319. package/core/built/admin/assets/index-ukW_0Uie.js +0 -67
  320. package/core/built/admin/assets/input-group-CGOXcy4l.js +0 -1
  321. package/core/built/admin/assets/label-picker-Uq91atex.js +0 -1
  322. package/core/built/admin/assets/labels-Cyayfd-e.js +0 -1
  323. package/core/built/admin/assets/layout-WUcz6tu1.js +0 -1
  324. package/core/built/admin/assets/link-helpers-tOlxmuH5.js +0 -1
  325. package/core/built/admin/assets/member-custom-fields-NCzrMuba.js +0 -1
  326. package/core/built/admin/assets/member-detail-DPWDtYnN.js +0 -1
  327. package/core/built/admin/assets/members-B9rO7qr4.js +0 -10
  328. package/core/built/admin/assets/modals-CxRaan1J.js +0 -31
  329. package/core/built/admin/assets/moderation-D8zon60E.js +0 -1
  330. package/core/built/admin/assets/multi-select-combobox-1wYm3nle.js +0 -1
  331. package/core/built/admin/assets/newsletter-DbYMRxRS.js +0 -1
  332. package/core/built/admin/assets/note-PiWPnnrh.js +0 -1
  333. package/core/built/admin/assets/notifications-DEgqqB9G.js +0 -1
  334. package/core/built/admin/assets/nql-BsgFMd93.js +0 -1
  335. package/core/built/admin/assets/onboarding-route-BzF6HeVn.js +0 -1
  336. package/core/built/admin/assets/overview-DixLeIqY.js +0 -1
  337. package/core/built/admin/assets/post-analytics-header-DsMoLx0R.js +0 -1
  338. package/core/built/admin/assets/post-analytics-provider-BJCPLMIb.js +0 -1
  339. package/core/built/admin/assets/preferences-D7-j6A4C.js +0 -5
  340. package/core/built/admin/assets/profile-Dl69QPp0.js +0 -1
  341. package/core/built/admin/assets/select-C1qDZnW4.js +0 -1
  342. package/core/built/admin/assets/settings-BbmC_Iqq.js +0 -4
  343. package/core/built/admin/assets/social-urls-BPL1nADp.js +0 -8
  344. package/core/built/admin/assets/tabs-BQcJQ1ke.js +0 -1
  345. package/core/built/admin/assets/textarea-Bjccvh_E.js +0 -1
  346. package/core/built/admin/assets/use-filter-params-DjXkhO54.js +0 -1
  347. package/core/built/admin/assets/web-LtHNiNBV.js +0 -1
  348. package/core/server/services/gifts/gift-repository.js +0 -2
  349. package/core/server/services/gifts/gift-repository.ts +0 -29
  350. package/core/server/services/route-settings/activation-bridge.js +0 -164
  351. package/core/server/services/route-settings/activation-bridge.ts +0 -210
  352. package/core/server/services/route-settings/default-settings-manager.js +0 -63
  353. package/core/server/services/route-settings/route-settings.js +0 -170
  354. package/core/server/services/route-settings/settings-loader.js +0 -60
  355. package/core/server/services/route-settings/settings-path-manager.js +0 -47
  356. package/core/server/services/route-settings/validate.js +0 -440
@@ -0,0 +1,8 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('automated_email_recipients', 'automation_run_step_id', {
4
+ type: 'string',
5
+ maxlength: 24,
6
+ nullable: true,
7
+ references: 'automation_run_steps.id'
8
+ }, {algorithm: 'auto'});
@@ -0,0 +1,12 @@
1
+ const {combineTransactionalMigrations, removePermissionFromRole} = require('../../utils');
2
+
3
+ module.exports = combineTransactionalMigrations(
4
+ removePermissionFromRole({
5
+ permission: 'Add notifications',
6
+ role: 'Editor'
7
+ }),
8
+ removePermissionFromRole({
9
+ permission: 'Add notifications',
10
+ role: 'Super Editor'
11
+ })
12
+ );
@@ -25,6 +25,11 @@ function addTableColumn(tableName, tableBuilder, columnName, columnSpec = schema
25
25
  // creation distinguishes between text with fieldtype, string with maxlength and all others
26
26
  if (columnSpec.type === 'text' && Object.prototype.hasOwnProperty.call(columnSpec, 'fieldtype')) {
27
27
  column = tableBuilder[columnSpec.type](columnName, columnSpec.fieldtype);
28
+ } else if (columnSpec.type === 'binary' && Object.prototype.hasOwnProperty.call(columnSpec, 'maxlength')) {
29
+ // knex emits an unbounded `blob` for a length-less binary column, which
30
+ // MySQL can only index with a prefix. Passing the length gives us
31
+ // `varbinary(N)`, which is indexable in full.
32
+ column = tableBuilder[columnSpec.type](columnName, columnSpec.maxlength);
28
33
  } else if (columnSpec.type === 'string') {
29
34
  if (Object.prototype.hasOwnProperty.call(columnSpec, 'maxlength')) {
30
35
  column = tableBuilder[columnSpec.type](columnName, columnSpec.maxlength);
@@ -8,10 +8,6 @@
8
8
  "defaultValue": null,
9
9
  "type": "string"
10
10
  },
11
- "routes_hash": {
12
- "defaultValue": null,
13
- "type": "string"
14
- },
15
11
  "next_update_check": {
16
12
  "defaultValue": null,
17
13
  "type": "number"
@@ -1066,7 +1066,7 @@
1066
1066
  "gift_link": "manage"
1067
1067
  },
1068
1068
  "Super Editor": {
1069
- "notification": "all",
1069
+ "notification": ["browse", "destroy"],
1070
1070
  "post": "all",
1071
1071
  "setting": ["browse", "read"],
1072
1072
  "slug": "all",
@@ -1091,7 +1091,7 @@
1091
1091
  "gift_link": "manage"
1092
1092
  },
1093
1093
  "Editor": {
1094
- "notification": "all",
1094
+ "notification": ["browse", "destroy"],
1095
1095
  "post": "all",
1096
1096
  "setting": ["browse", "read"],
1097
1097
  "slug": "all",
@@ -1263,8 +1263,12 @@ module.exports = {
1263
1263
  to: {type: 'string', maxlength: 2000, nullable: false},
1264
1264
  post_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'posts.id', setNullDelete: true},
1265
1265
  automation_action_revision_id: {type: 'string', maxlength: 24, nullable: true, references: 'automation_action_revisions.id', setNullDelete: true},
1266
+ to_hash: {type: 'binary', maxlength: 32, nullable: true},
1266
1267
  created_at: {type: 'dateTime', nullable: false},
1267
- updated_at: {type: 'dateTime', nullable: true}
1268
+ updated_at: {type: 'dateTime', nullable: true},
1269
+ '@@UNIQUE_CONSTRAINTS@@': [
1270
+ ['automation_action_revision_id', 'to_hash']
1271
+ ]
1268
1272
  },
1269
1273
  members_click_events: {
1270
1274
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
@@ -1335,6 +1339,7 @@ module.exports = {
1335
1339
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
1336
1340
  automated_email_id: {type: 'string', maxlength: 24, nullable: true, references: 'welcome_email_automated_emails.id'},
1337
1341
  automation_action_revision_id: {type: 'string', maxlength: 24, nullable: true, references: 'automation_action_revisions.id'},
1342
+ automation_run_step_id: {type: 'string', maxlength: 24, nullable: true, references: 'automation_run_steps.id'},
1338
1343
  member_id: {type: 'string', maxlength: 24, nullable: false, index: true},
1339
1344
  member_uuid: {type: 'string', maxlength: 36, nullable: false},
1340
1345
  member_email: {type: 'string', maxlength: 191, nullable: false},
@@ -1346,7 +1346,16 @@ Post = ghostBookshelf.Model.extend({
1346
1346
  destroy: function destroy(unfilteredOptions) {
1347
1347
  let options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
1348
1348
 
1349
- const destroyPost = () => {
1349
+ const destroyPost = async () => {
1350
+ // The `comments.in_reply_to_id` references form chains between a post's
1351
+ // comments, which MySQL cannot resolve while cascade-deleting them
1352
+ // alongside `comments.parent_id`. Clear the references first so the
1353
+ // `comments.post_id` cascade delete can do its job
1354
+ await ghostBookshelf.knex('comments')
1355
+ .where('post_id', options.id)
1356
+ .update('in_reply_to_id', null)
1357
+ .transacting(options.transacting);
1358
+
1350
1359
  return ghostBookshelf.Model.destroy.call(this, options);
1351
1360
  };
1352
1361
 
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.browse = browse;
7
7
  exports.read = read;
8
+ exports.browseActionLinks = browseActionLinks;
8
9
  exports.edit = edit;
9
10
  exports.requestPoll = requestPoll;
10
11
  exports.trigger = trigger;
@@ -15,6 +16,7 @@ exports.retryStep = retryStep;
15
16
  exports.recordEmailSent = recordEmailSent;
16
17
  exports.getAutomatedEmailRecipientsByMailgunIds = getAutomatedEmailRecipientsByMailgunIds;
17
18
  exports.trackEmailDeliveredAndOpened = trackEmailDeliveredAndOpened;
19
+ exports.trackEmailClicked = trackEmailClicked;
18
20
  const errors_1 = __importDefault(require("@tryghost/errors"));
19
21
  const tpl_1 = __importDefault(require("@tryghost/tpl"));
20
22
  const bson_objectid_1 = __importDefault(require("bson-objectid"));
@@ -30,6 +32,7 @@ const StartAutomationsPollEvent = require('./events/start-automations-poll-event
30
32
  const MAX_AUTOMATION_ACTIONS = 20;
31
33
  const messages = {
32
34
  automationNotFound: 'Automation not found.',
35
+ automationActionNotFound: 'Automation action not found.',
33
36
  invalidAutomationPayload: 'Automation edit payload must include status, actions, and edges.',
34
37
  invalidAutomationStatus: 'Automation status must be one of: active, inactive.',
35
38
  duplicateAutomationActionIdentity: 'Automation action identifiers must be unique.',
@@ -86,6 +89,15 @@ async function read(automationId) {
86
89
  }
87
90
  return automation;
88
91
  }
92
+ async function browseActionLinks(automationId, actionId) {
93
+ const links = await repository.getAutomationActionLinks(automationId, actionId);
94
+ if (!links) {
95
+ throw new errors_1.default.NotFoundError({
96
+ message: (0, tpl_1.default)(messages.automationActionNotFound)
97
+ });
98
+ }
99
+ return links;
100
+ }
89
101
  async function edit(automationId, data) {
90
102
  const parsedData = await validateEditData(data);
91
103
  const automation = await repository.edit(automationId, parsedData);
@@ -311,3 +323,6 @@ async function getAutomatedEmailRecipientsByMailgunIds(...args) {
311
323
  async function trackEmailDeliveredAndOpened(...args) {
312
324
  return await repository.trackEmailDeliveredAndOpened(...args);
313
325
  }
326
+ async function trackEmailClicked(...args) {
327
+ await repository.trackEmailClicked(...args);
328
+ }
@@ -20,6 +20,7 @@ const MAX_AUTOMATION_ACTIONS = 20;
20
20
 
21
21
  const messages = {
22
22
  automationNotFound: 'Automation not found.',
23
+ automationActionNotFound: 'Automation action not found.',
23
24
  invalidAutomationPayload: 'Automation edit payload must include status, actions, and edges.',
24
25
  invalidAutomationStatus: 'Automation status must be one of: active, inactive.',
25
26
  duplicateAutomationActionIdentity: 'Automation action identifiers must be unique.',
@@ -87,6 +88,18 @@ export async function read(automationId: string) {
87
88
  return automation;
88
89
  }
89
90
 
91
+ export async function browseActionLinks(automationId: string, actionId: string) {
92
+ const links = await repository.getAutomationActionLinks(automationId, actionId);
93
+
94
+ if (!links) {
95
+ throw new errors.NotFoundError({
96
+ message: tpl(messages.automationActionNotFound)
97
+ });
98
+ }
99
+
100
+ return links;
101
+ }
102
+
90
103
  export async function edit(automationId: string, data: unknown) {
91
104
  const parsedData = await validateEditData(data);
92
105
 
@@ -375,3 +388,9 @@ export async function trackEmailDeliveredAndOpened(
375
388
  ) {
376
389
  return await repository.trackEmailDeliveredAndOpened(...args);
377
390
  }
391
+
392
+ export async function trackEmailClicked(
393
+ ...args: Parameters<AutomationsRepository['trackEmailClicked']>
394
+ ) {
395
+ await repository.trackEmailClicked(...args);
396
+ }
@@ -1,4 +1,5 @@
1
1
  import type {ReadonlyDeep} from 'type-fest';
2
+ import type {Knex} from 'knex';
2
3
 
3
4
  export interface Pagination {
4
5
  page: number;
@@ -32,6 +33,11 @@ export interface AutomationEmailStats {
32
33
  clicked_rate: number | null;
33
34
  }
34
35
 
36
+ export interface AutomationActionLink {
37
+ url: string;
38
+ clicked_count: number;
39
+ }
40
+
35
41
  export interface SendEmailAction {
36
42
  id: string;
37
43
  type: 'send_email';
@@ -84,6 +90,7 @@ export type AutomatedEmailEvents = {
84
90
 
85
91
  export type RecordEmailSentOptions = Readonly<{
86
92
  automationActionRevisionId: string;
93
+ automationRunStepId: string;
87
94
  mailgunMessageId?: string;
88
95
  memberEmail: string;
89
96
  memberId: string;
@@ -131,6 +138,7 @@ export type AutomationStepTerminalStatus =
131
138
  export interface AutomationsRepository {
132
139
  browse(): Promise<Page<AutomationSummary>>;
133
140
  getById(id: string): Promise<Automation | null>;
141
+ getAutomationActionLinks(automationId: string, actionId: string): Promise<AutomationActionLink[] | null>;
134
142
  edit(id: string, data: EditAutomationData): Promise<Automation | null>;
135
143
  trigger(options: {
136
144
  memberEmail: string;
@@ -195,4 +203,16 @@ export interface AutomationsRepository {
195
203
  trackEmailDeliveredAndOpened(
196
204
  eventsByAutomatedEmailRecipientId: ReadonlyDeep<Map<string, AutomatedEmailEvents>>
197
205
  ): Promise<void>;
206
+ /**
207
+ * Record the first click timestamp for the automated email recipient identified by a run step.
208
+ *
209
+ */
210
+ trackEmailClicked(options: {
211
+ automationActionRevisionId: string;
212
+ automationRunStepId: string;
213
+ memberId: string;
214
+ clickedAt: Readonly<Date>;
215
+ }, transactionOptions?: {
216
+ transacting?: Knex.Transaction;
217
+ }): Promise<void>;
198
218
  }
@@ -9,11 +9,13 @@ const tpl_1 = __importDefault(require("@tryghost/tpl"));
9
9
  const node_crypto_1 = __importDefault(require("node:crypto"));
10
10
  const bson_objectid_1 = __importDefault(require("bson-objectid"));
11
11
  const dequal_1 = require("dequal");
12
- const moment_1 = __importDefault(require("moment"));
12
+ // @ts-expect-error This module currently lacks type definitions.
13
+ const lexical_1 = __importDefault(require("../../lib/lexical"));
14
+ const url_utils_1 = __importDefault(require("../../../shared/url-utils"));
13
15
  const constants_1 = require("../member-welcome-emails/constants");
16
+ const database_date_1 = require("./database-date");
14
17
  const stale_lock_cutoff_1 = require("./stale-lock-cutoff");
15
18
  const HOUR_MS = 60 * 60 * 1000;
16
- const DATABASE_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
17
19
  const DEFAULT_WELCOME_EMAIL_AUTOMATIONS = [{
18
20
  name: 'Free member welcome flow',
19
21
  slug: constants_1.MEMBER_WELCOME_EMAIL_SLUGS.free
@@ -28,9 +30,6 @@ const messages = {
28
30
  defaultEmailDesignSettingNotFound: 'Default automated email design setting not found.'
29
31
  };
30
32
  const DEFAULT_EMAIL_DESIGN_SETTING_REFERENCE = constants_1.DEFAULT_EMAIL_DESIGN_SETTING_SLUG;
31
- function toDatabaseDate(date) {
32
- return (0, moment_1.default)(date).format(DATABASE_DATE_FORMAT);
33
- }
34
33
  function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier }) {
35
34
  return {
36
35
  async browse() {
@@ -54,6 +53,33 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
54
53
  return await buildAutomation(trx, automation);
55
54
  });
56
55
  },
56
+ async getAutomationActionLinks(automationId, actionId) {
57
+ const action = await knex('automation_actions')
58
+ .select('id')
59
+ .where({
60
+ id: actionId,
61
+ automation_id: automationId
62
+ })
63
+ .whereNull('deleted_at')
64
+ .first();
65
+ if (!action) {
66
+ return null;
67
+ }
68
+ const rows = await knex('redirects as redirects')
69
+ .countDistinct({ clicked_count: 'members_click_events.member_id' })
70
+ .select(knex.raw('MIN(??) as ??', ['redirects.to', 'url']))
71
+ .innerJoin('automation_action_revisions as revisions', 'revisions.id', 'redirects.automation_action_revision_id')
72
+ .leftJoin('members_click_events', 'members_click_events.redirect_id', 'redirects.id')
73
+ .where('revisions.action_id', actionId)
74
+ .whereNotNull('redirects.to_hash')
75
+ .groupBy('redirects.to_hash')
76
+ .orderBy('clicked_count', 'desc')
77
+ .orderBy('url', 'asc');
78
+ return rows.map(row => ({
79
+ url: url_utils_1.default.transformReadyToAbsolute(row.url),
80
+ clicked_count: Number(row.clicked_count)
81
+ }));
82
+ },
57
83
  async edit(id, data) {
58
84
  return await knex.transaction(async (trx) => {
59
85
  const automation = await loadAutomation(trx, id);
@@ -64,7 +90,7 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
64
90
  const updatedAutomation = await updateAutomation(trx, {
65
91
  ...automation,
66
92
  status: data.status,
67
- updated_at: toDatabaseDate(now)
93
+ updated_at: (0, database_date_1.toDatabaseDate)(now)
68
94
  });
69
95
  await replaceAutomationGraph(trx, updatedAutomation.id, data.actions, data.edges);
70
96
  if (updatedAutomation.status === 'inactive') {
@@ -101,7 +127,7 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
101
127
  .update({
102
128
  email_sent_count: trx.raw('COALESCE(??, 0) + ?', ['email_sent_count', 1])
103
129
  });
104
- const now = toDatabaseDate(new Date());
130
+ const now = (0, database_date_1.toDatabaseDate)(new Date());
105
131
  await trx('automated_email_recipients').insert({
106
132
  id: (0, bson_objectid_1.default)().toHexString(),
107
133
  member_id: options.memberId,
@@ -109,6 +135,7 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
109
135
  member_email: options.memberEmail,
110
136
  member_name: options.memberName,
111
137
  automation_action_revision_id: options.automationActionRevisionId,
138
+ automation_run_step_id: options.automationRunStepId,
112
139
  ...(options.mailgunMessageId ? { mailgun_message_id: options.mailgunMessageId } : {}),
113
140
  track_clicks: options.trackClicks,
114
141
  track_opens: options.trackOpens,
@@ -131,6 +158,13 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
131
158
  return;
132
159
  }
133
160
  await knex.transaction(async (trx) => {
161
+ const revisionIds = new Set();
162
+ for (const { openedAt, automationActionRevisionId } of eventsByAutomatedEmailRecipientId.values()) {
163
+ if (openedAt) {
164
+ revisionIds.add(automationActionRevisionId);
165
+ }
166
+ }
167
+ const orderedRevisionIds = await lockActionRevisions(trx, revisionIds);
134
168
  const notYetOpened = await lockNotYetOpened(trx, eventsByAutomatedEmailRecipientId);
135
169
  const newOpensPerRevision = new Map();
136
170
  for (const [id, { deliveredAt, openedAt, automationActionRevisionId }] of eventsByAutomatedEmailRecipientId) {
@@ -151,10 +185,11 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
151
185
  newOpensPerRevision.set(automationActionRevisionId, (newOpensPerRevision.get(automationActionRevisionId) ?? 0) + 1);
152
186
  }
153
187
  }
154
- // Keep lock acquisition order consistent across concurrent transactions to avoid deadlocks.
155
- const revisions = [...newOpensPerRevision.entries()]
156
- .sort(([left], [right]) => left.localeCompare(right));
157
- for (const [id, opens] of revisions) {
188
+ for (const id of orderedRevisionIds) {
189
+ const opens = newOpensPerRevision.get(id);
190
+ if (!opens) {
191
+ continue;
192
+ }
158
193
  await trx('automation_action_revisions')
159
194
  .where({ id })
160
195
  .update({
@@ -162,9 +197,61 @@ function createDatabaseAutomationsRepository({ knex, fakeWaitHoursMultiplier })
162
197
  });
163
198
  }
164
199
  });
200
+ },
201
+ async trackEmailClicked({ automationActionRevisionId, automationRunStepId, memberId, clickedAt }, { transacting } = {}) {
202
+ const trackClick = async (trx) => {
203
+ await lockActionRevisions(trx, [automationActionRevisionId]);
204
+ const recipient = await trx('automated_email_recipients')
205
+ .select('id', 'clicked_at')
206
+ .where({
207
+ automation_action_revision_id: automationActionRevisionId,
208
+ automation_run_step_id: automationRunStepId,
209
+ member_id: memberId,
210
+ track_clicks: true
211
+ })
212
+ .forUpdate()
213
+ .first();
214
+ if (!recipient || recipient.clicked_at !== null) {
215
+ return;
216
+ }
217
+ const updated = await trx('automated_email_recipients')
218
+ .where({ id: recipient.id })
219
+ .whereNull('clicked_at')
220
+ .update({ clicked_at: clickedAt });
221
+ if (updated === 0) {
222
+ return;
223
+ }
224
+ await trx('automation_action_revisions')
225
+ .where({ id: automationActionRevisionId })
226
+ .update({
227
+ email_clicked_count: trx.raw('COALESCE(email_clicked_count, 0) + 1')
228
+ });
229
+ };
230
+ if (transacting) {
231
+ await trackClick(transacting);
232
+ return;
233
+ }
234
+ await knex.transaction(trackClick);
165
235
  }
166
236
  };
167
237
  }
238
+ /**
239
+ * Lock revisions before recipients because inserting a recipient takes a shared
240
+ * foreign-key lock on its revision. Updating that revision later can deadlock
241
+ * with another transaction that has already locked the revision and is waiting
242
+ * for the recipient. Keep multi-revision lock acquisition deterministic.
243
+ */
244
+ async function lockActionRevisions(trx, revisionIds) {
245
+ const sortedRevisionIds = [...new Set(revisionIds)]
246
+ .sort((left, right) => left.localeCompare(right));
247
+ if (sortedRevisionIds.length > 0) {
248
+ await trx('automation_action_revisions')
249
+ .select('id')
250
+ .whereIn('id', sortedRevisionIds)
251
+ .forUpdate();
252
+ }
253
+ return sortedRevisionIds;
254
+ }
168
255
  /**
169
256
  * Which of these recipients have yet to open, and so should count towards their
170
257
  * revision's open count. Locks them for the transaction, so a worker racing on
@@ -194,7 +281,7 @@ async function ensureDefaultAutomations(trx) {
194
281
  }
195
282
  }
196
283
  async function ensureAutomation(trx, defaults) {
197
- const now = toDatabaseDate(new Date());
284
+ const now = (0, database_date_1.toDatabaseDate)(new Date());
198
285
  const id = (0, bson_objectid_1.default)().toHexString();
199
286
  await trx('automations')
200
287
  .insert({
@@ -233,7 +320,7 @@ async function ensureWelcomeEmailAction(trx, automationId) {
233
320
  if (!email) {
234
321
  return;
235
322
  }
236
- const now = toDatabaseDate(new Date());
323
+ const now = (0, database_date_1.toDatabaseDate)(new Date());
237
324
  const actionId = (0, bson_objectid_1.default)().toHexString();
238
325
  await insertActions(trx, [{
239
326
  id: actionId,
@@ -263,7 +350,7 @@ async function trigger(trx, options) {
263
350
  return;
264
351
  }
265
352
  const now = new Date();
266
- const nowString = toDatabaseDate(now);
353
+ const nowString = (0, database_date_1.toDatabaseDate)(now);
267
354
  const readyAt = getReadyAtForAction(firstAction, now, fakeWaitHoursMultiplier);
268
355
  const run = {
269
356
  id: (0, bson_objectid_1.default)().toHexString(),
@@ -282,14 +369,14 @@ async function trigger(trx, options) {
282
369
  });
283
370
  }
284
371
  async function insertRunStep(trx, { automationRunId, automationActionRevisionId, now, readyAt }) {
285
- const nowString = toDatabaseDate(now);
372
+ const nowString = (0, database_date_1.toDatabaseDate)(now);
286
373
  await trx('automation_run_steps').insert({
287
374
  id: (0, bson_objectid_1.default)().toHexString(),
288
375
  created_at: nowString,
289
376
  updated_at: nowString,
290
377
  automation_run_id: automationRunId,
291
378
  automation_action_revision_id: automationActionRevisionId,
292
- ready_at: toDatabaseDate(readyAt)
379
+ ready_at: (0, database_date_1.toDatabaseDate)(readyAt)
293
380
  });
294
381
  }
295
382
  async function fetchAndLockSteps(trx, limit) {
@@ -304,9 +391,9 @@ async function fetchAndLockSteps(trx, limit) {
304
391
  // 2. Try to lock those rows.
305
392
  // 3. Select any rows we successfully locked.
306
393
  const now = new Date();
307
- const nowString = toDatabaseDate(now);
394
+ const nowString = (0, database_date_1.toDatabaseDate)(now);
308
395
  const staleLockCutoff = (0, stale_lock_cutoff_1.getStaleLockCutoff)(now);
309
- const staleLockCutoffString = toDatabaseDate(staleLockCutoff);
396
+ const staleLockCutoffString = (0, database_date_1.toDatabaseDate)(staleLockCutoff);
310
397
  const lockId = node_crypto_1.default.randomUUID();
311
398
  // 1. Select up to `limit` candidate rows.
312
399
  const candidates = await trx('automation_run_steps')
@@ -374,7 +461,7 @@ async function findNextPendingReadyAt(trx, staleLockCutoff) {
374
461
  .where((builder) => {
375
462
  builder
376
463
  .whereNull('locked_by')
377
- .orWhere('locked_at', '<', toDatabaseDate(staleLockCutoff));
464
+ .orWhere('locked_at', '<', (0, database_date_1.toDatabaseDate)(staleLockCutoff));
378
465
  })
379
466
  .orderBy('ready_at')
380
467
  .first();
@@ -479,7 +566,7 @@ async function findNextActionRevision(trx, sourceActionId) {
479
566
  return row ?? null;
480
567
  }
481
568
  async function markStepTerminal(trx, step, status) {
482
- const nowString = toDatabaseDate(new Date());
569
+ const nowString = (0, database_date_1.toDatabaseDate)(new Date());
483
570
  return await updateStep(trx, step, {
484
571
  status,
485
572
  finished_at: nowString,
@@ -491,12 +578,12 @@ async function retryStep(trx, step, retryAt) {
491
578
  await markStepTerminal(trx, step, 'automation disabled');
492
579
  return false;
493
580
  }
494
- const nowString = toDatabaseDate(new Date());
581
+ const nowString = (0, database_date_1.toDatabaseDate)(new Date());
495
582
  return await updateStep(trx, step, {
496
583
  status: 'pending',
497
584
  started_at: null,
498
585
  finished_at: null,
499
- ready_at: toDatabaseDate(retryAt),
586
+ ready_at: (0, database_date_1.toDatabaseDate)(retryAt),
500
587
  updated_at: nowString
501
588
  });
502
589
  }
@@ -521,8 +608,8 @@ function getReadyAtForAction(action, now, fakeWaitHoursMultiplier) {
521
608
  }
522
609
  }
523
610
  async function cancelCancelablePendingStepsForAutomation(trx, automationId, now) {
524
- const nowString = toDatabaseDate(now);
525
- const staleLockCutoff = toDatabaseDate((0, stale_lock_cutoff_1.getStaleLockCutoff)(now));
611
+ const nowString = (0, database_date_1.toDatabaseDate)(now);
612
+ const staleLockCutoff = (0, database_date_1.toDatabaseDate)((0, stale_lock_cutoff_1.getStaleLockCutoff)(now));
526
613
  await trx('automation_run_steps')
527
614
  .update({
528
615
  status: 'automation disabled',
@@ -626,11 +713,11 @@ async function updateAutomation(trx, automation) {
626
713
  async function replaceAutomationGraph(trx, automationId, submittedActions, edges) {
627
714
  const existingActions = await loadAutomationActionRows(trx, automationId);
628
715
  const existingActionById = new Map(existingActions.map(action => [action.id, action]));
629
- const actions = await resolveEmailDesignSettingIds(trx, submittedActions);
716
+ const actions = (await resolveEmailDesignSettingIds(trx, submittedActions)).map(formatActionOnWrite);
630
717
  const submittedActionIds = new Set(actions.map(action => action.id));
631
718
  const actionIdsWithOwners = await loadActionIdsWithOwners(trx, [...submittedActionIds]);
632
719
  const latestRevisionByActionId = new Map((await loadLatestActionRevisions(trx, [...submittedActionIds])).map(revision => [revision.action_id, revision]));
633
- const now = toDatabaseDate(new Date());
720
+ const now = (0, database_date_1.toDatabaseDate)(new Date());
634
721
  const actionsToInsert = [];
635
722
  const revisionsToInsert = [];
636
723
  for (const action of actions) {
@@ -680,6 +767,21 @@ async function replaceAutomationGraph(trx, automationId, submittedActions, edges
680
767
  await deleteAutomationEdges(trx, automationId);
681
768
  await insertActionEdges(trx, edges);
682
769
  }
770
+ function formatActionOnWrite(action) {
771
+ if (action.type !== 'send_email') {
772
+ return action;
773
+ }
774
+ return {
775
+ ...action,
776
+ data: {
777
+ ...action.data,
778
+ email_lexical: url_utils_1.default.lexicalToTransformReady(action.data.email_lexical, {
779
+ nodes: lexical_1.default.nodes,
780
+ transformMap: lexical_1.default.urlTransformMap
781
+ })
782
+ }
783
+ };
784
+ }
683
785
  async function resolveEmailDesignSettingIds(trx, actions) {
684
786
  if (!actions.some(action => action.type === 'send_email' && action.data.email_design_setting_id === DEFAULT_EMAIL_DESIGN_SETTING_REFERENCE)) {
685
787
  return [...actions];
@@ -793,14 +895,14 @@ async function insertActionRevisions(trx, revisions) {
793
895
  }
794
896
  function getNextRevisionCreatedAt(latestCreatedAt, requestedCreatedAt) {
795
897
  if (!latestCreatedAt) {
796
- return toDatabaseDate(requestedCreatedAt);
898
+ return (0, database_date_1.toDatabaseDate)(requestedCreatedAt);
797
899
  }
798
900
  const requestedTime = new Date(requestedCreatedAt).getTime();
799
901
  const latestTime = new Date(latestCreatedAt).getTime();
800
902
  if (requestedTime > latestTime) {
801
- return toDatabaseDate(requestedCreatedAt);
903
+ return (0, database_date_1.toDatabaseDate)(requestedCreatedAt);
802
904
  }
803
- return toDatabaseDate(new Date(latestTime + 1000));
905
+ return (0, database_date_1.toDatabaseDate)(new Date(latestTime + 1000));
804
906
  }
805
907
  function buildActionRevision(actionId, action, createdAt) {
806
908
  switch (action.type) {
@@ -946,7 +1048,7 @@ function buildActionPayload(row, stats) {
946
1048
  type: 'send_email',
947
1049
  data: {
948
1050
  email_subject: requireValue(row, 'email_subject'),
949
- email_lexical: requireValue(row, 'email_lexical'),
1051
+ email_lexical: url_utils_1.default.transformReadyToAbsolute(requireValue(row, 'email_lexical')),
950
1052
  email_design_setting_id: requireValue(row, 'email_design_setting_id')
951
1053
  },
952
1054
  stats: stats ?? EMPTY_EMAIL_STATS