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
@@ -4,42 +4,34 @@ import logging from '@tryghost/logging';
4
4
  import type {Knex} from 'knex';
5
5
  import {z} from 'zod';
6
6
  import {FIELD_TYPES, subFieldsOf, type FieldType} from '@tryghost/custom-field-types';
7
- import {DbCustomFieldValueWithField, FIELD_STATUS, StoredCompositeValue} from './schema';
7
+ import {DbCustomFieldLeaf, DbCustomFieldValue, FIELD_STATUS} from './schema';
8
8
  import {activeFields} from './queries';
9
- import {storageCodecFor, storageColumnsFor} from './storage';
9
+ import {leavesToWrite, valuesFromLeaves, type StoredLeaf} from './storage';
10
10
 
11
11
  const FIELDS_TABLE = 'members_custom_fields';
12
12
  const VALUES_TABLE = 'members_custom_field_values';
13
13
 
14
- // Matches the `members_custom_fields.key` column (schema.js), so no key a site
15
- // could actually have minted is ever refused by it.
14
+ /** Matches the `members_custom_fields.key` column, so no key a site could hold is refused. */
16
15
  const MAX_KEY_LENGTH = 191;
17
16
 
18
- // Values arrive keyed by field key. Each value stays `unknown` here: it is
19
- // validated by its own field type's schema, which isn't known until the key is
20
- // resolved to a definition.
21
- const ValuesInput = z.record(z.string().max(MAX_KEY_LENGTH), z.unknown());
17
+ /**
18
+ * Rows per insert statement, bounded by knex rather than by either database. SQLite takes
19
+ * a multi-row `VALUES` perfectly well; knex's SQLite dialect emits that form only for a
20
+ * single row and compiles anything longer into `INSERT ... SELECT ? UNION ALL SELECT ?`,
21
+ * which SQLite refuses past 500 terms.
22
+ *
23
+ * Open as knex#721 since 2015, with an approved but unmerged fix in knex#5780. This can
24
+ * go when that lands; until then the alternative is hand-written upsert SQL per engine.
25
+ */
26
+ const UPSERT_CHUNK = 400;
22
27
 
23
- // JSON.parse as a total function. Anything unreadable unparseable text, or the null
24
- // a nullable column can always return — reads as `undefined`, which no value schema
25
- // accepts. That way a corrupt blob is turned away by the same check that turns away a
26
- // well-formed one of the wrong shape, instead of needing a branch of its own.
27
- function decodeJson(raw: string | null): unknown {
28
- if (raw === null) {
29
- return undefined;
30
- }
31
- try {
32
- return JSON.parse(raw);
33
- } catch {
34
- return undefined;
35
- }
36
- }
28
+ /** Derived, not restated, so a column changing shape in `schema.ts` changes here too. */
29
+ type DbLeafRow = z.infer<typeof DbCustomFieldValue>;
30
+
31
+ // Values stay `unknown`: each is validated by its own field type, which is not known
32
+ // until the key is resolved to a definition.
33
+ const ValuesInput = z.record(z.string().max(MAX_KEY_LENGTH), z.unknown());
37
34
 
38
- // The field facts the value path needs: the id to write the FK, the key to match
39
- // input against, the name for error messages, and the type to pick the validator
40
- // and storage column. All columns on members_custom_fields, so a value operation
41
- // resolves them with a direct query rather than routing through the definitions
42
- // service — the same way the read path already joins the table directly.
43
35
  interface ActiveField {
44
36
  id: string;
45
37
  key: string;
@@ -47,30 +39,20 @@ interface ActiveField {
47
39
  type: FieldType;
48
40
  }
49
41
 
50
- // A resolved, validated write. `value` absent means clear (delete the row).
42
+ /** An absent `value` means clear the field. */
51
43
  interface PlannedWrite {
52
44
  field: ActiveField;
53
45
  value?: unknown;
54
46
  }
55
47
 
56
48
  /**
57
- * The value half of member custom fields: reading and writing what a member holds
58
- * for each defined field. It belongs to the member aggregate a value is data
59
- * about a member while field definitions belong to the site's settings; that
60
- * aggregate boundary, not a technical layer, is why this is a separate service.
61
- *
62
- * It owns the `members_custom_field_values` table and reads `members_custom_fields`
63
- * directly for the reference data it needs (which fields are active, and how they
64
- * validate), rather than calling the definitions service — the same way the other
65
- * knex services here talk to the database.
49
+ * What a member holds for each defined field. Separate from the definitions service
50
+ * because a value belongs to the member and a definition belongs to the site's settings,
51
+ * which are different aggregates rather than different layers.
66
52
  */
67
53
  export class CustomFieldValuesService {
68
54
  private knex: Knex;
69
- /**
70
- * @private
71
- * A getter rather than a number: the ceiling is an operator setting that can
72
- * change between requests.
73
- */
55
+ /** A getter, not a number: the ceiling is an operator setting that changes between requests. */
74
56
  private getMaxDefinitions: () => number;
75
57
 
76
58
  constructor({knex, getMaxDefinitions}: {knex: Knex, getMaxDefinitions: () => number}) {
@@ -78,11 +60,6 @@ export class CustomFieldValuesService {
78
60
  this.getMaxDefinitions = getMaxDefinitions;
79
61
  }
80
62
 
81
- /**
82
- * The active fields for a set of keys, keyed by key, for resolving a write.
83
- * Scoped to the keys the caller is actually writing rather than every active
84
- * field, since a publisher may have defined many and an edit touches few.
85
- */
86
63
  private async activeFieldsByKey(keys: string[]): Promise<Map<string, ActiveField>> {
87
64
  if (keys.length === 0) {
88
65
  return new Map();
@@ -94,63 +71,39 @@ export class CustomFieldValuesService {
94
71
  }
95
72
 
96
73
  /**
97
- * Members' values for every active field, keyed by member id, then by field
98
- * key. A member with no values is absent from the outer map; a field a member
99
- * has no value for is absent from the inner one — "not set" is the absence of
100
- * a key, told apart from a value by not being there. Archived fields are
101
- * excluded to match the definitions `browse`: their values stay in the
102
- * database, they just stop being addressable.
74
+ * Members' values, keyed by member id then field key; anything absent is unset.
103
75
  *
104
- * One query for the whole set, so a member list can't turn into an N+1.
105
- *
106
- * Reading is best-effort per value: a stored value whose type has since left
107
- * the catalog, or that won't decode, is omitted and logged rather than failing
108
- * the whole read. Stored data outlives the catalog, so one stale row must not
109
- * take down a member's payload.
76
+ * Archived fields are excluded to match the definitions browse: their values stay in
77
+ * the database and stop being addressable. A row that will not parse is dropped and
78
+ * logged rather than failing the read, so one stale row cannot take down a member.
110
79
  */
111
80
  async getValuesForMembers(memberIds: string[]): Promise<Map<string, Record<string, unknown>>> {
112
- const byMember = new Map<string, Record<string, unknown>>();
113
81
  if (memberIds.length === 0) {
114
- return byMember;
82
+ return new Map();
115
83
  }
116
84
 
117
85
  const rows = await this.knex(VALUES_TABLE)
118
86
  .join(FIELDS_TABLE, `${VALUES_TABLE}.custom_field_id`, `${FIELDS_TABLE}.id`)
119
87
  .whereIn(`${VALUES_TABLE}.member_id`, memberIds)
120
- // Read join, so the status filter is qualified and lives inline — it
121
- // shares the invariant's vocabulary with queries.ts, not its builder.
122
88
  .where(`${FIELDS_TABLE}.status`, FIELD_STATUS.active)
123
89
  .orderBy(`${FIELDS_TABLE}.created_at`, 'asc')
124
90
  .orderBy(`${FIELDS_TABLE}.id`, 'asc')
125
- .select(`${VALUES_TABLE}.member_id`, `${FIELDS_TABLE}.key`, `${FIELDS_TABLE}.type`, `${VALUES_TABLE}.value_text`, `${VALUES_TABLE}.value_json`);
91
+ .orderBy(`${VALUES_TABLE}.path`, 'asc')
92
+ .select(`${VALUES_TABLE}.member_id`, `${FIELDS_TABLE}.key`, `${FIELDS_TABLE}.type`, `${VALUES_TABLE}.path`, `${VALUES_TABLE}.value_text`);
126
93
 
94
+ const leaves: StoredLeaf[] = [];
127
95
  for (const row of rows) {
128
96
  try {
129
- const {member_id: memberId, key, type, ...stored} = DbCustomFieldValueWithField.parse(row);
130
- const codec = storageCodecFor(type);
131
- const raw = stored[codec.column];
132
- // The column the type routes to is the only one that can hold this
133
- // field's value; a null there means the row is not carrying one.
134
- if (raw === null) {
135
- continue;
136
- }
137
- const values = byMember.get(memberId) ?? {};
138
- values[key] = codec.decode(raw);
139
- byMember.set(memberId, values);
97
+ leaves.push(DbCustomFieldLeaf.parse(row));
140
98
  } catch (err) {
141
- logging.warn(`Skipping undecodable custom field value (field '${row.key}', type '${row.type}'): ${err instanceof Error ? err.message : String(err)}`);
99
+ logging.warn(`Skipping unreadable custom field value (field '${row.key}', path '${row.path}'): ${err instanceof Error ? err.message : String(err)}`);
142
100
  }
143
101
  }
144
102
 
145
- return byMember;
103
+ return valuesFromLeaves(leaves);
146
104
  }
147
105
 
148
- /**
149
- * @private
150
- * Input as the values object it claims to be, rejecting anything that isn't
151
- * one. Shared by every caller so they cannot disagree on what a values object
152
- * is, or on the error when it isn't one.
153
- */
106
+ /** Shared by every caller so they cannot disagree on what a values object is. */
154
107
  private parseValues(input: unknown): Record<string, unknown> {
155
108
  const parsed = ValuesInput.safeParse(input);
156
109
  if (!parsed.success) {
@@ -161,12 +114,8 @@ export class CustomFieldValuesService {
161
114
  }
162
115
 
163
116
  /**
164
- * Whether input names any values. An absent key names none; anything present
165
- * that isn't a values object throws, with the same error resolving it would
166
- * have raised.
167
- *
168
- * Answers the shape question alone, with no catalog lookup, so it can be asked
169
- * before a write is known to be permitted.
117
+ * Whether input names any values. Asks the shape question alone, with no catalog
118
+ * lookup, so it can be asked before a write is known to be permitted.
170
119
  */
171
120
  namesValues(input: unknown): boolean {
172
121
  if (input === undefined) {
@@ -177,20 +126,16 @@ export class CustomFieldValuesService {
177
126
  }
178
127
 
179
128
  /**
180
- * Resolve input into the writes it implies, rejecting anything invalid, and
181
- * writing nothing. Returned so a caller can validate before it commits to a
182
- * change it would have to unwind (the member edit and the importer both validate
183
- * up front, before opening a transaction), then apply the same plan without
184
- * re-resolving or re-validating.
129
+ * Resolve input into the writes it implies, writing nothing. Returned so a caller can
130
+ * validate before opening a transaction it would otherwise have to unwind, then apply
131
+ * the same plan without re-resolving it.
185
132
  */
186
133
  async planWrite(input: unknown): Promise<PlannedWrite[]> {
187
134
  const values = this.parseValues(input);
188
135
  const keys = Object.keys(values);
189
136
 
190
- // A write cannot name more fields than the site may define, so the
191
- // definitions ceiling bounds it. This also holds the lookup below within the
192
- // database driver's bound-parameter limit, which one key per parameter would
193
- // otherwise exceed.
137
+ // Bounded by the definitions ceiling, which also holds the lookup below inside
138
+ // the driver's bound-parameter limit.
194
139
  const maxKeys = this.getMaxDefinitions();
195
140
  if (keys.length > maxKeys) {
196
141
  throw new errors.ValidationError({
@@ -205,29 +150,29 @@ export class CustomFieldValuesService {
205
150
  for (const [key, raw] of Object.entries(values)) {
206
151
  const field = byKey.get(key);
207
152
  if (!field) {
208
- // Unknown (or archived) key. Rejected rather than ignored: a typo
209
- // that silently drops a value is worse than a failed save.
153
+ // Unknown or archived. Refused rather than ignored: a typo that silently
154
+ // drops what somebody typed is worse than a save that fails. The catalog
155
+ // applies the same rule to the parts of a composite value.
210
156
  throw new errors.ValidationError({message: `Unknown custom field: ${key}`, property: `custom_fields.${key}`});
211
157
  }
212
158
 
213
- // `null` clears any field. An empty string clears a text-backed field
214
- // an emptied input but for any other storage type '' is a value to
215
- // validate, not a clear, so nothing is coerced: a string sent to a
216
- // (future) non-text field fails validation rather than silently deleting.
217
- if (raw === null || (raw === '' && FIELD_TYPES[field.type].storageType === 'text')) {
159
+ // `null` clears any field, and `''` clears one with no parts. For a value
160
+ // with parts `''` names nothing, so it is left to fail validation rather than
161
+ // being read as a silent delete.
162
+ if (raw === null || (raw === '' && subFieldsOf(field.type) === null)) {
218
163
  writes.push({field});
219
164
  continue;
220
165
  }
221
166
 
222
- // The field type owns what a valid value is core just runs it. For a
223
- // composite the issue path points at the offending sub-field, so the
224
- // property reads `custom_fields.home_address.postal_code`.
167
+ // Message only, no `context`: the API error handler moves a message into
168
+ // `context` when `context` is empty and prepends it when it is not, so
169
+ // anything added here reaches the client glued to the front of the reason.
170
+ // Which field failed rides in `property`.
225
171
  const value = FIELD_TYPES[field.type].value.safeParse(raw);
226
172
  if (!value.success) {
227
173
  const issue = value.error.issues[0];
228
174
  throw new errors.ValidationError({
229
- message: `Invalid value for custom field '${field.name}'.`,
230
- context: issue.message,
175
+ message: issue.message,
231
176
  property: [`custom_fields.${key}`, ...issue.path].join('.')
232
177
  });
233
178
  }
@@ -237,157 +182,84 @@ export class CustomFieldValuesService {
237
182
  return writes;
238
183
  }
239
184
 
240
- /**
241
- * @private
242
- * The composite values this member already holds for the fields in a plan,
243
- * keyed by field id. Composites only: a scalar has nothing to merge into.
244
- *
245
- * Read through the caller's executor, never the base connection. On a
246
- * single-connection pool a read off `this.knex` inside an open transaction
247
- * waits for the connection that transaction is holding, and hangs.
248
- *
249
- * A blob that won't decode is left out, so the write replaces it rather than
250
- * merging into something unreadable.
251
- */
252
- private async storedComposites(trx: Knex, memberId: string, writes: PlannedWrite[]): Promise<Map<string, Record<string, unknown>>> {
253
- const composites = new Map<string, Record<string, unknown>>();
254
- // Merging is only meaningful for a value with parts, which is a fact about the
255
- // type's shape rather than the column it lands in. Asking the storage type
256
- // instead would sweep in the first json-backed type that isn't a record — a
257
- // multi-select held as a list, say — and log a decode failure for every row of
258
- // an import, over a value that reads back perfectly well.
259
- const fieldIds = writes
260
- .filter(({field, value}) => value !== undefined && subFieldsOf(field.type) !== null)
261
- .map(({field}) => field.id);
262
-
263
- if (fieldIds.length === 0) {
264
- return composites;
265
- }
266
-
267
- const rows = await trx(VALUES_TABLE)
268
- .where('member_id', memberId)
269
- .whereIn('custom_field_id', fieldIds)
270
- .whereNotNull('value_json')
271
- .select('custom_field_id', 'value_json')
272
- // Merging reads a value and writes it back, so the row has to stay still in
273
- // between: without the lock a write committing after this read is overwritten
274
- // by the sub-fields it just replaced. knex omits this on SQLite, which needs
275
- // no lock — its pool is a single connection, so writes never overlap.
276
- .forUpdate();
277
-
278
- for (const row of rows) {
279
- // The blob is the edge here: the columns come from a query written just
280
- // above, but whatever an earlier write left in value_json is unknown until
281
- // it is read back. Parsing it is what narrows the type, and it rejects the
282
- // array, null or bare number the column could equally be holding.
283
- const stored = StoredCompositeValue.safeParse(decodeJson(row.value_json));
284
- if (stored.success) {
285
- composites.set(row.custom_field_id, stored.data);
286
- continue;
287
- }
288
- logging.warn(`Replacing an unreadable custom field value rather than merging into it (member '${memberId}', field '${row.custom_field_id}').`);
289
- }
290
-
291
- return composites;
292
- }
293
-
294
- /**
295
- * @private
296
- * The stored sub-fields with the incoming ones written over them, re-checked
297
- * against the field type before it is handed back.
298
- *
299
- * The re-check is what makes this safe to write without a second thought. Both
300
- * halves were valid when they were written, but "valid" is a moving target: the
301
- * stored half may predate a change to the type, so it can carry a sub-field the
302
- * type has since dropped or a bound it has since tightened. Parsing the result
303
- * strips what no longer belongs instead of faithfully persisting it on every
304
- * import from here on.
305
- *
306
- * A merged value that won't parse falls back to the incoming value alone rather
307
- * than failing. The row is trying to write something valid, and refusing it over
308
- * data that was already in the database punishes the wrong write.
309
- */
310
- private mergeComposite(field: ActiveField, existing: Record<string, unknown>, incoming: Record<string, unknown>): unknown {
311
- const merged = FIELD_TYPES[field.type].value.safeParse({...existing, ...incoming});
312
- if (merged.success) {
313
- return merged.data;
314
- }
315
-
316
- logging.warn(`Replacing a custom field value that no longer validates rather than merging into it (field '${field.key}').`);
317
- return incoming;
318
- }
319
-
320
185
  /**
321
186
  * Apply a plan from `planWrite`.
322
187
  *
323
- * Merge, not replace: only the fields in the plan are touched, so a caller
324
- * that doesn't know about a field can't erase it.
325
- *
326
- * `mergeComposites` extends that same rule from a field to a composite's
327
- * sub-fields, because a sub-field is a field: a row writes the sub-fields it
328
- * fills and leaves the rest alone, exactly as a blank `name` column leaves a
329
- * member's name alone. The CSV import needs it — without it a file naming one
330
- * address column would replace the whole address, clearing five sub-fields it
331
- * never mentioned. The API does not merge: a PUT sends a whole address, so
332
- * what it sends is what should be stored.
333
- *
334
- * The merge is asked for here rather than at plan time, where the caller's intent
335
- * is otherwise expressed, because it cannot happen before a transaction is open:
336
- * it reads the stored value and writes it back, and the row lock that keeps those
337
- * two steps together only exists inside the transaction. `planWrite` runs before
338
- * one is opened -- the importer plans a row, then opens a transaction per member --
339
- * so a merge decided there would have to read outside the lock it depends on.
188
+ * A write touches the paths it names and nothing else, at every level: naming a path
189
+ * with an empty value clears that part, naming the field with `null` clears all of
190
+ * them, and saying nothing about a path leaves it alone. There is no whole-value
191
+ * replace, so a caller that does not know about a field cannot erase it.
340
192
  *
341
- * Always transactional, so a mid-batch failure rolls the batch back. Passed an
342
- * executor it joins that transaction -- the importer passes its per-member one, so a
343
- * failed value write takes the member with it; passed nothing it opens its own.
193
+ * Always transactional. Given an executor it joins that transaction, so the importer's
194
+ * failed value write takes its member with it; given none it opens its own.
344
195
  */
345
196
  async applyWrite(
346
197
  memberId: string,
347
198
  writes: PlannedWrite[],
348
- executor: Knex = this.knex,
349
- {mergeComposites = false}: {mergeComposites?: boolean} = {}
199
+ {executor = this.knex}: {executor?: Knex} = {}
350
200
  ): Promise<void> {
351
201
  if (writes.length === 0) {
352
202
  return;
353
203
  }
354
204
 
355
205
  const apply = async (trx: Knex) => {
356
- const stored = mergeComposites
357
- ? await this.storedComposites(trx, memberId, writes)
358
- : new Map<string, Record<string, unknown>>();
206
+ // Built first, then sent as whole statements: a handful per member rather
207
+ // than one per part, under one timestamp, because a write happened once
208
+ // however many rows record it.
209
+ const now = new Date();
210
+ const clearedFields: string[] = [];
211
+ const clearedPaths: Array<{fieldId: string, paths: string[]}> = [];
212
+ const rows: DbLeafRow[] = [];
359
213
 
360
214
  for (const {field, value} of writes) {
361
- const target = {member_id: memberId, custom_field_id: field.id};
362
-
363
215
  if (value === undefined) {
364
- await trx(VALUES_TABLE).where(target).del();
216
+ clearedFields.push(field.id);
365
217
  continue;
366
218
  }
367
219
 
368
- // Both sides of the merge go through the same shape check, so it joins
369
- // two objects the type system knows about rather than two assertions.
370
- // The plan already validated `value` against its field type, so for a
371
- // composite this parse cannot fail; a scalar simply doesn't match, and
372
- // falls through to being written as it stands.
373
- const existing = stored.get(field.id);
374
- const incoming = StoredCompositeValue.safeParse(value);
375
- const merged = existing && incoming.success
376
- ? this.mergeComposite(field, existing, incoming.data)
377
- : value;
378
-
379
- const valueColumns = storageColumnsFor(field.type, merged);
380
- await trx(VALUES_TABLE)
381
- .insert({id: new ObjectID().toHexString(), ...target, ...valueColumns, created_at: new Date()})
382
- // The unique index on (member_id, custom_field_id) is
383
- // what makes this an upsert rather than a read-then-write race.
384
- .onConflict(['member_id', 'custom_field_id'])
385
- .merge({...valueColumns, updated_at: new Date()});
220
+ const {set, cleared} = leavesToWrite(value);
221
+ if (cleared.length > 0) {
222
+ clearedPaths.push({fieldId: field.id, paths: cleared});
223
+ }
224
+
225
+ rows.push(...set.map(leaf => ({
226
+ id: new ObjectID().toHexString(),
227
+ member_id: memberId,
228
+ custom_field_id: field.id,
229
+ path: leaf.path,
230
+ value_text: leaf.value_text,
231
+ created_at: now,
232
+ updated_at: now
233
+ })));
234
+ }
235
+
236
+ if (clearedFields.length > 0) {
237
+ await trx(VALUES_TABLE).where('member_id', memberId).whereIn('custom_field_id', clearedFields).del();
238
+ }
239
+
240
+ if (clearedPaths.length > 0) {
241
+ // One statement with a group per field, rather than a statement per field.
242
+ await trx(VALUES_TABLE).where('member_id', memberId).where((builder) => {
243
+ for (const {fieldId, paths} of clearedPaths) {
244
+ builder.orWhere(pair => pair.where('custom_field_id', fieldId).whereIn('path', paths));
245
+ }
246
+ }).del();
247
+ }
248
+
249
+ for (let from = 0; from < rows.length; from += UPSERT_CHUNK) {
250
+ // Typed as the plain row because `merge` takes its columns as `keyof` the
251
+ // builder's record, which for a composite table registration is the scope
252
+ // names rather than the columns.
253
+ await trx<DbLeafRow>(VALUES_TABLE)
254
+ .insert(rows.slice(from, from + UPSERT_CHUNK))
255
+ // Naming the columns rather than giving values takes each from the row
256
+ // that lost the conflict, so every part updates to its own value.
257
+ .onConflict(['member_id', 'custom_field_id', 'path'])
258
+ .merge(['value_text', 'updated_at']);
386
259
  }
387
260
  };
388
261
 
389
- // isTransaction is knex's marker for a transactor: join an existing transaction
390
- // rather than nesting a savepoint under it, otherwise open one.
262
+ // knex's marker for a transactor: join it rather than nesting a savepoint under it.
391
263
  if (executor.isTransaction) {
392
264
  await apply(executor);
393
265
  } else {
@@ -30,7 +30,7 @@ module.exports = class ResourceService {
30
30
  */
31
31
  async getByURL(url) {
32
32
  const path = this.#urlUtils.absoluteToRelative(url.href, {withoutSubdirectory: true});
33
- const resource = await this.#urlService.facade.resolveUrl(path);
33
+ const resource = await this.#urlService.resolveUrl(path);
34
34
  if (resource?.type === 'posts') {
35
35
  return {
36
36
  type: 'post',
@@ -20,7 +20,7 @@ const jobsService = require('../mentions-jobs');
20
20
  // relations it reads for filtered collections (event-emitted models don't
21
21
  // reliably carry them).
22
22
  async function getPostData(post) {
23
- const missing = urlService.facade.getRequiredRelations().filter(relation => !post.relations[relation]);
23
+ const missing = urlService.getRequiredRelations().filter(relation => !post.relations[relation]);
24
24
  if (missing.length) {
25
25
  await post.load(missing);
26
26
  }
@@ -31,7 +31,7 @@ function getPostUrl(id, postData) {
31
31
  const jsonModel = {...postData};
32
32
  // The URL service routes by resource type. Pages and posts share the Post
33
33
  // model, so the page's own type must reach forPost — otherwise it defaults
34
- // to 'posts', matches no post collection, and 404s under the lazy service.
34
+ // to 'posts', matches no post collection, and 404s.
35
35
  const type = postData.type === 'page' ? 'pages' : 'posts';
36
36
  outputSerializerUrlUtil.forPost(id, jsonModel, {options: {}}, type);
37
37
  return jsonModel.url;
@@ -1,11 +1,6 @@
1
1
  const debug = require('@tryghost/debug')('services:route-settings:service');
2
2
  const logging = require('@tryghost/logging');
3
3
  const errors = require('@tryghost/errors');
4
- const tpl = require('@tryghost/tpl');
5
-
6
- const messages = {
7
- loadError: 'Could not load routes.yaml file.'
8
- };
9
4
 
10
5
  function isStoredContentError(err) {
11
6
  return err.errorType === 'ValidationError' || err.errorType === 'IncorrectUsageError';
@@ -25,8 +20,7 @@ class DynamicRoutingService {
25
20
 
26
21
  /**
27
22
  * Wire the storage-layer dependency so the API surface (upload, download)
28
- * works immediately after boot even when the frontend is disabled and
29
- * `start()` is never called.
23
+ * works from early boot, well before `start()` wires up routing.
30
24
  *
31
25
  * @param {object} deps
32
26
  * @param {RouteSettingsStore} deps.store - adapter-manager provided store
@@ -38,11 +32,13 @@ class DynamicRoutingService {
38
32
 
39
33
  /**
40
34
  * Wire the routing dependencies and load route settings into the router.
41
- * Called from initDynamicRouting in boot.js only when the frontend is enabled.
35
+ * Called from initDynamicRouting in boot.js on every boot: the URL service
36
+ * has to learn the routes even where no page is served, so that the APIs and
37
+ * background services can build URLs.
42
38
  *
43
39
  * @param {object} deps
44
40
  * @param {object} deps.routerManager - frontend RouterManager singleton
45
- * @param {object} deps.urlService - UrlServiceFacade
41
+ * @param {object} deps.urlService - the URL service singleton
46
42
  */
47
43
  async start({routerManager, urlService}) {
48
44
  debug('start');
@@ -90,60 +86,14 @@ class DynamicRoutingService {
90
86
  // Parse and validate before anything is persisted — an invalid
91
87
  // upload is rejected here and never reaches the store.
92
88
  const next = parseRouteSettings(parseYaml(yamlContent), yamlContent);
93
- let previous = null;
94
- try {
95
- previous = await this.store.get();
96
- } catch (err) {
97
- if (!isStoredContentError(err)) {
98
- throw err;
99
- }
100
- }
101
89
 
102
90
  await this.store.replace(next);
103
91
 
104
- urlService.resetGenerators({releaseResourcesOnly: true});
105
-
106
- const bringBackValidRoutes = async () => {
107
- urlService.resetGenerators({releaseResourcesOnly: true});
108
-
109
- if (previous) {
110
- await this.store.replace(previous);
111
- }
112
-
113
- return bridge.reloadFrontend(this, urlService);
114
- };
115
-
92
+ // Registration is synchronous inside the reload: RouterManager.start()
93
+ // always mounts the static pages router, which has permalinks, so the
94
+ // URL service has routers again — and reports ready — by the time this
95
+ // resolves. Nothing left to wait for.
116
96
  await bridge.reloadFrontend(this, urlService);
117
-
118
- let tries = 0;
119
-
120
- function isBlogRunning() {
121
- debug('waiting for blog running');
122
- return new Promise((resolve) => {
123
- setTimeout(resolve, 1000);
124
- })
125
- .then(() => {
126
- debug('waited for blog running');
127
- if (!urlService.hasFinished()) {
128
- if (tries > 5) {
129
- throw new errors.InternalServerError({
130
- message: tpl(messages.loadError)
131
- });
132
- }
133
-
134
- tries = tries + 1;
135
- return isBlogRunning();
136
- }
137
- });
138
- }
139
-
140
- return isBlogRunning()
141
- .catch((err) => {
142
- return bringBackValidRoutes()
143
- .finally(() => {
144
- throw err;
145
- });
146
- });
147
97
  }
148
98
  }
149
99
 
@@ -54,7 +54,7 @@ class SlackPingService {
54
54
  const blogTitle = this.settingsCache.get('title');
55
55
  // If this is a post, we want to send the link of the post
56
56
  if (hasPostProperties(post)) {
57
- message = this.urlService.facade.getUrlForResource({ ...post, type: 'posts' }, { absolute: true });
57
+ message = this.urlService.getUrlForResource({ ...post, type: 'posts' }, { absolute: true });
58
58
  title = post.title ? post.title : null;
59
59
  author = post.authors ? post.authors[0] : null;
60
60
  if (post.custom_excerpt) {
@@ -136,7 +136,7 @@ class SlackPingService {
136
136
  fields: [
137
137
  {
138
138
  title: 'Author',
139
- value: author ? `<${this.urlService.facade.getUrlForResource({ ...author, type: 'authors' }, { absolute: true })} | ${author.name}>` : null,
139
+ value: author ? `<${this.urlService.getUrlForResource({ ...author, type: 'authors' }, { absolute: true })} | ${author.name}>` : null,
140
140
  short: true
141
141
  }
142
142
  ],