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
@@ -368,10 +368,13 @@ class MemberWelcomeEmailService {
368
368
  * @param {null | object} options.email.designSettings
369
369
  * @param {'welcome' | 'automation'} options.emailType
370
370
  * @param {boolean} [options.trackOpens]
371
+ * @param {boolean} [options.trackClicks]
372
+ * @param {string | null} [options.automationActionRevisionId]
373
+ * @param {string | null} [options.automationRunStepId]
371
374
  * @param {null | {url: string, oneClickUrl: string}} [options.unsubscribe] - When set, the footer links to an unsubscribe URL and the email carries one-click List-Unsubscribe headers
372
375
  * @returns {Promise<unknown>}
373
376
  */
374
- async #sendEmail({member, memberStatus, email, emailType, trackOpens, unsubscribe = null}) {
377
+ async #sendEmail({member, memberStatus, email, emailType, trackOpens, trackClicks = false, automationActionRevisionId = null, automationRunStepId = null, unsubscribe = null}) {
375
378
  if (!member.email) {
376
379
  throw new errors.IncorrectUsageError({
377
380
  message: MESSAGES.MISSING_RECIPIENT_EMAIL
@@ -396,7 +399,10 @@ class MemberWelcomeEmailService {
396
399
  uuid: member.uuid
397
400
  },
398
401
  siteSettings: this.#getSiteSettings(),
399
- unsubscribeUrl: unsubscribe?.url
402
+ unsubscribeUrl: unsubscribe?.url,
403
+ trackClicks,
404
+ automationActionRevisionId,
405
+ automationRunStepId
400
406
  });
401
407
 
402
408
  const senderOptions = await this.#getEffectiveSenderOptions(
@@ -474,9 +480,12 @@ class MemberWelcomeEmailService {
474
480
  * @param {string} options.member.uuid
475
481
  * @param {'free' | 'paid'} options.memberStatus
476
482
  * @param {boolean} options.trackOpens
483
+ * @param {boolean} options.trackClicks
484
+ * @param {string} options.automationActionRevisionId
485
+ * @param {string} options.automationRunStepId
477
486
  * @returns {Promise<unknown>}
478
487
  */
479
- async sendAutomationEmail({email, member, memberStatus, trackOpens}) {
488
+ async sendAutomationEmail({email, member, memberStatus, trackOpens, trackClicks, automationActionRevisionId, automationRunStepId}) {
480
489
  const designSettings = email.designSettingId ?
481
490
  await EmailDesignSetting.findOne({id: email.designSettingId}) :
482
491
  null;
@@ -502,6 +511,9 @@ class MemberWelcomeEmailService {
502
511
  designSettings: designSettingsJson
503
512
  },
504
513
  trackOpens,
514
+ trackClicks,
515
+ automationActionRevisionId,
516
+ automationRunStepId,
505
517
  unsubscribe
506
518
  });
507
519
  }
@@ -244,8 +244,7 @@ function createApiInstance(config) {
244
244
  EmailSpamComplaintEvent: models.EmailSpamComplaintEvent,
245
245
  Automation: models.Automation,
246
246
  WelcomeEmailAutomationRun: models.WelcomeEmailAutomationRun,
247
- AutomatedEmailRecipient: models.AutomatedEmailRecipient,
248
- Gift: models.Gift
247
+ AutomatedEmailRecipient: models.AutomatedEmailRecipient
249
248
  },
250
249
  stripeAPIService: stripeService.api,
251
250
  tiersService: tiersService,
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stripFormulaGuard = stripFormulaGuard;
4
+ // The export writes with papaparse's escapeFormulae on (see serialize), which prefixes a
5
+ // cell starting with one of these with an apostrophe so a spreadsheet won't read it as a
6
+ // formula: `=SUM(A1)` is written as `'=SUM(A1)`.
7
+ const FORMULA_TRIGGERS = ['=', '+', '-', '@', '\t', '\r'];
8
+ // Strip that guard back off, so a re-imported value doesn't gain an apostrophe on every
9
+ // round trip. Only a lone apostrophe before a trigger is the guard; one before anything
10
+ // else is a character the value actually starts with, and is left alone.
11
+ function stripFormulaGuard(cell) {
12
+ if (cell.charAt(0) === '\'' && FORMULA_TRIGGERS.includes(cell.charAt(1))) {
13
+ return cell.slice(1);
14
+ }
15
+ return cell;
16
+ }
@@ -0,0 +1,14 @@
1
+ // The export writes with papaparse's escapeFormulae on (see serialize), which prefixes a
2
+ // cell starting with one of these with an apostrophe so a spreadsheet won't read it as a
3
+ // formula: `=SUM(A1)` is written as `'=SUM(A1)`.
4
+ const FORMULA_TRIGGERS = ['=', '+', '-', '@', '\t', '\r'];
5
+
6
+ // Strip that guard back off, so a re-imported value doesn't gain an apostrophe on every
7
+ // round trip. Only a lone apostrophe before a trigger is the guard; one before anything
8
+ // else is a character the value actually starts with, and is left alone.
9
+ export function stripFormulaGuard(cell: string): string {
10
+ if (cell.charAt(0) === '\'' && FORMULA_TRIGGERS.includes(cell.charAt(1))) {
11
+ return cell.slice(1);
12
+ }
13
+ return cell;
14
+ }
@@ -3,8 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.serialize = exports.parse = void 0;
6
+ exports.stripFormulaGuard = exports.serialize = exports.parse = void 0;
7
7
  var parse_1 = require("./parse");
8
8
  Object.defineProperty(exports, "parse", { enumerable: true, get: function () { return __importDefault(parse_1).default; } });
9
9
  var serialize_1 = require("./serialize");
10
10
  Object.defineProperty(exports, "serialize", { enumerable: true, get: function () { return __importDefault(serialize_1).default; } });
11
+ var formula_1 = require("./formula");
12
+ Object.defineProperty(exports, "stripFormulaGuard", { enumerable: true, get: function () { return formula_1.stripFormulaGuard; } });
@@ -1,2 +1,3 @@
1
1
  export {default as parse, type Row} from './parse';
2
2
  export {default as serialize} from './serialize';
3
+ export {stripFormulaGuard} from './formula';
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = buildCompletionEmail;
4
4
  const csv_1 = require("../csv");
5
+ const csv_2 = require("@tryghost/custom-field-types/csv");
5
6
  const emailTemplate = require('./email-template');
6
7
  // Turn an ORM validation error into copy a member manager can act on. Presentation
7
8
  // only: the API response keeps the raw messages, and just the emailed report is
@@ -17,6 +18,14 @@ function humaniseError(message) {
17
18
  function stringifyLabels(labels) {
18
19
  return labels.map(label => (typeof label === 'string' ? label : label.name)).join(',');
19
20
  }
21
+ // The custom_fields.* cells a submitted row carried, echoed untouched so a manager can
22
+ // fix a failed row and re-upload the values they mapped.
23
+ function customFieldCells(row) {
24
+ return Object.fromEntries(Object.entries(row).filter(([column]) => (0, csv_2.isCustomFieldColumn)(column)));
25
+ }
26
+ // Shape a failed import row into its fixed error-report cells, with the raw ORM message
27
+ // rewritten into copy the member manager can act on. Custom field cells are merged on by
28
+ // buildErrorReport, which owns the dynamic column set.
20
29
  function toErrorReportRow(row) {
21
30
  return {
22
31
  id: row.id,
@@ -34,10 +43,20 @@ function toErrorReportRow(row) {
34
43
  error: humaniseError(row.error)
35
44
  };
36
45
  }
37
- // The error report attached to the completion email: the failed rows as CSV. No column
38
- // list is passed -- the typed ErrorReportRow defines the columns.
46
+ // The error report attached to the completion email: the failed rows as CSV. It shares
47
+ // the serialiser with the export but not the shaping -- the export writes db members,
48
+ // this echoes submitted rows. Member columns come from the shaper's keys (so the type
49
+ // stays the single source); the custom_fields.* columns across the rows are threaded in
50
+ // before the last error column, and each row's custom cells merged on.
39
51
  function buildErrorReport(errors) {
40
- return (0, csv_1.serialize)(errors.map(toErrorReportRow));
52
+ if (errors.length === 0) {
53
+ return (0, csv_1.serialize)([]);
54
+ }
55
+ const memberColumns = Object.keys(toErrorReportRow(errors[0])).filter(column => column !== 'error');
56
+ const customColumns = [...new Set(errors.flatMap(row => Object.keys(customFieldCells(row))))];
57
+ const columns = [...memberColumns, ...customColumns, 'error'];
58
+ const rows = errors.map(row => ({ ...toErrorReportRow(row), ...customFieldCells(row) }));
59
+ return (0, csv_1.serialize)(rows, { columns });
41
60
  }
42
61
  // Compose the completion email for a finished import: the summary and its links,
43
62
  // plus the attached error report. Owns how the outcome is presented, so the
@@ -1,4 +1,5 @@
1
1
  import {serialize} from '../csv';
2
+ import {isCustomFieldColumn} from '@tryghost/custom-field-types/csv';
2
3
  import type {MemberImportRow, ImportErrorRow, ImportLabel, Label} from './row';
3
4
 
4
5
  const emailTemplate = require('./email-template');
@@ -43,10 +44,12 @@ function humaniseError(message: string): string {
43
44
  .replace(/No such customer:[^,]*/, 'Could not find Stripe customer');
44
45
  }
45
46
 
46
- // One row of the attached error report, in emit order. papaparse takes the columns from
47
- // these keys, so the report cannot drift from the shaper -- a new column must be added
48
- // here or the shaper stops compiling. tiers and deleted_at are export-vocabulary columns
49
- // an import never fills; kept always-empty so the report matches the members export CSV.
47
+ // One row of the fixed, member-vocabulary part of the error report, in emit order.
48
+ // papaparse takes the fixed columns from these keys, so the report cannot drift from the
49
+ // shaper -- a new member column must be added here or the shaper stops compiling. tiers
50
+ // and deleted_at are export-vocabulary columns an import never fills; kept always-empty
51
+ // so the report matches the members export CSV. Any custom_fields.* columns a submitted
52
+ // row carried are dynamic, so they are threaded in separately by buildErrorReport.
50
53
  type ErrorReportRow = {
51
54
  id: MemberImportRow['id'];
52
55
  email: MemberImportRow['email'];
@@ -67,6 +70,15 @@ function stringifyLabels(labels: Array<string | Label>): string {
67
70
  return labels.map(label => (typeof label === 'string' ? label : label.name)).join(',');
68
71
  }
69
72
 
73
+ // The custom_fields.* cells a submitted row carried, echoed untouched so a manager can
74
+ // fix a failed row and re-upload the values they mapped.
75
+ function customFieldCells(row: ImportErrorRow): Record<string, unknown> {
76
+ return Object.fromEntries(Object.entries(row).filter(([column]) => isCustomFieldColumn(column)));
77
+ }
78
+
79
+ // Shape a failed import row into its fixed error-report cells, with the raw ORM message
80
+ // rewritten into copy the member manager can act on. Custom field cells are merged on by
81
+ // buildErrorReport, which owns the dynamic column set.
70
82
  function toErrorReportRow(row: ImportErrorRow): ErrorReportRow {
71
83
  return {
72
84
  id: row.id,
@@ -85,10 +97,20 @@ function toErrorReportRow(row: ImportErrorRow): ErrorReportRow {
85
97
  };
86
98
  }
87
99
 
88
- // The error report attached to the completion email: the failed rows as CSV. No column
89
- // list is passed -- the typed ErrorReportRow defines the columns.
100
+ // The error report attached to the completion email: the failed rows as CSV. It shares
101
+ // the serialiser with the export but not the shaping -- the export writes db members,
102
+ // this echoes submitted rows. Member columns come from the shaper's keys (so the type
103
+ // stays the single source); the custom_fields.* columns across the rows are threaded in
104
+ // before the last error column, and each row's custom cells merged on.
90
105
  function buildErrorReport(errors: ImportErrorRow[]): string {
91
- return serialize(errors.map(toErrorReportRow));
106
+ if (errors.length === 0) {
107
+ return serialize([]);
108
+ }
109
+ const memberColumns = Object.keys(toErrorReportRow(errors[0])).filter(column => column !== 'error');
110
+ const customColumns = [...new Set(errors.flatMap(row => Object.keys(customFieldCells(row))))];
111
+ const columns = [...memberColumns, ...customColumns, 'error'];
112
+ const rows = errors.map(row => ({...toErrorReportRow(row), ...customFieldCells(row)}));
113
+ return serialize(rows, {columns});
92
114
  }
93
115
 
94
116
  // Compose the completion email for a finished import: the summary and its links,
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const moment_timezone_1 = __importDefault(require("moment-timezone"));
7
7
  const completion_email_1 = __importDefault(require("./completion-email"));
8
+ const csv_1 = require("../csv");
9
+ const csv_2 = require("@tryghost/custom-field-types/csv");
8
10
  const metrics = require('@tryghost/metrics');
9
11
  const errors = require('@tryghost/errors');
10
12
  const tpl = require('@tryghost/tpl');
@@ -47,11 +49,12 @@ class MembersCSVImporter {
47
49
  _tiers;
48
50
  _stripe;
49
51
  _gifts;
52
+ _customFields;
50
53
  _email;
51
54
  _addJob;
52
55
  _getTimezone;
53
56
  _getInlineThreshold;
54
- constructor({ knex, readRows, spool, members, tiers, stripe, gifts, email, addJob, getTimezone, getInlineThreshold }) {
57
+ constructor({ knex, readRows, spool, members, tiers, stripe, gifts, customFields, email, addJob, getTimezone, getInlineThreshold }) {
55
58
  this._knex = knex;
56
59
  this._readRows = readRows;
57
60
  this._spool = spool;
@@ -59,6 +62,7 @@ class MembersCSVImporter {
59
62
  this._tiers = tiers;
60
63
  this._stripe = stripe;
61
64
  this._gifts = gifts;
65
+ this._customFields = customFields;
62
66
  this._email = email;
63
67
  this._addJob = addJob;
64
68
  this._getTimezone = getTimezone;
@@ -132,6 +136,9 @@ class MembersCSVImporter {
132
136
  const globalLabels = [importLabel, ...extraLabels];
133
137
  const performStart = Date.now();
134
138
  const defaultTier = await this._tiers.getDefault();
139
+ // The field set a custom_fields.* column is read against; empty when the feature
140
+ // is off, so a carried-through column is dropped and the write boundary stays shut.
141
+ const activeCustomFields = await this._customFields.activeFields();
135
142
  const tierIdCache = new Map();
136
143
  const archivableStripePriceIds = [];
137
144
  // Copied per row: the member model stamps ids and trims names onto these in
@@ -143,8 +150,7 @@ class MembersCSVImporter {
143
150
  let imported = 0;
144
151
  const importErrors = [];
145
152
  for (const row of rows) {
146
- const trx = await this._knex.transaction(undefined, { doNotRejectOnRollback: false });
147
- const options = { transacting: trx, context: IMPORT_CONTEXT };
153
+ let trx;
148
154
  try {
149
155
  if (row.gift_id) {
150
156
  if (row.import_tier) {
@@ -154,6 +160,15 @@ class MembersCSVImporter {
154
160
  throw wrapGiftError(new errors.DataImportError({ message: tpl(messages.giftCannotCombineWithComplimentary) }));
155
161
  }
156
162
  }
163
+ // Validate the row's custom field values before the transaction opens.
164
+ // planWrite only reads, and it throws on an invalid value to fail the row
165
+ // before any member write -- so there is no reason to hold a transaction
166
+ // across it, and doing so would deadlock the single-connection SQLite pool.
167
+ const customFieldPlan = activeCustomFields.length > 0
168
+ ? await this._customFields.planWrite((0, csv_2.fieldValuesFromCsvRow)(activeCustomFields, row, csv_1.stripFormulaGuard))
169
+ : [];
170
+ trx = await this._knex.transaction(undefined, { doNotRejectOnRollback: false });
171
+ const options = { transacting: trx, context: IMPORT_CONTEXT };
157
172
  const createdAt = (row.created_at && (0, moment_timezone_1.default)(row.created_at).isAfter((0, moment_timezone_1.default)())) ? (0, moment_timezone_1.default)().toDate() : row.created_at;
158
173
  const memberValues = {
159
174
  email: row.email,
@@ -243,6 +258,8 @@ class MembersCSVImporter {
243
258
  throw wrapGiftError(giftError);
244
259
  }
245
260
  }
261
+ // On the row's transaction, so the values commit or roll back with the member.
262
+ await this._customFields.applyWrite(member.id, customFieldPlan, trx);
246
263
  await trx.commit();
247
264
  imported += 1;
248
265
  }
@@ -251,7 +268,11 @@ class MembersCSVImporter {
251
268
  const errorMessage = errorList
252
269
  .map(e => (typeof e === 'object' && e !== null && 'message' in e ? e.message : undefined))
253
270
  .join(', ');
254
- await trx.rollback();
271
+ // trx is unset if the row failed before the transaction opened (a bad
272
+ // custom field value or gift combination).
273
+ if (trx) {
274
+ await trx.rollback();
275
+ }
255
276
  importErrors.push({ ...row, error: errorMessage });
256
277
  }
257
278
  }
@@ -1,5 +1,7 @@
1
1
  import moment from 'moment-timezone';
2
2
  import buildCompletionEmail from './completion-email';
3
+ import {stripFormulaGuard} from '../csv';
4
+ import {fieldValuesFromCsvRow, type CsvField} from '@tryghost/custom-field-types/csv';
3
5
  import type {Knex} from 'knex';
4
6
  import type {MemberImportRow, ImportErrorRow, ImportLabel, Label} from './row';
5
7
  import type {RowSpool, SpooledRows} from './spool';
@@ -80,7 +82,7 @@ interface StripeSubscriptions {
80
82
 
81
83
  // Gifts: reassigning a redeemed gift to the imported member.
82
84
  export interface GiftService {
83
- reassignRedeemer(giftId: string, memberId: string, options: object): Promise<void>;
85
+ reassignRedeemer(giftId: string, memberId: string, options: {transacting?: Knex.Transaction}): Promise<void>;
84
86
  }
85
87
 
86
88
  // The completion email concern: who it goes to, the links it carries, and sending
@@ -91,6 +93,20 @@ export interface EmailNotifications {
91
93
  urlFor(type: string, data: unknown, absolute: boolean): string;
92
94
  }
93
95
 
96
+ // Opaque to the import: planWrite produces it, applyWrite consumes it.
97
+ type CustomFieldPlan = unknown;
98
+
99
+ // The custom fields collaborator as the import needs it. activeFields is the field set a
100
+ // custom_fields.* column is read against, empty when the feature is off; planWrite
101
+ // validates a row's values (throwing so the row fails whole) and applyWrite persists
102
+ // them, merging a composite's sub-fields into whatever is already stored, both on the
103
+ // row's transaction.
104
+ export interface CustomFieldsImport {
105
+ activeFields(): Promise<CsvField[]>;
106
+ planWrite(values: Record<string, unknown>): Promise<CustomFieldPlan[]>;
107
+ applyWrite(memberId: string, plan: CustomFieldPlan[], executor: Knex): Promise<void>;
108
+ }
109
+
94
110
  // The collaborators the import depends on, one per concern.
95
111
  interface ImporterDeps {
96
112
  knex: Knex;
@@ -100,6 +116,7 @@ interface ImporterDeps {
100
116
  tiers: TiersRepository;
101
117
  stripe: StripeSubscriptions;
102
118
  gifts: GiftService;
119
+ customFields: CustomFieldsImport;
103
120
  email: EmailNotifications;
104
121
  addJob: (job: {job: () => Promise<void>; offloaded: boolean; name: string}) => void;
105
122
  getTimezone: () => string;
@@ -183,12 +200,13 @@ class MembersCSVImporter {
183
200
  private _tiers: TiersRepository;
184
201
  private _stripe: StripeSubscriptions;
185
202
  private _gifts: GiftService;
203
+ private _customFields: CustomFieldsImport;
186
204
  private _email: EmailNotifications;
187
205
  private _addJob: (job: {job: () => Promise<void>; offloaded: boolean; name: string}) => void;
188
206
  private _getTimezone: () => string;
189
207
  private _getInlineThreshold: () => number;
190
208
 
191
- constructor({knex, readRows, spool, members, tiers, stripe, gifts, email, addJob, getTimezone, getInlineThreshold}: ImporterDeps) {
209
+ constructor({knex, readRows, spool, members, tiers, stripe, gifts, customFields, email, addJob, getTimezone, getInlineThreshold}: ImporterDeps) {
192
210
  this._knex = knex;
193
211
  this._readRows = readRows;
194
212
  this._spool = spool;
@@ -196,6 +214,7 @@ class MembersCSVImporter {
196
214
  this._tiers = tiers;
197
215
  this._stripe = stripe;
198
216
  this._gifts = gifts;
217
+ this._customFields = customFields;
199
218
  this._email = email;
200
219
  this._addJob = addJob;
201
220
  this._getTimezone = getTimezone;
@@ -283,6 +302,9 @@ class MembersCSVImporter {
283
302
  const globalLabels: Label[] = [importLabel, ...extraLabels];
284
303
  const performStart = Date.now();
285
304
  const defaultTier = await this._tiers.getDefault();
305
+ // The field set a custom_fields.* column is read against; empty when the feature
306
+ // is off, so a carried-through column is dropped and the write boundary stays shut.
307
+ const activeCustomFields = await this._customFields.activeFields();
286
308
  const tierIdCache = new Map();
287
309
  const archivableStripePriceIds: string[] = [];
288
310
  // Copied per row: the member model stamps ids and trims names onto these in
@@ -295,8 +317,7 @@ class MembersCSVImporter {
295
317
  let imported = 0;
296
318
  const importErrors: ImportErrorRow[] = [];
297
319
  for (const row of rows) {
298
- const trx = await this._knex.transaction(undefined, {doNotRejectOnRollback: false});
299
- const options = {transacting: trx, context: IMPORT_CONTEXT};
320
+ let trx: Knex.Transaction | undefined;
300
321
  try {
301
322
  if (row.gift_id) {
302
323
  if (row.import_tier) {
@@ -307,6 +328,17 @@ class MembersCSVImporter {
307
328
  }
308
329
  }
309
330
 
331
+ // Validate the row's custom field values before the transaction opens.
332
+ // planWrite only reads, and it throws on an invalid value to fail the row
333
+ // before any member write -- so there is no reason to hold a transaction
334
+ // across it, and doing so would deadlock the single-connection SQLite pool.
335
+ const customFieldPlan = activeCustomFields.length > 0
336
+ ? await this._customFields.planWrite(fieldValuesFromCsvRow(activeCustomFields, row, stripFormulaGuard))
337
+ : [];
338
+
339
+ trx = await this._knex.transaction(undefined, {doNotRejectOnRollback: false});
340
+ const options = {transacting: trx, context: IMPORT_CONTEXT};
341
+
310
342
  const createdAt = (row.created_at && moment(row.created_at).isAfter(moment())) ? moment().toDate() : row.created_at;
311
343
  const memberValues: MemberImportValues = {
312
344
  email: row.email,
@@ -394,6 +426,9 @@ class MembersCSVImporter {
394
426
  }
395
427
  }
396
428
 
429
+ // On the row's transaction, so the values commit or roll back with the member.
430
+ await this._customFields.applyWrite(member.id, customFieldPlan, trx);
431
+
397
432
  await trx.commit();
398
433
  imported += 1;
399
434
  } catch (error) {
@@ -401,7 +436,11 @@ class MembersCSVImporter {
401
436
  const errorMessage = errorList
402
437
  .map(e => (typeof e === 'object' && e !== null && 'message' in e ? e.message : undefined))
403
438
  .join(', ');
404
- await trx.rollback();
439
+ // trx is unset if the row failed before the transaction opened (a bad
440
+ // custom field value or gift combination).
441
+ if (trx) {
442
+ await trx.rollback();
443
+ }
405
444
  importErrors.push({...row, error: errorMessage});
406
445
  }
407
446
  }
@@ -40,7 +40,23 @@ function makeImporter(deps) {
40
40
  // Gifts is initialised at boot and always present at request time; the getter
41
41
  // resolves it lazily so the ready service is picked up whenever a row uses it.
42
42
  const gifts = {
43
- reassignRedeemer: (giftId, memberId, options) => deps.getGiftService().reassignRedeemer(giftId, memberId, options)
43
+ reassignRedeemer: (giftId, memberId, options) => deps.getGiftService().reassignRedeemer({
44
+ giftId,
45
+ memberId,
46
+ transacting: options.transacting
47
+ })
48
+ };
49
+ // Gated by the same labs flag as the export, so the two halves round-trip or stay
50
+ // silent together: off, activeFields resolves empty and every custom_fields.* column
51
+ // is dropped.
52
+ const customFields = {
53
+ activeFields: async () => (labs.isSet('membersCustomFields') ? deps.customFields.definitions.browse() : []),
54
+ planWrite: values => deps.customFields.values.planWrite(values),
55
+ // mergeComposites: a sub-field is a field, so a row writes the sub-fields it fills
56
+ // and leaves the rest alone, the same rule a blank `name` column follows. Without
57
+ // it a file naming one address column would replace the whole address, clearing
58
+ // five sub-fields it never mentioned.
59
+ applyWrite: (memberId, plan, executor) => deps.customFields.values.applyWrite(memberId, plan, executor, { mergeComposites: true })
44
60
  };
45
61
  return new importer_1.default({
46
62
  knex: deps.knex,
@@ -56,6 +72,7 @@ function makeImporter(deps) {
56
72
  productRepository: deps.productRepository
57
73
  }),
58
74
  gifts,
75
+ customFields,
59
76
  email,
60
77
  addJob: deps.addJob,
61
78
  getTimezone: deps.getTimezone,
@@ -1,5 +1,6 @@
1
1
  import type {Knex} from 'knex';
2
- import MembersCSVImporter, {type MembersRepository, type GiftService, type EmailNotifications, type Tier} from './import/importer';
2
+ import type {CsvField} from '@tryghost/custom-field-types/csv';
3
+ import MembersCSVImporter, {type MembersRepository, type GiftService, type EmailNotifications, type Tier, type CustomFieldsImport} from './import/importer';
3
4
  import readMemberRows from './import/reader';
4
5
  import {createRowSpool} from './import/spool';
5
6
  import MembersCSVExporter, {type ExportOptions, type CustomFieldDefinition} from './export/exporter';
@@ -17,7 +18,9 @@ interface ImporterServices {
17
18
  getMembersRepository(): Promise<Omit<MembersRepository, 'getImportLabel'>>;
18
19
  getDefaultTier(): Promise<Tier>;
19
20
  getTierByName(name: string): Promise<Tier | null>;
20
- getGiftService(): GiftService;
21
+ getGiftService(): {
22
+ reassignRedeemer(input: {giftId: string; memberId: string; transacting?: Knex.Transaction}): Promise<void>;
23
+ };
21
24
  sendEmail: EmailNotifications['send'];
22
25
  urlFor: EmailNotifications['urlFor'];
23
26
  addJob(job: {job: () => Promise<void>; offloaded: boolean; name: string}): void;
@@ -25,6 +28,14 @@ interface ImporterServices {
25
28
  getInlineThreshold(): number;
26
29
  stripeAPIService: unknown;
27
30
  productRepository: unknown;
31
+ // The custom fields services the members service hands the import composition root.
32
+ customFields: {
33
+ definitions: {browse(): Promise<CsvField[]>};
34
+ values: {
35
+ planWrite(values: Record<string, unknown>): Promise<unknown[]>;
36
+ applyWrite(memberId: string, plan: unknown[], executor: Knex, options?: {mergeComposites?: boolean}): Promise<void>;
37
+ };
38
+ };
28
39
  }
29
40
 
30
41
  // The custom fields services the members service hands the export composition root.
@@ -63,7 +74,24 @@ export function makeImporter(deps: ImporterServices) {
63
74
  // Gifts is initialised at boot and always present at request time; the getter
64
75
  // resolves it lazily so the ready service is picked up whenever a row uses it.
65
76
  const gifts: GiftService = {
66
- reassignRedeemer: (giftId, memberId, options) => deps.getGiftService().reassignRedeemer(giftId, memberId, options)
77
+ reassignRedeemer: (giftId, memberId, options) => deps.getGiftService().reassignRedeemer({
78
+ giftId,
79
+ memberId,
80
+ transacting: options.transacting
81
+ })
82
+ };
83
+
84
+ // Gated by the same labs flag as the export, so the two halves round-trip or stay
85
+ // silent together: off, activeFields resolves empty and every custom_fields.* column
86
+ // is dropped.
87
+ const customFields: CustomFieldsImport = {
88
+ activeFields: async () => (labs.isSet('membersCustomFields') ? deps.customFields.definitions.browse() : []),
89
+ planWrite: values => deps.customFields.values.planWrite(values),
90
+ // mergeComposites: a sub-field is a field, so a row writes the sub-fields it fills
91
+ // and leaves the rest alone, the same rule a blank `name` column follows. Without
92
+ // it a file naming one address column would replace the whole address, clearing
93
+ // five sub-fields it never mentioned.
94
+ applyWrite: (memberId, plan, executor) => deps.customFields.values.applyWrite(memberId, plan, executor, {mergeComposites: true})
67
95
  };
68
96
 
69
97
  return new MembersCSVImporter({
@@ -80,6 +108,7 @@ export function makeImporter(deps: ImporterServices) {
80
108
  productRepository: deps.productRepository
81
109
  }),
82
110
  gifts,
111
+ customFields,
83
112
  email,
84
113
  addJob: deps.addJob,
85
114
  getTimezone: deps.getTimezone,