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.
- package/bin/validate-adapters.js +74 -0
- package/bin/validate-adapters.ts +87 -0
- package/bin/warm-compile-cache.sh +60 -0
- package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
- package/components/tryghost-kg-html-to-lexical-1.3.3.tgz +0 -0
- package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
- package/components/tryghost-parse-email-address-0.0.0.tgz +0 -0
- package/core/app.js +1 -1
- package/core/boot.js +21 -25
- package/core/bridge.js +10 -12
- package/core/built/admin/assets/Combination-BDvCdg1t.js +41 -0
- package/core/built/admin/assets/about-C7O-AgRZ.js +1 -0
- package/core/built/admin/assets/{account-migration-D3O_yVck.js → account-migration-Cv_ol1ko.js} +1 -1
- package/core/built/admin/assets/action-list-lGzZLHc8.js +1 -0
- package/core/built/admin/assets/activitypub/{account-migration-D8FbP-Z-.js → account-migration-DHFbePl6.js} +7 -7
- package/core/built/admin/assets/activitypub/activitypub.js +3 -3
- package/core/built/admin/assets/activitypub/{avatar-11EA1VSs.js → avatar-CT4tS2_S.js} +2 -2
- package/core/built/admin/assets/activitypub/{avatar-flipboard-jfbLG-xe.js → avatar-flipboard-ByYWKipo.js} +2 -2
- package/core/built/admin/assets/activitypub/{bluesky-sharing-f1jTQe2P.js → bluesky-sharing-srVnSHDz.js} +8 -8
- package/core/built/admin/assets/activitypub/{content-formatters-nmTOPY9M.js → content-formatters-C2sos5Ht.js} +3 -3
- package/core/built/admin/assets/activitypub/{copy-BCwo9ep3.js → copy-B1UBTOoZ.js} +2 -2
- package/core/built/admin/assets/activitypub/{dist-BEfX-0aW.js → dist-MPsb2WZ1.js} +4 -4
- package/core/built/admin/assets/activitypub/{domain-AaDlZlrA.js → domain-DzcM9Dzo.js} +9 -9
- package/core/built/admin/assets/activitypub/{edit-profile-DPi8Tl5M.js → edit-profile-CsQ_4xAk.js} +5 -5
- package/core/built/admin/assets/activitypub/{explore-CxTzjILQ.js → explore-CJe0DZBK.js} +8 -8
- package/core/built/admin/assets/activitypub/{feed-DPv5x2MX.js → feed-CKV3qWON.js} +9 -9
- package/core/built/admin/assets/activitypub/{field-DbgA8lat.js → field-DckHfDJf.js} +3 -3
- package/core/built/admin/assets/activitypub/{hash-BWuZoYkA.js → hash-Dlp8RM55.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-35VDQ2K2.js → inbox-CsPy4R0M.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-DSrF_i1O.js → inbox-DAsjzUUe.js} +10 -10
- package/core/built/admin/assets/activitypub/{label-CPjFBbo7.js → label-DWyo9E5T.js} +3 -3
- package/core/built/admin/assets/activitypub/layout-DM_3pqsU.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-BkDC1XVZ.js → moderation-B0Inc03M.js} +7 -7
- package/core/built/admin/assets/activitypub/{note-xEyH4xT-.js → note-CSE54lER.js} +7 -7
- package/core/built/admin/assets/activitypub/{notifications-DClaL3ui.js → notifications-RSwbKqdh.js} +9 -9
- package/core/built/admin/assets/activitypub/{preferences-DpnOa-4Z.js → preferences-CE231n2_.js} +11 -11
- package/core/built/admin/assets/activitypub/{profile-Dvi9mz-6.js → profile-aaQ1j1Eo.js} +13 -13
- package/core/built/admin/assets/activitypub/{reply-Cs9ySd6Z.js → reply-Cjb0Vs3m.js} +2 -2
- package/core/built/admin/assets/activitypub/{routes-DFBeTnfn.js → routes-Djb0mc9k.js} +26 -26
- package/core/built/admin/assets/activitypub/{separator-C5u_VjzY.js → separator-D8pROI5n.js} +3 -3
- package/core/built/admin/assets/activitypub/{settings-CV2fFu6D.js → settings-DMc32IQl.js} +5 -5
- package/core/built/admin/assets/activitypub/{site-_TgDd_EM.js → site-DVIcsohX.js} +4 -4
- package/core/built/admin/assets/activitypub/{step-1-BfnNL2oF.js → step-1-9Yey9usF.js} +5 -5
- package/core/built/admin/assets/activitypub/{step-2-DLndZkO-.js → step-2-DdYmKRXV.js} +6 -6
- package/core/built/admin/assets/activitypub/{step-3-A-PZTtRE.js → step-3-hdSkcjZU.js} +10 -10
- package/core/built/admin/assets/activitypub/{tabs-BUbz7tea.js → tabs-CUqseYzQ.js} +6 -6
- package/core/built/admin/assets/activitypub/{topic-filter-BdX_xYYT.js → topic-filter-Coni6OXM.js} +3 -3
- package/core/built/admin/assets/activitypub/{use-activity-pub-queries-EwuRemHh.js → use-activity-pub-queries-MeHTPMQv.js} +2 -2
- package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-cy4ACBc4.js → use-navigate-with-base-path-B046AM38.js} +24 -7
- package/core/built/admin/assets/activitypub/{use-reply-chain-data-CyFcGfan.js → use-reply-chain-data-1AO-Xob6.js} +5 -5
- package/core/built/admin/assets/activitypub/{x-7zk8mV2p.js → x-BRfRYd16.js} +2 -2
- package/core/built/admin/assets/add-integration-modal-m9Dv6f77.js +1 -0
- package/core/built/admin/assets/add-newsletter-modal-CPSVZL2H.js +1 -0
- package/core/built/admin/assets/add-recommendation-modal-Ci9Z5yat.js +1 -0
- package/core/built/admin/assets/analytics-DqSCVV8U.js +1 -0
- package/core/built/admin/assets/announcement-bar-modal-DU3avV7d.js +1 -0
- package/core/built/admin/assets/api-keys-BK0LylyK.js +1 -0
- package/core/built/admin/assets/api-keys-_iCT4dxr.js +1 -0
- package/core/built/admin/assets/{audience-CkBb2LlO.js → audience-DTJtXXWY.js} +1 -1
- package/core/built/admin/assets/automated-emails-PayPCT8L.js +1 -0
- package/core/built/admin/assets/{automations-DhKeS_U5.js → automations-162q5HjL.js} +1 -1
- package/core/built/admin/assets/{automations-cMllU545.js → automations-dtQ_MRZ7.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-DPGpyD-G.js → bluesky-sharing-B8Es3AuV.js} +1 -1
- package/core/built/admin/assets/brand-icon-09YqFZjV.js +1 -0
- package/core/built/admin/assets/{breadcrumb-DIfcF5ow.js → breadcrumb-tg5zL--z.js} +1 -1
- package/core/built/admin/assets/{chart-helpers-DV0FkdcD.js → chart-helpers-CZElTGYL.js} +1 -1
- package/core/built/admin/assets/checkbox-Cyo-x7mC.js +1 -0
- package/core/built/admin/assets/{chunk.524.87b52f4fada529df2c4c.js → chunk.524.cd5498dd4e195f679266.js} +4 -4
- package/core/built/admin/assets/{chunk.582.340bcca873e806cbd4bb.js → chunk.582.6f92ec1f03734bf2ea3f.js} +6 -6
- package/core/built/admin/assets/{chunk.332.05991a101d67f732dec3.js → chunk.873.b8fc05a8ab877fb819ed.js} +28 -27
- package/core/built/admin/assets/code-editor-view-Dq1rQ4tS.js +1 -0
- package/core/built/admin/assets/color-picker-field-BonBgxhU.js +1 -0
- package/core/built/admin/assets/color-utils-BsrwXuDz.js +1 -0
- package/core/built/admin/assets/combobox-uX5r_paI.js +1 -0
- package/core/built/admin/assets/command-DxwBO2pF.js +1 -0
- package/core/built/admin/assets/comments-Dn-RO-nU.js +1 -0
- package/core/built/admin/assets/components-CwL6a6As-B0E0-NBJ.js +1 -0
- package/core/built/admin/assets/confirmation-modal-B6D6mk6R.js +1 -0
- package/core/built/admin/assets/content-api-modal-BxQ-7T6u.js +1 -0
- package/core/built/admin/assets/copy-field-BAxL4Zqn.js +1 -0
- package/core/built/admin/assets/currency-Dy4Tc2R2.js +1 -0
- package/core/built/admin/assets/custom-integration-modal---d_l7lc.js +1 -0
- package/core/built/admin/assets/{date-range-select-dXc-GsVe.js → date-range-select-PB2n1HZf.js} +1 -1
- package/core/built/admin/assets/debounce-Dg2fW5Rg.js +3 -0
- package/core/built/admin/assets/design-and-theme-modal-DYRT207a.js +23 -0
- package/core/built/admin/assets/dirty-confirm-dialog-BnDJ39ao.js +1 -0
- package/core/built/admin/assets/disabled-sources-indicator-iMHrkyV3.js +1 -0
- package/core/built/admin/assets/dist-B2PvRwLn.js +1 -0
- package/core/built/admin/assets/{dist-pHiOxyxa.js → dist-D5vhMdXf.js} +1 -1
- package/core/built/admin/assets/dist-DlCyLrOC.js +1 -0
- package/core/built/admin/assets/dist-WeOIvSYM.js +23 -0
- package/core/built/admin/assets/{dist-DyViJwIy.js → dist-cFzKLG7y.js} +3 -3
- package/core/built/admin/assets/dist-j1xxxnY8.js +1 -0
- package/core/built/admin/assets/dist-myQaY0ka.js +1 -0
- package/core/built/admin/assets/{domain-B5ogXUBa.js → domain-BR8FIdjD.js} +1 -1
- package/core/built/admin/assets/{dropzone-BnFG7dDP.js → dropzone-hNxmUvwD.js} +1 -1
- package/core/built/admin/assets/{edit-profile-B07jCZy9.js → edit-profile-CENrkY0l.js} +1 -1
- package/core/built/admin/assets/editor-B18D0lm8.js +8 -0
- package/core/built/admin/assets/embed-signup-form-modal-BsCYUMvQ.js +9 -0
- package/core/built/admin/assets/{explore-Ca1y8HW_.js → explore-BmrnljWw.js} +1 -1
- package/core/built/admin/assets/{feed-x4iFMBP4.js → feed-Dy4li6i8.js} +1 -1
- package/core/built/admin/assets/{field-ChCAabZg.js → field-Dh_lwk13.js} +1 -1
- package/core/built/admin/assets/{filter-sources-LdtH1e-T.js → filter-sources-D2zDuJH5.js} +1 -1
- package/core/built/admin/assets/filters-3JBQcNtJ.js +1 -0
- package/core/built/admin/assets/{filters-CjVKRP8U.js → filters-CFt_BDe4.js} +1 -1
- package/core/built/admin/assets/first-promoter-modal-D6Rjxugw.js +1 -0
- package/core/built/admin/assets/format-url-C-t8G6Qd.js +1 -0
- package/core/built/admin/assets/{ghost-7f01131115186d1af84ead76e79646dd.js → ghost-d683fbeb945b13155ea669caa5d8db41.js} +300 -298
- package/core/built/admin/assets/{gift-link-modal-Buqh2vXh.js → gift-link-modal-zMtlJhLQ.js} +1 -1
- package/core/built/admin/assets/{gift-links-CsUhefWG.js → gift-links-CVuqLhsZ.js} +1 -1
- package/core/built/admin/assets/global-data-provider-B949LDXJ.js +1 -0
- package/core/built/admin/assets/grid-CUciBfjf.js +1 -0
- package/core/built/admin/assets/{growth-Dy2UOSay.js → growth-C38Xtp8D.js} +2 -2
- package/core/built/admin/assets/{growth-DajOBdN0.js → growth-CFws67_i.js} +1 -1
- package/core/built/admin/assets/history-modal-ILAOkbxH.js +1 -0
- package/core/built/admin/assets/{hooks-CixO01lL.js → hooks-D6EE7BIt.js} +2 -2
- package/core/built/admin/assets/html-field-9a9Id_2q.js +1 -0
- package/core/built/admin/assets/iframe-buffering-e8vH4-Qn.js +1 -0
- package/core/built/admin/assets/images-CrQKVc9Q.js +1 -0
- package/core/built/admin/assets/{inbox-BUOLDoji.js → inbox-BrSRzyvz.js} +13 -13
- package/core/built/admin/assets/index-BHy2DZX-.js +67 -0
- package/core/built/admin/assets/index-Ds2iUoF6.css +1 -0
- package/core/built/admin/assets/index.es-B9tvf-t4.js +1 -0
- package/core/built/admin/assets/{input-group-uSK3XT7b.js → input-group-6vEPvX9S.js} +1 -1
- package/core/built/admin/assets/integration-header-B4DyXV06.js +1 -0
- package/core/built/admin/assets/integrations-BT6_iJvu.js +1 -0
- package/core/built/admin/assets/invite-user-modal-5xDCEw90.js +1 -0
- package/core/built/admin/assets/isEmail-BOfC647y.js +1 -0
- package/core/built/admin/assets/koenig-editor-base-BWLEB9Vf.js +1 -0
- package/core/built/admin/assets/{koenig-lexical-BFc4yFed.js → koenig-lexical-Cj62r_y9.js} +1 -1
- package/core/built/admin/assets/{kpi-card-BN-E6WYX.js → kpi-card-DOUnkZcN.js} +1 -1
- package/core/built/admin/assets/{kpi-card-CCt-qeWD.js → kpi-card-IHDo8Q56.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-DlxTbTs4.js → kpi-helpers-CjUdECAK.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-BlcUttpn.js → kpi-tabs-X3DkOYbI.js} +1 -1
- package/core/built/admin/assets/{label-AVrxwmsz.js → label-Dq6QaOuS.js} +1 -1
- package/core/built/admin/assets/label-picker-8YlhS2U3.js +1 -0
- package/core/built/admin/assets/{labels-DAIqKed7.js → labels-DAPjKPy2.js} +1 -1
- package/core/built/admin/assets/layout-TMj4P-vv.js +1 -0
- package/core/built/admin/assets/limit-modal-BRPsit_B.js +1 -0
- package/core/built/admin/assets/{link-helpers-D9LsDOkG.js → link-helpers-Bzkk68GD.js} +1 -1
- package/core/built/admin/assets/member-custom-fields-CmvsuuV_.js +1 -0
- package/core/built/admin/assets/member-detail-DCvRrkmJ.js +1 -0
- package/core/built/admin/assets/members-DxbSKngD.js +11 -0
- package/core/built/admin/assets/modal-page-BMTzFHVO.js +1 -0
- package/core/built/admin/assets/{moderation-CQ3urTFE.js → moderation-Da5lhhWh.js} +1 -1
- package/core/built/admin/assets/multi-select-combobox-CYdkH7KQ.js +1 -0
- package/core/built/admin/assets/navigation-modal-BIoTwtMy.js +1 -0
- package/core/built/admin/assets/newsletter-BptlVoJm.js +1 -0
- package/core/built/admin/assets/newsletter-detail-modal-BIvxqE0N.js +1 -0
- package/core/built/admin/assets/newsletter-emails-Bf8IZLsP.js +1 -0
- package/core/built/admin/assets/{newsletters-vt3Ni1Ov.js → newsletters-BgrIZk0V.js} +1 -1
- package/core/built/admin/assets/{newsletters-BXJbJgA-.js → newsletters-Du0lzskS.js} +1 -1
- package/core/built/admin/assets/{note-Dk877BTv.js → note-DYcLOmDb.js} +1 -1
- package/core/built/admin/assets/{notifications-CPlLjPxO.js → notifications-B2SPFIV9.js} +1 -1
- package/core/built/admin/assets/{offers-DEniVRD5.js → offers-Um6ab71O.js} +1 -1
- package/core/built/admin/assets/offers-container-modal-BZnZITMw.js +1 -0
- package/core/built/admin/assets/{onboarding-route-DdStOtjy.js → onboarding-route-DgdfxkQh.js} +1 -1
- package/core/built/admin/assets/overview-BYQJXSog.js +1 -0
- package/core/built/admin/assets/overview-Dt2_mR6U.js +1 -0
- package/core/built/admin/assets/{page-header-pp4dWmOS.js → page-header-CpmxJj5B.js} +1 -1
- package/core/built/admin/assets/{pages-B4goRPUn.js → pages-BVMnpI2O.js} +1 -1
- package/core/built/admin/assets/pintura-modal-B4MuBuuO.js +1 -0
- package/core/built/admin/assets/portal-frame-BF0NBIji.js +1 -0
- package/core/built/admin/assets/portal-modal-DMVbm0oy.js +1 -0
- package/core/built/admin/assets/{post-analytics-BMJrI81R.js → post-analytics-CXWBOGyP.js} +1 -1
- package/core/built/admin/assets/post-analytics-header-ChzjXVPt.js +1 -0
- package/core/built/admin/assets/{post-analytics-provider-Btml5vJI.js → post-analytics-provider-Cgw4U_fw.js} +1 -1
- package/core/built/admin/assets/{post-share-modal-AcUZkbfL.js → post-share-modal-B9Vmk_XM.js} +1 -1
- package/core/built/admin/assets/{posts-DhXE7b6r.js → posts-CxG7v0-M.js} +1 -1
- package/core/built/admin/assets/preferences-BfzJmnsd.js +5 -0
- package/core/built/admin/assets/preview-chrome-BOVO6_PE.js +1 -0
- package/core/built/admin/assets/preview-modal-Dw_HtD6S.js +1 -0
- package/core/built/admin/assets/profile-BT5vtv9b.js +1 -0
- package/core/built/admin/assets/radio-group-I7ou7PoJ.js +1 -0
- package/core/built/admin/assets/{referrers-pmJT4k3-.js → referrers-B9QDAfYm.js} +1 -1
- package/core/built/admin/assets/{router-provider-BmEZf6c6.js → router-provider-DSQfprxH.js} +1 -1
- package/core/built/admin/assets/select-D9oU1IoM.js +1 -0
- package/core/built/admin/assets/settings-CfZFbyu_.js +4 -0
- package/core/built/admin/assets/{settings-BGgk7oCI.js → settings-CpvHWix7.js} +1 -1
- package/core/built/admin/assets/settings-breadcrumbs-K0USP2nw.js +1 -0
- package/core/built/admin/assets/{share-modal-BeuydaBf.js → share-modal-ClIyPzc5.js} +1 -1
- package/core/built/admin/assets/slack-modal-1XXqOfiw.js +1 -0
- package/core/built/admin/assets/social-urls-CHQ9cTAK.js +1 -0
- package/core/built/admin/assets/{sort-button-Df4t2AL8.js → sort-button-CdBsm40v.js} +1 -1
- package/core/built/admin/assets/{source-icon-ajoaKb41.js → source-icon-CEYmVLXR.js} +1 -1
- package/core/built/admin/assets/{stats-C_lfxZAw.js → stats-D8nZCTy5.js} +1 -1
- package/core/built/admin/assets/{stats-view-Cxot1iD8.js → stats-view-CugFVf0g.js} +1 -1
- package/core/built/admin/assets/{step-1-ogAA4lDr.js → step-1-DOYjrORn.js} +1 -1
- package/core/built/admin/assets/{step-2-C5Xini-7.js → step-2-vA6RsarH.js} +1 -1
- package/core/built/admin/assets/{step-3-CZv1FV81.js → step-3-BtIBxMId.js} +2 -2
- package/core/built/admin/assets/stripe-button-UFP4QRXd.js +1 -0
- package/core/built/admin/assets/stripe-connect-modal-C_L3uM6n.js +1 -0
- package/core/built/admin/assets/switch-CE2NuMn-.js +1 -0
- package/core/built/admin/assets/{table-DFj1wiYv.js → table-TMCgCQK7.js} +1 -1
- package/core/built/admin/assets/tabs-Dd6oOKbv.js +1 -0
- package/core/built/admin/assets/tag-detail-D3l-UJPR.js +1 -0
- package/core/built/admin/assets/tags-BwCgmr5v.js +1 -0
- package/core/built/admin/assets/tags-WdKQGWR8.js +1 -0
- package/core/built/admin/assets/testimonials-modal-CqWp-xXJ.js +1 -0
- package/core/built/admin/assets/tier-detail-modal-BkcR2u0A.js +1 -0
- package/core/built/admin/assets/{tiers-DYsa4DCo.js → tiers-Da6kxHQP.js} +1 -1
- package/core/built/admin/assets/toggle-group-Guorw93z.js +1 -0
- package/core/built/admin/assets/{topic-filter-DGDxDalo.js → topic-filter-CbgoWIHn.js} +1 -1
- package/core/built/admin/assets/transistor-modal-DuNnnpal.js +1 -0
- package/core/built/admin/assets/unsplash-modal-y2bVwTHH.js +1 -0
- package/core/built/admin/assets/{use-analytics-data-C76v4LIp.js → use-analytics-data-B-b4wNSC.js} +1 -1
- package/core/built/admin/assets/use-check-theme-limit-error-m09J38Mq.js +2 -0
- package/core/built/admin/assets/use-feature-flag-ud0Qz8vt.js +1 -0
- package/core/built/admin/assets/use-filter-params-BnbBDc7T.js +1 -0
- package/core/built/admin/assets/use-filterable-api-iMpDZBxD.js +1 -0
- package/core/built/admin/assets/use-form-vRjI0Kwk.js +1 -0
- package/core/built/admin/assets/{use-growth-stats-aNXJOmQr.js → use-growth-stats-DPU5rt3W.js} +1 -1
- package/core/built/admin/assets/use-hash-link-navigation-guard-B9j6HIrV.js +1 -0
- package/core/built/admin/assets/use-limiter-BkN3E4gw.js +2 -0
- package/core/built/admin/assets/use-pintura-config-C_z8HTG2.js +1 -0
- package/core/built/admin/assets/use-pintura-editor-DNgbkhGp.js +1 -0
- package/core/built/admin/assets/{use-post-referrers-CG0SZioj.js → use-post-referrers-C2mXyrp-.js} +1 -1
- package/core/built/admin/assets/use-query-params-DHhhu8wi.js +1 -0
- package/core/built/admin/assets/{use-reply-chain-data-RLdPKRmS.js → use-reply-chain-data-DaK8q2qQ.js} +1 -1
- package/core/built/admin/assets/use-setting-group-YlYcoNyP.js +1 -0
- package/core/built/admin/assets/use-settings-navigation-Bf1g1rEh.js +1 -0
- package/core/built/admin/assets/{use-simple-pagination-DgcF69KI.js → use-simple-pagination-D9ckKo5-.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-query-C1R_Kbmy.js → use-tinybird-query-GSa32hVw.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-C9fPjVM_.js → use-tinybird-token-CKLOZFsw.js} +1 -1
- package/core/built/admin/assets/use-url-input-z-p0Kewm.js +5 -0
- package/core/built/admin/assets/user-detail-modal-BMxOA4Sv.js +1 -0
- package/core/built/admin/assets/{users-Dkv8NQZe.js → users-B6hIds6n.js} +1 -1
- package/core/built/admin/assets/validator-Cr0sMKuW.js +7 -0
- package/core/built/admin/assets/{virtual-list-BmTUzeoF.js → virtual-list-RHeEL05L.js} +1 -1
- package/core/built/admin/assets/web-Bw2QMO5u.js +2 -0
- package/core/built/admin/assets/web-xJtcwMy4.js +1 -0
- package/core/built/admin/assets/zapier-modal-BP2f4-vY.js +1 -0
- package/core/built/admin/index.html +15 -13
- package/core/frontend/helpers/authors.js +1 -1
- package/core/frontend/helpers/link.js +3 -8
- package/core/frontend/helpers/tags.js +1 -1
- package/core/frontend/meta/author-url.js +2 -2
- package/core/frontend/meta/url.js +4 -4
- package/core/frontend/services/proxy.js +1 -1
- package/core/frontend/services/routing/events.js +20 -6
- package/core/frontend/services/routing/permalink-adapter.js +0 -13
- package/core/frontend/services/routing/permalink-adapter.ts +0 -13
- package/core/frontend/services/routing/router-manager.js +15 -9
- package/core/frontend/services/sitemap/base-site-map-generator.js +1 -28
- package/core/frontend/services/sitemap/site-map-manager.js +22 -57
- package/core/server/adapters/route-settings/utils.js +10 -2
- package/core/server/adapters/route-settings/utils.ts +12 -2
- package/core/server/api/endpoints/email-post.js +1 -1
- package/core/server/api/endpoints/previews.js +1 -1
- package/core/server/api/endpoints/search-index-public.js +3 -3
- package/core/server/api/endpoints/search-index.js +4 -4
- package/core/server/api/endpoints/utils/serializers/input/authors.js +2 -2
- package/core/server/api/endpoints/utils/serializers/input/comments.js +1 -1
- package/core/server/api/endpoints/utils/serializers/input/pages.js +3 -3
- package/core/server/api/endpoints/utils/serializers/input/posts.js +3 -3
- package/core/server/api/endpoints/utils/serializers/input/tags.js +1 -1
- package/core/server/api/endpoints/utils/serializers/input/users.js +2 -2
- package/core/server/api/endpoints/utils/serializers/input/utils/url.js +31 -13
- package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +2 -2
- package/core/server/api/endpoints/utils/serializers/output/mappers/tags.js +2 -2
- package/core/server/api/endpoints/utils/serializers/output/mappers/users.js +8 -0
- package/core/server/api/endpoints/utils/serializers/output/utils/url.js +18 -11
- package/core/server/data/migrations/versions/6.57/2026-08-04-18-00-00-store-custom-field-values-as-leaf-rows.js +74 -0
- package/core/server/data/migrations/versions/6.57/2026-08-04-18-54-14-reset-automation-email-analytics.js +25 -0
- package/core/server/data/schema/commands.js +21 -5
- package/core/server/data/schema/schema.js +24 -5
- package/core/server/lib/common/to-plain.ts +1 -1
- package/core/server/models/post.js +2 -2
- package/core/server/services/adapter-manager/adapter-manager.js +1 -45
- package/core/server/services/adapter-manager/adapter-manager.ts +2 -48
- package/core/server/services/adapter-manager/adapter-paths.js +24 -0
- package/core/server/services/adapter-manager/adapter-paths.ts +21 -0
- package/core/server/services/adapter-manager/base-classes.js +22 -0
- package/core/server/services/adapter-manager/base-classes.ts +22 -0
- package/core/server/services/adapter-manager/index.js +4 -27
- package/core/server/services/adapter-manager/index.ts +4 -31
- package/core/server/services/adapter-manager/utils.js +50 -0
- package/core/server/services/adapter-manager/utils.ts +57 -0
- package/core/server/services/audience-feedback/audience-feedback-controller.js +1 -1
- package/core/server/services/audience-feedback/audience-feedback-service.js +2 -2
- package/core/server/services/automations/service.js +2 -2
- package/core/server/services/automations/service.ts +2 -2
- package/core/server/services/comments/comments-service-emails.js +1 -1
- package/core/server/services/comments/comments-service.js +1 -1
- package/core/server/services/email-analytics/email-analytics-service-wrapper.js +11 -2
- package/core/server/services/email-analytics/email-analytics-service.js +1 -1
- package/core/server/services/email-service/email-service-wrapper.js +2 -1
- package/core/server/services/email-service/mailgun-email-provider.js +15 -0
- package/core/server/services/indexnow-ping/indexnow-ping-service.js +1 -1
- package/core/server/services/indexnow-ping/indexnow-ping-service.ts +2 -4
- package/core/server/services/lib/mailgun-client.js +3 -3
- package/core/server/services/media-inliner/external-media-inliner.js +12 -8
- package/core/server/services/member-attribution/url-translator.js +5 -7
- package/core/server/services/member-welcome-emails/service.js +72 -12
- package/core/server/services/members/api.js +1 -1
- package/core/server/services/members/import-export/import/completion-email.js +8 -4
- package/core/server/services/members/import-export/import/completion-email.ts +9 -4
- package/core/server/services/members/import-export/import/importer.js +25 -4
- package/core/server/services/members/import-export/import/importer.ts +26 -6
- package/core/server/services/members/import-export/import/row.ts +5 -1
- package/core/server/services/members/import-export/index.js +1 -5
- package/core/server/services/members/import-export/index.ts +2 -6
- package/core/server/services/members-custom-fields/queries.js +4 -13
- package/core/server/services/members-custom-fields/queries.ts +4 -13
- package/core/server/services/members-custom-fields/schema.js +18 -27
- package/core/server/services/members-custom-fields/schema.ts +17 -27
- package/core/server/services/members-custom-fields/storage.js +94 -27
- package/core/server/services/members-custom-fields/storage.ts +114 -39
- package/core/server/services/members-custom-fields/values-service.js +98 -219
- package/core/server/services/members-custom-fields/values-service.ts +110 -238
- package/core/server/services/mentions/resource-service.js +1 -1
- package/core/server/services/mentions/service.js +2 -2
- package/core/server/services/route-settings/dynamic-routing-service.js +9 -59
- package/core/server/services/slack-ping/slack-ping-service.js +2 -2
- package/core/server/services/slack-ping/slack-ping-service.ts +3 -5
- package/core/server/services/stats/content-stats-service.js +3 -3
- package/core/server/services/stats/posts-stats-service.js +2 -2
- package/core/server/services/url/config.js +90 -135
- package/core/server/services/url/index.js +6 -49
- package/core/server/services/url/lazy-find-resource.js +14 -11
- package/core/server/services/url/lazy-find-resource.ts +14 -11
- package/core/server/services/url/lazy-url-service.js +174 -94
- package/core/server/services/url/lazy-url-service.ts +233 -97
- package/core/server/services/url/routable-resources.js +62 -71
- package/core/server/services/url/router-filter.js +12 -4
- package/core/server/services/url/router-filter.ts +12 -4
- package/core/server/services/webhooks/index.js +1 -1
- package/core/server/services/webhooks/serialize.js +2 -1
- package/core/shared/config/env/config.testing-mysql.json +1 -2
- package/core/shared/config/env/config.testing.json +1 -2
- package/core/shared/events/index.js +1 -2
- package/core/shared/labs.js +3 -3
- package/package.json +5 -6
- package/pnpm-lock.yaml +6 -9
- package/pnpm-workspace.yaml +2 -2
- package/tsconfig.json +1 -0
- package/core/built/admin/assets/Combination-BCm9FL4o.js +0 -41
- package/core/built/admin/assets/activitypub/layout-CTgrezN7.js +0 -1
- package/core/built/admin/assets/automated-emails-6t2bIRWf.js +0 -7
- package/core/built/admin/assets/checkbox-BIl077a0.js +0 -1
- package/core/built/admin/assets/code-editor-view-C0aWNwdl.js +0 -1
- package/core/built/admin/assets/comments-qwhIjH3c.js +0 -1
- package/core/built/admin/assets/disabled-sources-indicator-DYAXW-6V.js +0 -1
- package/core/built/admin/assets/dist-BUKmD51u.js +0 -1
- package/core/built/admin/assets/dist-CNlMj7ML.js +0 -1
- package/core/built/admin/assets/dist-DDY6GRVz.js +0 -1
- package/core/built/admin/assets/dist-DG78yroX.js +0 -23
- package/core/built/admin/assets/editor-DDYSOXGN.js +0 -8
- package/core/built/admin/assets/esm-CFX93H5M.js +0 -2
- package/core/built/admin/assets/filters-CLehPo_D.js +0 -1
- package/core/built/admin/assets/index-08n05-rc.js +0 -67
- package/core/built/admin/assets/index-DgUzgFvS.css +0 -1
- package/core/built/admin/assets/label-picker-DVnpu_IF.js +0 -1
- package/core/built/admin/assets/layout-B0YLh7R9.js +0 -1
- package/core/built/admin/assets/member-custom-fields-C8NpZYza.js +0 -1
- package/core/built/admin/assets/member-detail-CIVAdKXf.js +0 -1
- package/core/built/admin/assets/members-oLPiSkXD.js +0 -10
- package/core/built/admin/assets/modals-CB7v-k3T.js +0 -31
- package/core/built/admin/assets/newsletter-rbeDX9Wk.js +0 -1
- package/core/built/admin/assets/overview--4V-kx9c.js +0 -1
- package/core/built/admin/assets/overview-DSLv30oz.js +0 -1
- package/core/built/admin/assets/post-analytics-header-pU2m9ZuV.js +0 -1
- package/core/built/admin/assets/preferences-Ru4CRPHC.js +0 -5
- package/core/built/admin/assets/profile-BNb1oj7D.js +0 -1
- package/core/built/admin/assets/select-B0HteHrp.js +0 -1
- package/core/built/admin/assets/settings-B1C9Q3wo.js +0 -4
- package/core/built/admin/assets/social-urls-DA2lXrhW.js +0 -8
- package/core/built/admin/assets/switch-CyQcwMAv.js +0 -1
- package/core/built/admin/assets/tabs-D4y1gqUd.js +0 -1
- package/core/built/admin/assets/tags-Dmft-Y4O.js +0 -1
- package/core/built/admin/assets/toggle-group-R2QMFvXW.js +0 -1
- package/core/built/admin/assets/use-filter-params-WrdhAISv.js +0 -1
- package/core/built/admin/assets/web-BqW0b0_C.js +0 -1
- package/core/built/admin/assets/web-Cy5GKMWM.js +0 -2
- package/core/server/services/url/local-file-cache.js +0 -75
- package/core/server/services/url/queue.js +0 -260
- package/core/server/services/url/resource.js +0 -102
- package/core/server/services/url/resources.js +0 -480
- package/core/server/services/url/url-generator.js +0 -276
- package/core/server/services/url/url-service-facade.js +0 -366
- package/core/server/services/url/url-service-facade.ts +0 -519
- package/core/server/services/url/url-service.js +0 -387
- package/core/server/services/url/urls.js +0 -140
- package/core/shared/events/url-resource-updated-event.js +0 -33
- /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 {
|
|
7
|
+
import {DbCustomFieldLeaf, DbCustomFieldValue, FIELD_STATUS} from './schema';
|
|
8
8
|
import {activeFields} from './queries';
|
|
9
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
|
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
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
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
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
165
|
-
*
|
|
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,
|
|
181
|
-
*
|
|
182
|
-
*
|
|
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
|
-
//
|
|
191
|
-
//
|
|
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
|
|
209
|
-
//
|
|
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
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
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
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
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:
|
|
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
|
-
*
|
|
324
|
-
* that
|
|
325
|
-
*
|
|
326
|
-
*
|
|
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
|
|
342
|
-
*
|
|
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 =
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
216
|
+
clearedFields.push(field.id);
|
|
365
217
|
continue;
|
|
366
218
|
}
|
|
367
219
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
:
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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 -
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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.
|
|
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.
|
|
139
|
+
value: author ? `<${this.urlService.getUrlForResource({ ...author, type: 'authors' }, { absolute: true })} | ${author.name}>` : null,
|
|
140
140
|
short: true
|
|
141
141
|
}
|
|
142
142
|
],
|