ghost 6.56.0 → 6.57.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 (385) hide show
  1. package/bin/validate-adapters.js +74 -0
  2. package/bin/validate-adapters.ts +87 -0
  3. package/bin/warm-compile-cache.sh +60 -0
  4. package/components/tryghost-custom-field-types-0.0.0.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-parse-email-address-0.0.0.tgz +0 -0
  8. package/core/app.js +1 -1
  9. package/core/boot.js +21 -25
  10. package/core/bridge.js +10 -12
  11. package/core/built/admin/assets/Combination-BDvCdg1t.js +41 -0
  12. package/core/built/admin/assets/about-C7O-AgRZ.js +1 -0
  13. package/core/built/admin/assets/{account-migration-D3O_yVck.js → account-migration-Cv_ol1ko.js} +1 -1
  14. package/core/built/admin/assets/action-list-lGzZLHc8.js +1 -0
  15. package/core/built/admin/assets/activitypub/{account-migration-D8FbP-Z-.js → account-migration-DHFbePl6.js} +7 -7
  16. package/core/built/admin/assets/activitypub/activitypub.js +3 -3
  17. package/core/built/admin/assets/activitypub/{avatar-11EA1VSs.js → avatar-CT4tS2_S.js} +2 -2
  18. package/core/built/admin/assets/activitypub/{avatar-flipboard-jfbLG-xe.js → avatar-flipboard-ByYWKipo.js} +2 -2
  19. package/core/built/admin/assets/activitypub/{bluesky-sharing-f1jTQe2P.js → bluesky-sharing-srVnSHDz.js} +8 -8
  20. package/core/built/admin/assets/activitypub/{content-formatters-nmTOPY9M.js → content-formatters-C2sos5Ht.js} +3 -3
  21. package/core/built/admin/assets/activitypub/{copy-BCwo9ep3.js → copy-B1UBTOoZ.js} +2 -2
  22. package/core/built/admin/assets/activitypub/{dist-BEfX-0aW.js → dist-MPsb2WZ1.js} +4 -4
  23. package/core/built/admin/assets/activitypub/{domain-AaDlZlrA.js → domain-DzcM9Dzo.js} +9 -9
  24. package/core/built/admin/assets/activitypub/{edit-profile-DPi8Tl5M.js → edit-profile-CsQ_4xAk.js} +5 -5
  25. package/core/built/admin/assets/activitypub/{explore-CxTzjILQ.js → explore-CJe0DZBK.js} +8 -8
  26. package/core/built/admin/assets/activitypub/{feed-DPv5x2MX.js → feed-CKV3qWON.js} +9 -9
  27. package/core/built/admin/assets/activitypub/{field-DbgA8lat.js → field-DckHfDJf.js} +3 -3
  28. package/core/built/admin/assets/activitypub/{hash-BWuZoYkA.js → hash-Dlp8RM55.js} +2 -2
  29. package/core/built/admin/assets/activitypub/{inbox-35VDQ2K2.js → inbox-CsPy4R0M.js} +2 -2
  30. package/core/built/admin/assets/activitypub/{inbox-DSrF_i1O.js → inbox-DAsjzUUe.js} +10 -10
  31. package/core/built/admin/assets/activitypub/{label-CPjFBbo7.js → label-DWyo9E5T.js} +3 -3
  32. package/core/built/admin/assets/activitypub/layout-DM_3pqsU.js +1 -0
  33. package/core/built/admin/assets/activitypub/{moderation-BkDC1XVZ.js → moderation-B0Inc03M.js} +7 -7
  34. package/core/built/admin/assets/activitypub/{note-xEyH4xT-.js → note-CSE54lER.js} +7 -7
  35. package/core/built/admin/assets/activitypub/{notifications-DClaL3ui.js → notifications-RSwbKqdh.js} +9 -9
  36. package/core/built/admin/assets/activitypub/{preferences-DpnOa-4Z.js → preferences-CE231n2_.js} +11 -11
  37. package/core/built/admin/assets/activitypub/{profile-Dvi9mz-6.js → profile-aaQ1j1Eo.js} +13 -13
  38. package/core/built/admin/assets/activitypub/{reply-Cs9ySd6Z.js → reply-Cjb0Vs3m.js} +2 -2
  39. package/core/built/admin/assets/activitypub/{routes-DFBeTnfn.js → routes-Djb0mc9k.js} +26 -26
  40. package/core/built/admin/assets/activitypub/{separator-C5u_VjzY.js → separator-D8pROI5n.js} +3 -3
  41. package/core/built/admin/assets/activitypub/{settings-CV2fFu6D.js → settings-DMc32IQl.js} +5 -5
  42. package/core/built/admin/assets/activitypub/{site-_TgDd_EM.js → site-DVIcsohX.js} +4 -4
  43. package/core/built/admin/assets/activitypub/{step-1-BfnNL2oF.js → step-1-9Yey9usF.js} +5 -5
  44. package/core/built/admin/assets/activitypub/{step-2-DLndZkO-.js → step-2-DdYmKRXV.js} +6 -6
  45. package/core/built/admin/assets/activitypub/{step-3-A-PZTtRE.js → step-3-hdSkcjZU.js} +10 -10
  46. package/core/built/admin/assets/activitypub/{tabs-BUbz7tea.js → tabs-CUqseYzQ.js} +6 -6
  47. package/core/built/admin/assets/activitypub/{topic-filter-BdX_xYYT.js → topic-filter-Coni6OXM.js} +3 -3
  48. package/core/built/admin/assets/activitypub/{use-activity-pub-queries-EwuRemHh.js → use-activity-pub-queries-MeHTPMQv.js} +2 -2
  49. package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-cy4ACBc4.js → use-navigate-with-base-path-B046AM38.js} +24 -7
  50. package/core/built/admin/assets/activitypub/{use-reply-chain-data-CyFcGfan.js → use-reply-chain-data-1AO-Xob6.js} +5 -5
  51. package/core/built/admin/assets/activitypub/{x-7zk8mV2p.js → x-BRfRYd16.js} +2 -2
  52. package/core/built/admin/assets/add-integration-modal-m9Dv6f77.js +1 -0
  53. package/core/built/admin/assets/add-newsletter-modal-CPSVZL2H.js +1 -0
  54. package/core/built/admin/assets/add-recommendation-modal-Ci9Z5yat.js +1 -0
  55. package/core/built/admin/assets/analytics-DqSCVV8U.js +1 -0
  56. package/core/built/admin/assets/announcement-bar-modal-DU3avV7d.js +1 -0
  57. package/core/built/admin/assets/api-keys-BK0LylyK.js +1 -0
  58. package/core/built/admin/assets/api-keys-_iCT4dxr.js +1 -0
  59. package/core/built/admin/assets/{audience-CkBb2LlO.js → audience-DTJtXXWY.js} +1 -1
  60. package/core/built/admin/assets/automated-emails-PayPCT8L.js +1 -0
  61. package/core/built/admin/assets/{automations-DhKeS_U5.js → automations-162q5HjL.js} +1 -1
  62. package/core/built/admin/assets/{automations-cMllU545.js → automations-dtQ_MRZ7.js} +1 -1
  63. package/core/built/admin/assets/{bluesky-sharing-DPGpyD-G.js → bluesky-sharing-B8Es3AuV.js} +1 -1
  64. package/core/built/admin/assets/brand-icon-09YqFZjV.js +1 -0
  65. package/core/built/admin/assets/{breadcrumb-DIfcF5ow.js → breadcrumb-tg5zL--z.js} +1 -1
  66. package/core/built/admin/assets/{chart-helpers-DV0FkdcD.js → chart-helpers-CZElTGYL.js} +1 -1
  67. package/core/built/admin/assets/checkbox-Cyo-x7mC.js +1 -0
  68. package/core/built/admin/assets/{chunk.524.87b52f4fada529df2c4c.js → chunk.524.cd5498dd4e195f679266.js} +4 -4
  69. package/core/built/admin/assets/{chunk.582.340bcca873e806cbd4bb.js → chunk.582.6f92ec1f03734bf2ea3f.js} +6 -6
  70. package/core/built/admin/assets/{chunk.332.05991a101d67f732dec3.js → chunk.873.b8fc05a8ab877fb819ed.js} +28 -27
  71. package/core/built/admin/assets/code-editor-view-Dq1rQ4tS.js +1 -0
  72. package/core/built/admin/assets/color-picker-field-BonBgxhU.js +1 -0
  73. package/core/built/admin/assets/color-utils-BsrwXuDz.js +1 -0
  74. package/core/built/admin/assets/combobox-uX5r_paI.js +1 -0
  75. package/core/built/admin/assets/command-DxwBO2pF.js +1 -0
  76. package/core/built/admin/assets/comments-Dn-RO-nU.js +1 -0
  77. package/core/built/admin/assets/components-CwL6a6As-B0E0-NBJ.js +1 -0
  78. package/core/built/admin/assets/confirmation-modal-B6D6mk6R.js +1 -0
  79. package/core/built/admin/assets/content-api-modal-BxQ-7T6u.js +1 -0
  80. package/core/built/admin/assets/copy-field-BAxL4Zqn.js +1 -0
  81. package/core/built/admin/assets/currency-Dy4Tc2R2.js +1 -0
  82. package/core/built/admin/assets/custom-integration-modal---d_l7lc.js +1 -0
  83. package/core/built/admin/assets/{date-range-select-dXc-GsVe.js → date-range-select-PB2n1HZf.js} +1 -1
  84. package/core/built/admin/assets/debounce-Dg2fW5Rg.js +3 -0
  85. package/core/built/admin/assets/design-and-theme-modal-DYRT207a.js +23 -0
  86. package/core/built/admin/assets/dirty-confirm-dialog-BnDJ39ao.js +1 -0
  87. package/core/built/admin/assets/disabled-sources-indicator-iMHrkyV3.js +1 -0
  88. package/core/built/admin/assets/dist-B2PvRwLn.js +1 -0
  89. package/core/built/admin/assets/{dist-pHiOxyxa.js → dist-D5vhMdXf.js} +1 -1
  90. package/core/built/admin/assets/dist-DlCyLrOC.js +1 -0
  91. package/core/built/admin/assets/dist-WeOIvSYM.js +23 -0
  92. package/core/built/admin/assets/{dist-DyViJwIy.js → dist-cFzKLG7y.js} +3 -3
  93. package/core/built/admin/assets/dist-j1xxxnY8.js +1 -0
  94. package/core/built/admin/assets/dist-myQaY0ka.js +1 -0
  95. package/core/built/admin/assets/{domain-B5ogXUBa.js → domain-BR8FIdjD.js} +1 -1
  96. package/core/built/admin/assets/{dropzone-BnFG7dDP.js → dropzone-hNxmUvwD.js} +1 -1
  97. package/core/built/admin/assets/{edit-profile-B07jCZy9.js → edit-profile-CENrkY0l.js} +1 -1
  98. package/core/built/admin/assets/editor-B18D0lm8.js +8 -0
  99. package/core/built/admin/assets/embed-signup-form-modal-BsCYUMvQ.js +9 -0
  100. package/core/built/admin/assets/{explore-Ca1y8HW_.js → explore-BmrnljWw.js} +1 -1
  101. package/core/built/admin/assets/{feed-x4iFMBP4.js → feed-Dy4li6i8.js} +1 -1
  102. package/core/built/admin/assets/{field-ChCAabZg.js → field-Dh_lwk13.js} +1 -1
  103. package/core/built/admin/assets/{filter-sources-LdtH1e-T.js → filter-sources-D2zDuJH5.js} +1 -1
  104. package/core/built/admin/assets/filters-3JBQcNtJ.js +1 -0
  105. package/core/built/admin/assets/{filters-CjVKRP8U.js → filters-CFt_BDe4.js} +1 -1
  106. package/core/built/admin/assets/first-promoter-modal-D6Rjxugw.js +1 -0
  107. package/core/built/admin/assets/format-url-C-t8G6Qd.js +1 -0
  108. package/core/built/admin/assets/{ghost-7f01131115186d1af84ead76e79646dd.js → ghost-d683fbeb945b13155ea669caa5d8db41.js} +300 -298
  109. package/core/built/admin/assets/{gift-link-modal-Buqh2vXh.js → gift-link-modal-zMtlJhLQ.js} +1 -1
  110. package/core/built/admin/assets/{gift-links-CsUhefWG.js → gift-links-CVuqLhsZ.js} +1 -1
  111. package/core/built/admin/assets/global-data-provider-B949LDXJ.js +1 -0
  112. package/core/built/admin/assets/grid-CUciBfjf.js +1 -0
  113. package/core/built/admin/assets/{growth-Dy2UOSay.js → growth-C38Xtp8D.js} +2 -2
  114. package/core/built/admin/assets/{growth-DajOBdN0.js → growth-CFws67_i.js} +1 -1
  115. package/core/built/admin/assets/history-modal-ILAOkbxH.js +1 -0
  116. package/core/built/admin/assets/{hooks-CixO01lL.js → hooks-D6EE7BIt.js} +2 -2
  117. package/core/built/admin/assets/html-field-9a9Id_2q.js +1 -0
  118. package/core/built/admin/assets/iframe-buffering-e8vH4-Qn.js +1 -0
  119. package/core/built/admin/assets/images-CrQKVc9Q.js +1 -0
  120. package/core/built/admin/assets/{inbox-BUOLDoji.js → inbox-BrSRzyvz.js} +13 -13
  121. package/core/built/admin/assets/index-BHy2DZX-.js +67 -0
  122. package/core/built/admin/assets/index-Ds2iUoF6.css +1 -0
  123. package/core/built/admin/assets/index.es-B9tvf-t4.js +1 -0
  124. package/core/built/admin/assets/{input-group-uSK3XT7b.js → input-group-6vEPvX9S.js} +1 -1
  125. package/core/built/admin/assets/integration-header-B4DyXV06.js +1 -0
  126. package/core/built/admin/assets/integrations-BT6_iJvu.js +1 -0
  127. package/core/built/admin/assets/invite-user-modal-5xDCEw90.js +1 -0
  128. package/core/built/admin/assets/isEmail-BOfC647y.js +1 -0
  129. package/core/built/admin/assets/koenig-editor-base-BWLEB9Vf.js +1 -0
  130. package/core/built/admin/assets/{koenig-lexical-BFc4yFed.js → koenig-lexical-Cj62r_y9.js} +1 -1
  131. package/core/built/admin/assets/{kpi-card-BN-E6WYX.js → kpi-card-DOUnkZcN.js} +1 -1
  132. package/core/built/admin/assets/{kpi-card-CCt-qeWD.js → kpi-card-IHDo8Q56.js} +1 -1
  133. package/core/built/admin/assets/{kpi-helpers-DlxTbTs4.js → kpi-helpers-CjUdECAK.js} +1 -1
  134. package/core/built/admin/assets/{kpi-tabs-BlcUttpn.js → kpi-tabs-X3DkOYbI.js} +1 -1
  135. package/core/built/admin/assets/{label-AVrxwmsz.js → label-Dq6QaOuS.js} +1 -1
  136. package/core/built/admin/assets/label-picker-8YlhS2U3.js +1 -0
  137. package/core/built/admin/assets/{labels-DAIqKed7.js → labels-DAPjKPy2.js} +1 -1
  138. package/core/built/admin/assets/layout-TMj4P-vv.js +1 -0
  139. package/core/built/admin/assets/limit-modal-BRPsit_B.js +1 -0
  140. package/core/built/admin/assets/{link-helpers-D9LsDOkG.js → link-helpers-Bzkk68GD.js} +1 -1
  141. package/core/built/admin/assets/member-custom-fields-CmvsuuV_.js +1 -0
  142. package/core/built/admin/assets/member-detail-DCvRrkmJ.js +1 -0
  143. package/core/built/admin/assets/members-DxbSKngD.js +11 -0
  144. package/core/built/admin/assets/modal-page-BMTzFHVO.js +1 -0
  145. package/core/built/admin/assets/{moderation-CQ3urTFE.js → moderation-Da5lhhWh.js} +1 -1
  146. package/core/built/admin/assets/multi-select-combobox-CYdkH7KQ.js +1 -0
  147. package/core/built/admin/assets/navigation-modal-BIoTwtMy.js +1 -0
  148. package/core/built/admin/assets/newsletter-BptlVoJm.js +1 -0
  149. package/core/built/admin/assets/newsletter-detail-modal-BIvxqE0N.js +1 -0
  150. package/core/built/admin/assets/newsletter-emails-Bf8IZLsP.js +1 -0
  151. package/core/built/admin/assets/{newsletters-vt3Ni1Ov.js → newsletters-BgrIZk0V.js} +1 -1
  152. package/core/built/admin/assets/{newsletters-BXJbJgA-.js → newsletters-Du0lzskS.js} +1 -1
  153. package/core/built/admin/assets/{note-Dk877BTv.js → note-DYcLOmDb.js} +1 -1
  154. package/core/built/admin/assets/{notifications-CPlLjPxO.js → notifications-B2SPFIV9.js} +1 -1
  155. package/core/built/admin/assets/{offers-DEniVRD5.js → offers-Um6ab71O.js} +1 -1
  156. package/core/built/admin/assets/offers-container-modal-BZnZITMw.js +1 -0
  157. package/core/built/admin/assets/{onboarding-route-DdStOtjy.js → onboarding-route-DgdfxkQh.js} +1 -1
  158. package/core/built/admin/assets/overview-BYQJXSog.js +1 -0
  159. package/core/built/admin/assets/overview-Dt2_mR6U.js +1 -0
  160. package/core/built/admin/assets/{page-header-pp4dWmOS.js → page-header-CpmxJj5B.js} +1 -1
  161. package/core/built/admin/assets/{pages-B4goRPUn.js → pages-BVMnpI2O.js} +1 -1
  162. package/core/built/admin/assets/pintura-modal-B4MuBuuO.js +1 -0
  163. package/core/built/admin/assets/portal-frame-BF0NBIji.js +1 -0
  164. package/core/built/admin/assets/portal-modal-DMVbm0oy.js +1 -0
  165. package/core/built/admin/assets/{post-analytics-BMJrI81R.js → post-analytics-CXWBOGyP.js} +1 -1
  166. package/core/built/admin/assets/post-analytics-header-ChzjXVPt.js +1 -0
  167. package/core/built/admin/assets/{post-analytics-provider-Btml5vJI.js → post-analytics-provider-Cgw4U_fw.js} +1 -1
  168. package/core/built/admin/assets/{post-share-modal-AcUZkbfL.js → post-share-modal-B9Vmk_XM.js} +1 -1
  169. package/core/built/admin/assets/{posts-DhXE7b6r.js → posts-CxG7v0-M.js} +1 -1
  170. package/core/built/admin/assets/preferences-BfzJmnsd.js +5 -0
  171. package/core/built/admin/assets/preview-chrome-BOVO6_PE.js +1 -0
  172. package/core/built/admin/assets/preview-modal-Dw_HtD6S.js +1 -0
  173. package/core/built/admin/assets/profile-BT5vtv9b.js +1 -0
  174. package/core/built/admin/assets/radio-group-I7ou7PoJ.js +1 -0
  175. package/core/built/admin/assets/{referrers-pmJT4k3-.js → referrers-B9QDAfYm.js} +1 -1
  176. package/core/built/admin/assets/{router-provider-BmEZf6c6.js → router-provider-DSQfprxH.js} +1 -1
  177. package/core/built/admin/assets/select-D9oU1IoM.js +1 -0
  178. package/core/built/admin/assets/settings-CfZFbyu_.js +4 -0
  179. package/core/built/admin/assets/{settings-BGgk7oCI.js → settings-CpvHWix7.js} +1 -1
  180. package/core/built/admin/assets/settings-breadcrumbs-K0USP2nw.js +1 -0
  181. package/core/built/admin/assets/{share-modal-BeuydaBf.js → share-modal-ClIyPzc5.js} +1 -1
  182. package/core/built/admin/assets/slack-modal-1XXqOfiw.js +1 -0
  183. package/core/built/admin/assets/social-urls-CHQ9cTAK.js +1 -0
  184. package/core/built/admin/assets/{sort-button-Df4t2AL8.js → sort-button-CdBsm40v.js} +1 -1
  185. package/core/built/admin/assets/{source-icon-ajoaKb41.js → source-icon-CEYmVLXR.js} +1 -1
  186. package/core/built/admin/assets/{stats-C_lfxZAw.js → stats-D8nZCTy5.js} +1 -1
  187. package/core/built/admin/assets/{stats-view-Cxot1iD8.js → stats-view-CugFVf0g.js} +1 -1
  188. package/core/built/admin/assets/{step-1-ogAA4lDr.js → step-1-DOYjrORn.js} +1 -1
  189. package/core/built/admin/assets/{step-2-C5Xini-7.js → step-2-vA6RsarH.js} +1 -1
  190. package/core/built/admin/assets/{step-3-CZv1FV81.js → step-3-BtIBxMId.js} +2 -2
  191. package/core/built/admin/assets/stripe-button-UFP4QRXd.js +1 -0
  192. package/core/built/admin/assets/stripe-connect-modal-C_L3uM6n.js +1 -0
  193. package/core/built/admin/assets/switch-CE2NuMn-.js +1 -0
  194. package/core/built/admin/assets/{table-DFj1wiYv.js → table-TMCgCQK7.js} +1 -1
  195. package/core/built/admin/assets/tabs-Dd6oOKbv.js +1 -0
  196. package/core/built/admin/assets/tag-detail-D3l-UJPR.js +1 -0
  197. package/core/built/admin/assets/tags-BwCgmr5v.js +1 -0
  198. package/core/built/admin/assets/tags-WdKQGWR8.js +1 -0
  199. package/core/built/admin/assets/testimonials-modal-CqWp-xXJ.js +1 -0
  200. package/core/built/admin/assets/tier-detail-modal-BkcR2u0A.js +1 -0
  201. package/core/built/admin/assets/{tiers-DYsa4DCo.js → tiers-Da6kxHQP.js} +1 -1
  202. package/core/built/admin/assets/toggle-group-Guorw93z.js +1 -0
  203. package/core/built/admin/assets/{topic-filter-DGDxDalo.js → topic-filter-CbgoWIHn.js} +1 -1
  204. package/core/built/admin/assets/transistor-modal-DuNnnpal.js +1 -0
  205. package/core/built/admin/assets/unsplash-modal-y2bVwTHH.js +1 -0
  206. package/core/built/admin/assets/{use-analytics-data-C76v4LIp.js → use-analytics-data-B-b4wNSC.js} +1 -1
  207. package/core/built/admin/assets/use-check-theme-limit-error-m09J38Mq.js +2 -0
  208. package/core/built/admin/assets/use-feature-flag-ud0Qz8vt.js +1 -0
  209. package/core/built/admin/assets/use-filter-params-BnbBDc7T.js +1 -0
  210. package/core/built/admin/assets/use-filterable-api-iMpDZBxD.js +1 -0
  211. package/core/built/admin/assets/use-form-vRjI0Kwk.js +1 -0
  212. package/core/built/admin/assets/{use-growth-stats-aNXJOmQr.js → use-growth-stats-DPU5rt3W.js} +1 -1
  213. package/core/built/admin/assets/use-hash-link-navigation-guard-B9j6HIrV.js +1 -0
  214. package/core/built/admin/assets/use-limiter-BkN3E4gw.js +2 -0
  215. package/core/built/admin/assets/use-pintura-config-C_z8HTG2.js +1 -0
  216. package/core/built/admin/assets/use-pintura-editor-DNgbkhGp.js +1 -0
  217. package/core/built/admin/assets/{use-post-referrers-CG0SZioj.js → use-post-referrers-C2mXyrp-.js} +1 -1
  218. package/core/built/admin/assets/use-query-params-DHhhu8wi.js +1 -0
  219. package/core/built/admin/assets/{use-reply-chain-data-RLdPKRmS.js → use-reply-chain-data-DaK8q2qQ.js} +1 -1
  220. package/core/built/admin/assets/use-setting-group-YlYcoNyP.js +1 -0
  221. package/core/built/admin/assets/use-settings-navigation-Bf1g1rEh.js +1 -0
  222. package/core/built/admin/assets/{use-simple-pagination-DgcF69KI.js → use-simple-pagination-D9ckKo5-.js} +1 -1
  223. package/core/built/admin/assets/{use-tinybird-query-C1R_Kbmy.js → use-tinybird-query-GSa32hVw.js} +1 -1
  224. package/core/built/admin/assets/{use-tinybird-token-C9fPjVM_.js → use-tinybird-token-CKLOZFsw.js} +1 -1
  225. package/core/built/admin/assets/use-url-input-z-p0Kewm.js +5 -0
  226. package/core/built/admin/assets/user-detail-modal-BMxOA4Sv.js +1 -0
  227. package/core/built/admin/assets/{users-Dkv8NQZe.js → users-B6hIds6n.js} +1 -1
  228. package/core/built/admin/assets/validator-Cr0sMKuW.js +7 -0
  229. package/core/built/admin/assets/{virtual-list-BmTUzeoF.js → virtual-list-RHeEL05L.js} +1 -1
  230. package/core/built/admin/assets/web-Bw2QMO5u.js +2 -0
  231. package/core/built/admin/assets/web-xJtcwMy4.js +1 -0
  232. package/core/built/admin/assets/zapier-modal-BP2f4-vY.js +1 -0
  233. package/core/built/admin/index.html +15 -13
  234. package/core/frontend/helpers/authors.js +1 -1
  235. package/core/frontend/helpers/link.js +3 -8
  236. package/core/frontend/helpers/tags.js +1 -1
  237. package/core/frontend/meta/author-url.js +2 -2
  238. package/core/frontend/meta/url.js +4 -4
  239. package/core/frontend/services/proxy.js +1 -1
  240. package/core/frontend/services/routing/events.js +20 -6
  241. package/core/frontend/services/routing/permalink-adapter.js +0 -13
  242. package/core/frontend/services/routing/permalink-adapter.ts +0 -13
  243. package/core/frontend/services/routing/router-manager.js +15 -9
  244. package/core/frontend/services/sitemap/base-site-map-generator.js +1 -28
  245. package/core/frontend/services/sitemap/site-map-manager.js +22 -57
  246. package/core/server/adapters/route-settings/utils.js +10 -2
  247. package/core/server/adapters/route-settings/utils.ts +12 -2
  248. package/core/server/api/endpoints/email-post.js +1 -1
  249. package/core/server/api/endpoints/previews.js +1 -1
  250. package/core/server/api/endpoints/search-index-public.js +3 -3
  251. package/core/server/api/endpoints/search-index.js +4 -4
  252. package/core/server/api/endpoints/utils/serializers/input/authors.js +2 -2
  253. package/core/server/api/endpoints/utils/serializers/input/comments.js +1 -1
  254. package/core/server/api/endpoints/utils/serializers/input/pages.js +3 -3
  255. package/core/server/api/endpoints/utils/serializers/input/posts.js +3 -3
  256. package/core/server/api/endpoints/utils/serializers/input/tags.js +1 -1
  257. package/core/server/api/endpoints/utils/serializers/input/users.js +2 -2
  258. package/core/server/api/endpoints/utils/serializers/input/utils/url.js +31 -13
  259. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +2 -2
  260. package/core/server/api/endpoints/utils/serializers/output/mappers/tags.js +2 -2
  261. package/core/server/api/endpoints/utils/serializers/output/mappers/users.js +8 -0
  262. package/core/server/api/endpoints/utils/serializers/output/utils/url.js +18 -11
  263. package/core/server/data/migrations/versions/6.57/2026-08-04-18-00-00-store-custom-field-values-as-leaf-rows.js +74 -0
  264. package/core/server/data/migrations/versions/6.57/2026-08-04-18-54-14-reset-automation-email-analytics.js +25 -0
  265. package/core/server/data/schema/commands.js +21 -5
  266. package/core/server/data/schema/schema.js +24 -5
  267. package/core/server/lib/common/to-plain.ts +1 -1
  268. package/core/server/models/post.js +2 -2
  269. package/core/server/services/adapter-manager/adapter-manager.js +1 -45
  270. package/core/server/services/adapter-manager/adapter-manager.ts +2 -48
  271. package/core/server/services/adapter-manager/adapter-paths.js +24 -0
  272. package/core/server/services/adapter-manager/adapter-paths.ts +21 -0
  273. package/core/server/services/adapter-manager/base-classes.js +22 -0
  274. package/core/server/services/adapter-manager/base-classes.ts +22 -0
  275. package/core/server/services/adapter-manager/index.js +4 -27
  276. package/core/server/services/adapter-manager/index.ts +4 -31
  277. package/core/server/services/adapter-manager/utils.js +50 -0
  278. package/core/server/services/adapter-manager/utils.ts +57 -0
  279. package/core/server/services/audience-feedback/audience-feedback-controller.js +1 -1
  280. package/core/server/services/audience-feedback/audience-feedback-service.js +2 -2
  281. package/core/server/services/automations/service.js +2 -2
  282. package/core/server/services/automations/service.ts +2 -2
  283. package/core/server/services/comments/comments-service-emails.js +1 -1
  284. package/core/server/services/comments/comments-service.js +1 -1
  285. package/core/server/services/email-analytics/email-analytics-service-wrapper.js +11 -2
  286. package/core/server/services/email-analytics/email-analytics-service.js +1 -1
  287. package/core/server/services/email-service/email-service-wrapper.js +2 -1
  288. package/core/server/services/email-service/mailgun-email-provider.js +15 -0
  289. package/core/server/services/indexnow-ping/indexnow-ping-service.js +1 -1
  290. package/core/server/services/indexnow-ping/indexnow-ping-service.ts +2 -4
  291. package/core/server/services/lib/mailgun-client.js +3 -3
  292. package/core/server/services/media-inliner/external-media-inliner.js +12 -8
  293. package/core/server/services/member-attribution/url-translator.js +5 -7
  294. package/core/server/services/member-welcome-emails/service.js +72 -12
  295. package/core/server/services/members/api.js +1 -1
  296. package/core/server/services/members/import-export/import/completion-email.js +8 -4
  297. package/core/server/services/members/import-export/import/completion-email.ts +9 -4
  298. package/core/server/services/members/import-export/import/importer.js +25 -4
  299. package/core/server/services/members/import-export/import/importer.ts +26 -6
  300. package/core/server/services/members/import-export/import/row.ts +5 -1
  301. package/core/server/services/members/import-export/index.js +1 -5
  302. package/core/server/services/members/import-export/index.ts +2 -6
  303. package/core/server/services/members-custom-fields/queries.js +4 -13
  304. package/core/server/services/members-custom-fields/queries.ts +4 -13
  305. package/core/server/services/members-custom-fields/schema.js +18 -27
  306. package/core/server/services/members-custom-fields/schema.ts +17 -27
  307. package/core/server/services/members-custom-fields/storage.js +94 -27
  308. package/core/server/services/members-custom-fields/storage.ts +114 -39
  309. package/core/server/services/members-custom-fields/values-service.js +98 -219
  310. package/core/server/services/members-custom-fields/values-service.ts +110 -238
  311. package/core/server/services/mentions/resource-service.js +1 -1
  312. package/core/server/services/mentions/service.js +2 -2
  313. package/core/server/services/route-settings/dynamic-routing-service.js +9 -59
  314. package/core/server/services/slack-ping/slack-ping-service.js +2 -2
  315. package/core/server/services/slack-ping/slack-ping-service.ts +3 -5
  316. package/core/server/services/stats/content-stats-service.js +3 -3
  317. package/core/server/services/stats/posts-stats-service.js +2 -2
  318. package/core/server/services/url/config.js +90 -135
  319. package/core/server/services/url/index.js +6 -49
  320. package/core/server/services/url/lazy-find-resource.js +14 -11
  321. package/core/server/services/url/lazy-find-resource.ts +14 -11
  322. package/core/server/services/url/lazy-url-service.js +174 -94
  323. package/core/server/services/url/lazy-url-service.ts +233 -97
  324. package/core/server/services/url/routable-resources.js +62 -71
  325. package/core/server/services/url/router-filter.js +12 -4
  326. package/core/server/services/url/router-filter.ts +12 -4
  327. package/core/server/services/webhooks/index.js +1 -1
  328. package/core/server/services/webhooks/serialize.js +2 -1
  329. package/core/shared/config/env/config.testing-mysql.json +1 -2
  330. package/core/shared/config/env/config.testing.json +1 -2
  331. package/core/shared/events/index.js +1 -2
  332. package/core/shared/labs.js +3 -3
  333. package/package.json +5 -6
  334. package/pnpm-lock.yaml +6 -9
  335. package/pnpm-workspace.yaml +2 -2
  336. package/tsconfig.json +1 -0
  337. package/core/built/admin/assets/Combination-BCm9FL4o.js +0 -41
  338. package/core/built/admin/assets/activitypub/layout-CTgrezN7.js +0 -1
  339. package/core/built/admin/assets/automated-emails-6t2bIRWf.js +0 -7
  340. package/core/built/admin/assets/checkbox-BIl077a0.js +0 -1
  341. package/core/built/admin/assets/code-editor-view-C0aWNwdl.js +0 -1
  342. package/core/built/admin/assets/comments-qwhIjH3c.js +0 -1
  343. package/core/built/admin/assets/disabled-sources-indicator-DYAXW-6V.js +0 -1
  344. package/core/built/admin/assets/dist-BUKmD51u.js +0 -1
  345. package/core/built/admin/assets/dist-CNlMj7ML.js +0 -1
  346. package/core/built/admin/assets/dist-DDY6GRVz.js +0 -1
  347. package/core/built/admin/assets/dist-DG78yroX.js +0 -23
  348. package/core/built/admin/assets/editor-DDYSOXGN.js +0 -8
  349. package/core/built/admin/assets/esm-CFX93H5M.js +0 -2
  350. package/core/built/admin/assets/filters-CLehPo_D.js +0 -1
  351. package/core/built/admin/assets/index-08n05-rc.js +0 -67
  352. package/core/built/admin/assets/index-DgUzgFvS.css +0 -1
  353. package/core/built/admin/assets/label-picker-DVnpu_IF.js +0 -1
  354. package/core/built/admin/assets/layout-B0YLh7R9.js +0 -1
  355. package/core/built/admin/assets/member-custom-fields-C8NpZYza.js +0 -1
  356. package/core/built/admin/assets/member-detail-CIVAdKXf.js +0 -1
  357. package/core/built/admin/assets/members-oLPiSkXD.js +0 -10
  358. package/core/built/admin/assets/modals-CB7v-k3T.js +0 -31
  359. package/core/built/admin/assets/newsletter-rbeDX9Wk.js +0 -1
  360. package/core/built/admin/assets/overview--4V-kx9c.js +0 -1
  361. package/core/built/admin/assets/overview-DSLv30oz.js +0 -1
  362. package/core/built/admin/assets/post-analytics-header-pU2m9ZuV.js +0 -1
  363. package/core/built/admin/assets/preferences-Ru4CRPHC.js +0 -5
  364. package/core/built/admin/assets/profile-BNb1oj7D.js +0 -1
  365. package/core/built/admin/assets/select-B0HteHrp.js +0 -1
  366. package/core/built/admin/assets/settings-B1C9Q3wo.js +0 -4
  367. package/core/built/admin/assets/social-urls-DA2lXrhW.js +0 -8
  368. package/core/built/admin/assets/switch-CyQcwMAv.js +0 -1
  369. package/core/built/admin/assets/tabs-D4y1gqUd.js +0 -1
  370. package/core/built/admin/assets/tags-Dmft-Y4O.js +0 -1
  371. package/core/built/admin/assets/toggle-group-R2QMFvXW.js +0 -1
  372. package/core/built/admin/assets/use-filter-params-WrdhAISv.js +0 -1
  373. package/core/built/admin/assets/web-BqW0b0_C.js +0 -1
  374. package/core/built/admin/assets/web-Cy5GKMWM.js +0 -2
  375. package/core/server/services/url/local-file-cache.js +0 -75
  376. package/core/server/services/url/queue.js +0 -260
  377. package/core/server/services/url/resource.js +0 -102
  378. package/core/server/services/url/resources.js +0 -480
  379. package/core/server/services/url/url-generator.js +0 -276
  380. package/core/server/services/url/url-service-facade.js +0 -366
  381. package/core/server/services/url/url-service-facade.ts +0 -519
  382. package/core/server/services/url/url-service.js +0 -387
  383. package/core/server/services/url/urls.js +0 -140
  384. package/core/shared/events/url-resource-updated-event.js +0 -33
  385. /package/core/built/admin/assets/{dist-CqO2lWS0.js → dist-CqO2lWS02.js} +0 -0
@@ -14,56 +14,34 @@ const queries_1 = require("./queries");
14
14
  const storage_1 = require("./storage");
15
15
  const FIELDS_TABLE = 'members_custom_fields';
16
16
  const VALUES_TABLE = 'members_custom_field_values';
17
- // Matches the `members_custom_fields.key` column (schema.js), so no key a site
18
- // could actually have minted is ever refused by it.
17
+ /** Matches the `members_custom_fields.key` column, so no key a site could hold is refused. */
19
18
  const MAX_KEY_LENGTH = 191;
20
- // Values arrive keyed by field key. Each value stays `unknown` here: it is
21
- // validated by its own field type's schema, which isn't known until the key is
22
- // resolved to a definition.
23
- const ValuesInput = zod_1.z.record(zod_1.z.string().max(MAX_KEY_LENGTH), zod_1.z.unknown());
24
- // JSON.parse as a total function. Anything unreadable — unparseable text, or the null
25
- // a nullable column can always return — reads as `undefined`, which no value schema
26
- // accepts. That way a corrupt blob is turned away by the same check that turns away a
27
- // well-formed one of the wrong shape, instead of needing a branch of its own.
28
- function decodeJson(raw) {
29
- if (raw === null) {
30
- return undefined;
31
- }
32
- try {
33
- return JSON.parse(raw);
34
- }
35
- catch {
36
- return undefined;
37
- }
38
- }
39
19
  /**
40
- * The value half of member custom fields: reading and writing what a member holds
41
- * for each defined field. It belongs to the member aggregate a value is data
42
- * about a member while field definitions belong to the site's settings; that
43
- * aggregate boundary, not a technical layer, is why this is a separate service.
20
+ * Rows per insert statement, bounded by knex rather than by either database. SQLite takes
21
+ * a multi-row `VALUES` perfectly well; knex's SQLite dialect emits that form only for a
22
+ * single row and compiles anything longer into `INSERT ... SELECT ? UNION ALL SELECT ?`,
23
+ * which SQLite refuses past 500 terms.
44
24
  *
45
- * It owns the `members_custom_field_values` table and reads `members_custom_fields`
46
- * directly for the reference data it needs (which fields are active, and how they
47
- * validate), rather than calling the definitions service — the same way the other
48
- * knex services here talk to the database.
25
+ * Open as knex#721 since 2015, with an approved but unmerged fix in knex#5780. This can
26
+ * go when that lands; until then the alternative is hand-written upsert SQL per engine.
27
+ */
28
+ const UPSERT_CHUNK = 400;
29
+ // Values stay `unknown`: each is validated by its own field type, which is not known
30
+ // until the key is resolved to a definition.
31
+ const ValuesInput = zod_1.z.record(zod_1.z.string().max(MAX_KEY_LENGTH), zod_1.z.unknown());
32
+ /**
33
+ * What a member holds for each defined field. Separate from the definitions service
34
+ * because a value belongs to the member and a definition belongs to the site's settings,
35
+ * which are different aggregates rather than different layers.
49
36
  */
50
37
  class CustomFieldValuesService {
51
38
  knex;
52
- /**
53
- * @private
54
- * A getter rather than a number: the ceiling is an operator setting that can
55
- * change between requests.
56
- */
39
+ /** A getter, not a number: the ceiling is an operator setting that changes between requests. */
57
40
  getMaxDefinitions;
58
41
  constructor({ knex, getMaxDefinitions }) {
59
42
  this.knex = knex;
60
43
  this.getMaxDefinitions = getMaxDefinitions;
61
44
  }
62
- /**
63
- * The active fields for a set of keys, keyed by key, for resolving a write.
64
- * Scoped to the keys the caller is actually writing rather than every active
65
- * field, since a publisher may have defined many and an edit touches few.
66
- */
67
45
  async activeFieldsByKey(keys) {
68
46
  if (keys.length === 0) {
69
47
  return new Map();
@@ -74,60 +52,36 @@ class CustomFieldValuesService {
74
52
  return new Map(fields.map(field => [field.key, field]));
75
53
  }
76
54
  /**
77
- * Members' values for every active field, keyed by member id, then by field
78
- * key. A member with no values is absent from the outer map; a field a member
79
- * has no value for is absent from the inner one — "not set" is the absence of
80
- * a key, told apart from a value by not being there. Archived fields are
81
- * excluded to match the definitions `browse`: their values stay in the
82
- * database, they just stop being addressable.
83
- *
84
- * One query for the whole set, so a member list can't turn into an N+1.
55
+ * Members' values, keyed by member id then field key; anything absent is unset.
85
56
  *
86
- * Reading is best-effort per value: a stored value whose type has since left
87
- * the catalog, or that won't decode, is omitted and logged rather than failing
88
- * the whole read. Stored data outlives the catalog, so one stale row must not
89
- * take down a member's payload.
57
+ * Archived fields are excluded to match the definitions browse: their values stay in
58
+ * the database and stop being addressable. A row that will not parse is dropped and
59
+ * logged rather than failing the read, so one stale row cannot take down a member.
90
60
  */
91
61
  async getValuesForMembers(memberIds) {
92
- const byMember = new Map();
93
62
  if (memberIds.length === 0) {
94
- return byMember;
63
+ return new Map();
95
64
  }
96
65
  const rows = await this.knex(VALUES_TABLE)
97
66
  .join(FIELDS_TABLE, `${VALUES_TABLE}.custom_field_id`, `${FIELDS_TABLE}.id`)
98
67
  .whereIn(`${VALUES_TABLE}.member_id`, memberIds)
99
- // Read join, so the status filter is qualified and lives inline — it
100
- // shares the invariant's vocabulary with queries.ts, not its builder.
101
68
  .where(`${FIELDS_TABLE}.status`, schema_1.FIELD_STATUS.active)
102
69
  .orderBy(`${FIELDS_TABLE}.created_at`, 'asc')
103
70
  .orderBy(`${FIELDS_TABLE}.id`, 'asc')
104
- .select(`${VALUES_TABLE}.member_id`, `${FIELDS_TABLE}.key`, `${FIELDS_TABLE}.type`, `${VALUES_TABLE}.value_text`, `${VALUES_TABLE}.value_json`);
71
+ .orderBy(`${VALUES_TABLE}.path`, 'asc')
72
+ .select(`${VALUES_TABLE}.member_id`, `${FIELDS_TABLE}.key`, `${FIELDS_TABLE}.type`, `${VALUES_TABLE}.path`, `${VALUES_TABLE}.value_text`);
73
+ const leaves = [];
105
74
  for (const row of rows) {
106
75
  try {
107
- const { member_id: memberId, key, type, ...stored } = schema_1.DbCustomFieldValueWithField.parse(row);
108
- const codec = (0, storage_1.storageCodecFor)(type);
109
- const raw = stored[codec.column];
110
- // The column the type routes to is the only one that can hold this
111
- // field's value; a null there means the row is not carrying one.
112
- if (raw === null) {
113
- continue;
114
- }
115
- const values = byMember.get(memberId) ?? {};
116
- values[key] = codec.decode(raw);
117
- byMember.set(memberId, values);
76
+ leaves.push(schema_1.DbCustomFieldLeaf.parse(row));
118
77
  }
119
78
  catch (err) {
120
- logging_1.default.warn(`Skipping undecodable custom field value (field '${row.key}', type '${row.type}'): ${err instanceof Error ? err.message : String(err)}`);
79
+ logging_1.default.warn(`Skipping unreadable custom field value (field '${row.key}', path '${row.path}'): ${err instanceof Error ? err.message : String(err)}`);
121
80
  }
122
81
  }
123
- return byMember;
82
+ return (0, storage_1.valuesFromLeaves)(leaves);
124
83
  }
125
- /**
126
- * @private
127
- * Input as the values object it claims to be, rejecting anything that isn't
128
- * one. Shared by every caller so they cannot disagree on what a values object
129
- * is, or on the error when it isn't one.
130
- */
84
+ /** Shared by every caller so they cannot disagree on what a values object is. */
131
85
  parseValues(input) {
132
86
  const parsed = ValuesInput.safeParse(input);
133
87
  if (!parsed.success) {
@@ -136,12 +90,8 @@ class CustomFieldValuesService {
136
90
  return parsed.data;
137
91
  }
138
92
  /**
139
- * Whether input names any values. An absent key names none; anything present
140
- * that isn't a values object throws, with the same error resolving it would
141
- * have raised.
142
- *
143
- * Answers the shape question alone, with no catalog lookup, so it can be asked
144
- * before a write is known to be permitted.
93
+ * Whether input names any values. Asks the shape question alone, with no catalog
94
+ * lookup, so it can be asked before a write is known to be permitted.
145
95
  */
146
96
  namesValues(input) {
147
97
  if (input === undefined) {
@@ -150,19 +100,15 @@ class CustomFieldValuesService {
150
100
  return Object.keys(this.parseValues(input)).length > 0;
151
101
  }
152
102
  /**
153
- * Resolve input into the writes it implies, rejecting anything invalid, and
154
- * writing nothing. Returned so a caller can validate before it commits to a
155
- * change it would have to unwind (the member edit and the importer both validate
156
- * up front, before opening a transaction), then apply the same plan without
157
- * re-resolving or re-validating.
103
+ * Resolve input into the writes it implies, writing nothing. Returned so a caller can
104
+ * validate before opening a transaction it would otherwise have to unwind, then apply
105
+ * the same plan without re-resolving it.
158
106
  */
159
107
  async planWrite(input) {
160
108
  const values = this.parseValues(input);
161
109
  const keys = Object.keys(values);
162
- // A write cannot name more fields than the site may define, so the
163
- // definitions ceiling bounds it. This also holds the lookup below within the
164
- // database driver's bound-parameter limit, which one key per parameter would
165
- // otherwise exceed.
110
+ // Bounded by the definitions ceiling, which also holds the lookup below inside
111
+ // the driver's bound-parameter limit.
166
112
  const maxKeys = this.getMaxDefinitions();
167
113
  if (keys.length > maxKeys) {
168
114
  throw new errors_1.default.ValidationError({
@@ -175,27 +121,27 @@ class CustomFieldValuesService {
175
121
  for (const [key, raw] of Object.entries(values)) {
176
122
  const field = byKey.get(key);
177
123
  if (!field) {
178
- // Unknown (or archived) key. Rejected rather than ignored: a typo
179
- // that silently drops a value is worse than a failed save.
124
+ // Unknown or archived. Refused rather than ignored: a typo that silently
125
+ // drops what somebody typed is worse than a save that fails. The catalog
126
+ // applies the same rule to the parts of a composite value.
180
127
  throw new errors_1.default.ValidationError({ message: `Unknown custom field: ${key}`, property: `custom_fields.${key}` });
181
128
  }
182
- // `null` clears any field. An empty string clears a text-backed field
183
- // an emptied input but for any other storage type '' is a value to
184
- // validate, not a clear, so nothing is coerced: a string sent to a
185
- // (future) non-text field fails validation rather than silently deleting.
186
- if (raw === null || (raw === '' && custom_field_types_1.FIELD_TYPES[field.type].storageType === 'text')) {
129
+ // `null` clears any field, and `''` clears one with no parts. For a value
130
+ // with parts `''` names nothing, so it is left to fail validation rather than
131
+ // being read as a silent delete.
132
+ if (raw === null || (raw === '' && (0, custom_field_types_1.subFieldsOf)(field.type) === null)) {
187
133
  writes.push({ field });
188
134
  continue;
189
135
  }
190
- // The field type owns what a valid value is core just runs it. For a
191
- // composite the issue path points at the offending sub-field, so the
192
- // property reads `custom_fields.home_address.postal_code`.
136
+ // Message only, no `context`: the API error handler moves a message into
137
+ // `context` when `context` is empty and prepends it when it is not, so
138
+ // anything added here reaches the client glued to the front of the reason.
139
+ // Which field failed rides in `property`.
193
140
  const value = custom_field_types_1.FIELD_TYPES[field.type].value.safeParse(raw);
194
141
  if (!value.success) {
195
142
  const issue = value.error.issues[0];
196
143
  throw new errors_1.default.ValidationError({
197
- message: `Invalid value for custom field '${field.name}'.`,
198
- context: issue.message,
144
+ message: issue.message,
199
145
  property: [`custom_fields.${key}`, ...issue.path].join('.')
200
146
  });
201
147
  }
@@ -203,139 +149,72 @@ class CustomFieldValuesService {
203
149
  }
204
150
  return writes;
205
151
  }
206
- /**
207
- * @private
208
- * The composite values this member already holds for the fields in a plan,
209
- * keyed by field id. Composites only: a scalar has nothing to merge into.
210
- *
211
- * Read through the caller's executor, never the base connection. On a
212
- * single-connection pool a read off `this.knex` inside an open transaction
213
- * waits for the connection that transaction is holding, and hangs.
214
- *
215
- * A blob that won't decode is left out, so the write replaces it rather than
216
- * merging into something unreadable.
217
- */
218
- async storedComposites(trx, memberId, writes) {
219
- const composites = new Map();
220
- // Merging is only meaningful for a value with parts, which is a fact about the
221
- // type's shape rather than the column it lands in. Asking the storage type
222
- // instead would sweep in the first json-backed type that isn't a record — a
223
- // multi-select held as a list, say — and log a decode failure for every row of
224
- // an import, over a value that reads back perfectly well.
225
- const fieldIds = writes
226
- .filter(({ field, value }) => value !== undefined && (0, custom_field_types_1.subFieldsOf)(field.type) !== null)
227
- .map(({ field }) => field.id);
228
- if (fieldIds.length === 0) {
229
- return composites;
230
- }
231
- const rows = await trx(VALUES_TABLE)
232
- .where('member_id', memberId)
233
- .whereIn('custom_field_id', fieldIds)
234
- .whereNotNull('value_json')
235
- .select('custom_field_id', 'value_json')
236
- // Merging reads a value and writes it back, so the row has to stay still in
237
- // between: without the lock a write committing after this read is overwritten
238
- // by the sub-fields it just replaced. knex omits this on SQLite, which needs
239
- // no lock — its pool is a single connection, so writes never overlap.
240
- .forUpdate();
241
- for (const row of rows) {
242
- // The blob is the edge here: the columns come from a query written just
243
- // above, but whatever an earlier write left in value_json is unknown until
244
- // it is read back. Parsing it is what narrows the type, and it rejects the
245
- // array, null or bare number the column could equally be holding.
246
- const stored = schema_1.StoredCompositeValue.safeParse(decodeJson(row.value_json));
247
- if (stored.success) {
248
- composites.set(row.custom_field_id, stored.data);
249
- continue;
250
- }
251
- logging_1.default.warn(`Replacing an unreadable custom field value rather than merging into it (member '${memberId}', field '${row.custom_field_id}').`);
252
- }
253
- return composites;
254
- }
255
- /**
256
- * @private
257
- * The stored sub-fields with the incoming ones written over them, re-checked
258
- * against the field type before it is handed back.
259
- *
260
- * The re-check is what makes this safe to write without a second thought. Both
261
- * halves were valid when they were written, but "valid" is a moving target: the
262
- * stored half may predate a change to the type, so it can carry a sub-field the
263
- * type has since dropped or a bound it has since tightened. Parsing the result
264
- * strips what no longer belongs instead of faithfully persisting it on every
265
- * import from here on.
266
- *
267
- * A merged value that won't parse falls back to the incoming value alone rather
268
- * than failing. The row is trying to write something valid, and refusing it over
269
- * data that was already in the database punishes the wrong write.
270
- */
271
- mergeComposite(field, existing, incoming) {
272
- const merged = custom_field_types_1.FIELD_TYPES[field.type].value.safeParse({ ...existing, ...incoming });
273
- if (merged.success) {
274
- return merged.data;
275
- }
276
- logging_1.default.warn(`Replacing a custom field value that no longer validates rather than merging into it (field '${field.key}').`);
277
- return incoming;
278
- }
279
152
  /**
280
153
  * Apply a plan from `planWrite`.
281
154
  *
282
- * Merge, not replace: only the fields in the plan are touched, so a caller
283
- * that doesn't know about a field can't erase it.
284
- *
285
- * `mergeComposites` extends that same rule from a field to a composite's
286
- * sub-fields, because a sub-field is a field: a row writes the sub-fields it
287
- * fills and leaves the rest alone, exactly as a blank `name` column leaves a
288
- * member's name alone. The CSV import needs it — without it a file naming one
289
- * address column would replace the whole address, clearing five sub-fields it
290
- * never mentioned. The API does not merge: a PUT sends a whole address, so
291
- * what it sends is what should be stored.
155
+ * A write touches the paths it names and nothing else, at every level: naming a path
156
+ * with an empty value clears that part, naming the field with `null` clears all of
157
+ * them, and saying nothing about a path leaves it alone. There is no whole-value
158
+ * replace, so a caller that does not know about a field cannot erase it.
292
159
  *
293
- * The merge is asked for here rather than at plan time, where the caller's intent
294
- * is otherwise expressed, because it cannot happen before a transaction is open:
295
- * it reads the stored value and writes it back, and the row lock that keeps those
296
- * two steps together only exists inside the transaction. `planWrite` runs before
297
- * one is opened -- the importer plans a row, then opens a transaction per member --
298
- * so a merge decided there would have to read outside the lock it depends on.
299
- *
300
- * Always transactional, so a mid-batch failure rolls the batch back. Passed an
301
- * executor it joins that transaction -- the importer passes its per-member one, so a
302
- * failed value write takes the member with it; passed nothing it opens its own.
160
+ * Always transactional. Given an executor it joins that transaction, so the importer's
161
+ * failed value write takes its member with it; given none it opens its own.
303
162
  */
304
- async applyWrite(memberId, writes, executor = this.knex, { mergeComposites = false } = {}) {
163
+ async applyWrite(memberId, writes, { executor = this.knex } = {}) {
305
164
  if (writes.length === 0) {
306
165
  return;
307
166
  }
308
167
  const apply = async (trx) => {
309
- const stored = mergeComposites
310
- ? await this.storedComposites(trx, memberId, writes)
311
- : new Map();
168
+ // Built first, then sent as whole statements: a handful per member rather
169
+ // than one per part, under one timestamp, because a write happened once
170
+ // however many rows record it.
171
+ const now = new Date();
172
+ const clearedFields = [];
173
+ const clearedPaths = [];
174
+ const rows = [];
312
175
  for (const { field, value } of writes) {
313
- const target = { member_id: memberId, custom_field_id: field.id };
314
176
  if (value === undefined) {
315
- await trx(VALUES_TABLE).where(target).del();
177
+ clearedFields.push(field.id);
316
178
  continue;
317
179
  }
318
- // Both sides of the merge go through the same shape check, so it joins
319
- // two objects the type system knows about rather than two assertions.
320
- // The plan already validated `value` against its field type, so for a
321
- // composite this parse cannot fail; a scalar simply doesn't match, and
322
- // falls through to being written as it stands.
323
- const existing = stored.get(field.id);
324
- const incoming = schema_1.StoredCompositeValue.safeParse(value);
325
- const merged = existing && incoming.success
326
- ? this.mergeComposite(field, existing, incoming.data)
327
- : value;
328
- const valueColumns = (0, storage_1.storageColumnsFor)(field.type, merged);
180
+ const { set, cleared } = (0, storage_1.leavesToWrite)(value);
181
+ if (cleared.length > 0) {
182
+ clearedPaths.push({ fieldId: field.id, paths: cleared });
183
+ }
184
+ rows.push(...set.map(leaf => ({
185
+ id: new bson_objectid_1.default().toHexString(),
186
+ member_id: memberId,
187
+ custom_field_id: field.id,
188
+ path: leaf.path,
189
+ value_text: leaf.value_text,
190
+ created_at: now,
191
+ updated_at: now
192
+ })));
193
+ }
194
+ if (clearedFields.length > 0) {
195
+ await trx(VALUES_TABLE).where('member_id', memberId).whereIn('custom_field_id', clearedFields).del();
196
+ }
197
+ if (clearedPaths.length > 0) {
198
+ // One statement with a group per field, rather than a statement per field.
199
+ await trx(VALUES_TABLE).where('member_id', memberId).where((builder) => {
200
+ for (const { fieldId, paths } of clearedPaths) {
201
+ builder.orWhere(pair => pair.where('custom_field_id', fieldId).whereIn('path', paths));
202
+ }
203
+ }).del();
204
+ }
205
+ for (let from = 0; from < rows.length; from += UPSERT_CHUNK) {
206
+ // Typed as the plain row because `merge` takes its columns as `keyof` the
207
+ // builder's record, which for a composite table registration is the scope
208
+ // names rather than the columns.
329
209
  await trx(VALUES_TABLE)
330
- .insert({ id: new bson_objectid_1.default().toHexString(), ...target, ...valueColumns, created_at: new Date() })
331
- // The unique index on (member_id, custom_field_id) is
332
- // what makes this an upsert rather than a read-then-write race.
333
- .onConflict(['member_id', 'custom_field_id'])
334
- .merge({ ...valueColumns, updated_at: new Date() });
210
+ .insert(rows.slice(from, from + UPSERT_CHUNK))
211
+ // Naming the columns rather than giving values takes each from the row
212
+ // that lost the conflict, so every part updates to its own value.
213
+ .onConflict(['member_id', 'custom_field_id', 'path'])
214
+ .merge(['value_text', 'updated_at']);
335
215
  }
336
216
  };
337
- // isTransaction is knex's marker for a transactor: join an existing transaction
338
- // rather than nesting a savepoint under it, otherwise open one.
217
+ // knex's marker for a transactor: join it rather than nesting a savepoint under it.
339
218
  if (executor.isTransaction) {
340
219
  await apply(executor);
341
220
  }