@shapio/cms 0.3.1 → 0.4.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/dist/admin/assets/AssetTile-C-Mu0BRF.js +2 -0
- package/dist/admin/assets/AssetTile-C-Mu0BRF.js.map +1 -0
- package/dist/admin/assets/{AssistError-BYR085vj.js → AssistError-B-bnaHhJ.js} +2 -2
- package/dist/admin/assets/{AssistError-BYR085vj.js.map → AssistError-B-bnaHhJ.js.map} +1 -1
- package/dist/admin/assets/{BlockMenuItems-BnLF3Efc.js → BlockMenuItems-Dn9WULjL.js} +3 -3
- package/dist/admin/assets/{BlockMenuItems-BnLF3Efc.js.map → BlockMenuItems-Dn9WULjL.js.map} +1 -1
- package/dist/admin/assets/Body-C0SwduLi.js +2 -0
- package/dist/admin/assets/{Body-CCOZq2q9.js.map → Body-C0SwduLi.js.map} +1 -1
- package/dist/admin/assets/Builder-C11yUgu9.js +2 -0
- package/dist/admin/assets/Builder-C11yUgu9.js.map +1 -0
- package/dist/admin/assets/{ConfirmDialog-Tu-gjEwm.js → ConfirmDialog-BfpN2MQx.js} +2 -2
- package/dist/admin/assets/{ConfirmDialog-Tu-gjEwm.js.map → ConfirmDialog-BfpN2MQx.js.map} +1 -1
- package/dist/admin/assets/{CopyButton-BuBaIlCk.js → CopyButton-Dxbzp7hC.js} +2 -2
- package/dist/admin/assets/{CopyButton-BuBaIlCk.js.map → CopyButton-Dxbzp7hC.js.map} +1 -1
- package/dist/admin/assets/{CursorPager-Bvu3CXpD.js → CursorPager-DkZs6rPg.js} +2 -2
- package/dist/admin/assets/{CursorPager-Bvu3CXpD.js.map → CursorPager-DkZs6rPg.js.map} +1 -1
- package/dist/admin/assets/{ExternalLink-CCr5oRXI.js → ExternalLink-KdO72pTj.js} +2 -2
- package/dist/admin/assets/{ExternalLink-CCr5oRXI.js.map → ExternalLink-KdO72pTj.js.map} +1 -1
- package/dist/admin/assets/FocalPoint-r1MG_y-r.js +2 -0
- package/dist/admin/assets/FocalPoint-r1MG_y-r.js.map +1 -0
- package/dist/admin/assets/Form-D8vp4Kd_.js +2 -0
- package/dist/admin/assets/{Form-Doc0JEzF.js.map → Form-D8vp4Kd_.js.map} +1 -1
- package/dist/admin/assets/{FormCheckboxGroup-eg39IJvf.js → FormCheckboxGroup-8ObPh9cH.js} +2 -2
- package/dist/admin/assets/{FormCheckboxGroup-eg39IJvf.js.map → FormCheckboxGroup-8ObPh9cH.js.map} +1 -1
- package/dist/admin/assets/{FormRadioGroup-ZppSTZK6.js → FormRadioGroup-CAB3qzVS.js} +2 -2
- package/dist/admin/assets/{FormRadioGroup-ZppSTZK6.js.map → FormRadioGroup-CAB3qzVS.js.map} +1 -1
- package/dist/admin/assets/{FormSelectField-Cosnxiz-.js → FormSelectField-C7hE05Wq.js} +2 -2
- package/dist/admin/assets/{FormSelectField-Cosnxiz-.js.map → FormSelectField-C7hE05Wq.js.map} +1 -1
- package/dist/admin/assets/{FormSwitchField-DsKQEZw6.js → FormSwitchField-7EEYkGOH.js} +2 -2
- package/dist/admin/assets/{FormSwitchField-DsKQEZw6.js.map → FormSwitchField-7EEYkGOH.js.map} +1 -1
- package/dist/admin/assets/{FormTextareaField-BXidSgIE.js → FormTextareaField-BasuRoXW.js} +2 -2
- package/dist/admin/assets/{FormTextareaField-BXidSgIE.js.map → FormTextareaField-BasuRoXW.js.map} +1 -1
- package/dist/admin/assets/{HintedLabel-USQku9Yt.js → HintedLabel-BDV81kIT.js} +2 -2
- package/dist/admin/assets/{HintedLabel-USQku9Yt.js.map → HintedLabel-BDV81kIT.js.map} +1 -1
- package/dist/admin/assets/{InfoHint-Bz_yvehr.js → InfoHint-DWn9kLf1.js} +2 -2
- package/dist/admin/assets/{InfoHint-Bz_yvehr.js.map → InfoHint-DWn9kLf1.js.map} +1 -1
- package/dist/admin/assets/InsertMenuItem-DhJk5BeH.js +2 -0
- package/dist/admin/assets/InsertMenuItem-DhJk5BeH.js.map +1 -0
- package/dist/admin/assets/{LinkTabs-D4U-rJyK.js → LinkTabs-stM3j4aH.js} +2 -2
- package/dist/admin/assets/{LinkTabs-D4U-rJyK.js.map → LinkTabs-stM3j4aH.js.map} +1 -1
- package/dist/admin/assets/{Match-UAw1Ss8a.js → Match-Zu4pwfCC.js} +2 -2
- package/dist/admin/assets/{Match-UAw1Ss8a.js.map → Match-Zu4pwfCC.js.map} +1 -1
- package/dist/admin/assets/{Panel-BdjqCkKs.js → Panel-ClY9b33D.js} +2 -2
- package/dist/admin/assets/{Panel-BdjqCkKs.js.map → Panel-ClY9b33D.js.map} +1 -1
- package/dist/admin/assets/{QueryView-Bkyizh1P.js → QueryView-Cu-KPtQI.js} +2 -2
- package/dist/admin/assets/{QueryView-Bkyizh1P.js.map → QueryView-Cu-KPtQI.js.map} +1 -1
- package/dist/admin/assets/{RadioTile-VHTPl88w.js → RadioTile-Ci0LucKI.js} +2 -2
- package/dist/admin/assets/{RadioTile-VHTPl88w.js.map → RadioTile-Ci0LucKI.js.map} +1 -1
- package/dist/admin/assets/RichTextField-BcYOFkeW.js +144 -0
- package/dist/admin/assets/{RichTextField-B21FqrT5.js.map → RichTextField-BcYOFkeW.js.map} +1 -1
- package/dist/admin/assets/{RowTitle-BD8QPs_v.js → RowTitle-BdDgA5Lb.js} +2 -2
- package/dist/admin/assets/{RowTitle-BD8QPs_v.js.map → RowTitle-BdDgA5Lb.js.map} +1 -1
- package/dist/admin/assets/{SecretReveal-BfJKUxXW.js → SecretReveal-ai1uMW_b.js} +2 -2
- package/dist/admin/assets/{SecretReveal-BfJKUxXW.js.map → SecretReveal-ai1uMW_b.js.map} +1 -1
- package/dist/admin/assets/{SharedGlyph-DDPdvsm_.js → SharedGlyph-y6ZsA_gz.js} +2 -2
- package/dist/admin/assets/{SharedGlyph-DDPdvsm_.js.map → SharedGlyph-y6ZsA_gz.js.map} +1 -1
- package/dist/admin/assets/{StepList-DKhkw1hN.js → StepList-xmD9CPZl.js} +2 -2
- package/dist/admin/assets/{StepList-DKhkw1hN.js.map → StepList-xmD9CPZl.js.map} +1 -1
- package/dist/admin/assets/Structure-DGt-TDk5.js +2 -0
- package/dist/admin/assets/{Structure-C7dr00Bp.js.map → Structure-DGt-TDk5.js.map} +1 -1
- package/dist/admin/assets/{SubNavLink-BAF2-wuF.js → SubNavLink-YjjaDN40.js} +2 -2
- package/dist/admin/assets/{SubNavLink-BAF2-wuF.js.map → SubNavLink-YjjaDN40.js.map} +1 -1
- package/dist/admin/assets/{SuggestAltButton-DEsbHvs4.js → SuggestAltButton-D1-v_0wB.js} +2 -2
- package/dist/admin/assets/{SuggestAltButton-DEsbHvs4.js.map → SuggestAltButton-D1-v_0wB.js.map} +1 -1
- package/dist/admin/assets/{TextLink-CegO3jom.js → TextLink-DIqr5zkJ.js} +2 -2
- package/dist/admin/assets/{TextLink-CegO3jom.js.map → TextLink-DIqr5zkJ.js.map} +1 -1
- package/dist/admin/assets/Thumbnail-cjA7fSWh.js +2 -0
- package/dist/admin/assets/Thumbnail-cjA7fSWh.js.map +1 -0
- package/dist/admin/assets/{UnsavedChangesGuard-Cbfg-Bgj.js → UnsavedChangesGuard-B57D0278.js} +2 -2
- package/dist/admin/assets/{UnsavedChangesGuard-Cbfg-Bgj.js.map → UnsavedChangesGuard-B57D0278.js.map} +1 -1
- package/dist/admin/assets/{UserAvatar-CiZ00-cr.js → UserAvatar--pMv2i3c.js} +2 -2
- package/dist/admin/assets/{UserAvatar-CiZ00-cr.js.map → UserAvatar--pMv2i3c.js.map} +1 -1
- package/dist/admin/assets/{auth-BaVAo0u_.js → auth-YVfK3SxF.js} +2 -2
- package/dist/admin/assets/{auth-BaVAo0u_.js.map → auth-YVfK3SxF.js.map} +1 -1
- package/dist/admin/assets/{badge-IBeVOhKd.js → badge-C5oMRtwJ.js} +2 -2
- package/dist/admin/assets/{badge-IBeVOhKd.js.map → badge-C5oMRtwJ.js.map} +1 -1
- package/dist/admin/assets/{boxes-BH0MDegO.js → boxes-DRvd25_u.js} +2 -2
- package/dist/admin/assets/{boxes-BH0MDegO.js.map → boxes-DRvd25_u.js.map} +1 -1
- package/dist/admin/assets/{calendar-clock-DKUk-w81.js → calendar-clock-VENf4s1R.js} +2 -2
- package/dist/admin/assets/{calendar-clock-DKUk-w81.js.map → calendar-clock-VENf4s1R.js.map} +1 -1
- package/dist/admin/assets/{changeSets-o_G67qPk.js → changeSets-CObZV8H2.js} +2 -2
- package/dist/admin/assets/{changeSets-o_G67qPk.js.map → changeSets-CObZV8H2.js.map} +1 -1
- package/dist/admin/assets/chevron-left-D2DnQyeV.js +2 -0
- package/dist/admin/assets/{chevron-left-D259hWJn.js.map → chevron-left-D2DnQyeV.js.map} +1 -1
- package/dist/admin/assets/{circle-check-Cidzt1Ww.js → circle-check-jmRQz1Jq.js} +2 -2
- package/dist/admin/assets/{circle-check-Cidzt1Ww.js.map → circle-check-jmRQz1Jq.js.map} +1 -1
- package/dist/admin/assets/{clock-BOtPNp9-.js → clock-DrSTMGjp.js} +2 -2
- package/dist/admin/assets/{clock-BOtPNp9-.js.map → clock-DrSTMGjp.js.map} +1 -1
- package/dist/admin/assets/cn-CL1YHRrS.js +2 -0
- package/dist/admin/assets/cn-CL1YHRrS.js.map +1 -0
- package/dist/admin/assets/coerce-B4MJTGg1.js +2 -0
- package/dist/admin/assets/{coerce-DWI5sgLW.js.map → coerce-B4MJTGg1.js.map} +1 -1
- package/dist/admin/assets/constants-DT4WPUvz.js +2 -0
- package/dist/admin/assets/{constants-CgUdkWmf.js.map → constants-DT4WPUvz.js.map} +1 -1
- package/dist/admin/assets/content-HvisCB5m.js +3 -0
- package/dist/admin/assets/content-HvisCB5m.js.map +1 -0
- package/dist/admin/assets/{contentCounts-T-LmvUf5.js → contentCounts-DzIvdeXR.js} +2 -2
- package/dist/admin/assets/{contentCounts-T-LmvUf5.js.map → contentCounts-DzIvdeXR.js.map} +1 -1
- package/dist/admin/assets/{contentTypes-BSlp81l1.js → contentTypes-ArLEfM29.js} +2 -2
- package/dist/admin/assets/{contentTypes-BSlp81l1.js.map → contentTypes-ArLEfM29.js.map} +1 -1
- package/dist/admin/assets/context-4Jl20mYc.js +2 -0
- package/dist/admin/assets/context-4Jl20mYc.js.map +1 -0
- package/dist/admin/assets/{copy-DT7dAzVm.js → copy-t6kAFw-O.js} +2 -2
- package/dist/admin/assets/{copy-DT7dAzVm.js.map → copy-t6kAFw-O.js.map} +1 -1
- package/dist/admin/assets/{dateTimeLocal-DRq7DuYO.js → dateTimeLocal-IDUOKulz.js} +2 -2
- package/dist/admin/assets/{dateTimeLocal-DRq7DuYO.js.map → dateTimeLocal-IDUOKulz.js.map} +1 -1
- package/dist/admin/assets/develop-DvKYqVeK.js +11 -0
- package/dist/admin/assets/develop-DvKYqVeK.js.map +1 -0
- package/dist/admin/assets/dropdown-menu-DPzDthV7.js +2 -0
- package/dist/admin/assets/dropdown-menu-DPzDthV7.js.map +1 -0
- package/dist/admin/assets/{ellipsis-xDmHl1SK.js → ellipsis-DEHUaCkn.js} +2 -2
- package/dist/admin/assets/{ellipsis-xDmHl1SK.js.map → ellipsis-DEHUaCkn.js.map} +1 -1
- package/dist/admin/assets/external-link-B8UFREQ_.js +2 -0
- package/dist/admin/assets/external-link-B8UFREQ_.js.map +1 -0
- package/dist/admin/assets/{fileRoute-Buucm0CU.js → fileRoute-sJvv4Ipn.js} +2 -2
- package/dist/admin/assets/{fileRoute-Buucm0CU.js.map → fileRoute-sJvv4Ipn.js.map} +1 -1
- package/dist/admin/assets/folder-plus-iDg1hhew.js +2 -0
- package/dist/admin/assets/folder-plus-iDg1hhew.js.map +1 -0
- package/dist/admin/assets/followPlural-tOh6gDgT.js +2 -0
- package/dist/admin/assets/{followPlural-Cuocm4Sv.js.map → followPlural-tOh6gDgT.js.map} +1 -1
- package/dist/admin/assets/{globe-CAhrwBgv.js → globe-TooNSp9V.js} +2 -2
- package/dist/admin/assets/{globe-CAhrwBgv.js.map → globe-TooNSp9V.js.map} +1 -1
- package/dist/admin/assets/{image-CGN6ffEu.js → image-CXZXXH58.js} +2 -2
- package/dist/admin/assets/{image-CGN6ffEu.js.map → image-CXZXXH58.js.map} +1 -1
- package/dist/admin/assets/{image-off-0zX_kODH.js → image-off-cxva5G-Y.js} +2 -2
- package/dist/admin/assets/{image-off-0zX_kODH.js.map → image-off-cxva5G-Y.js.map} +1 -1
- package/dist/admin/assets/{inbox-CukkApHK.js → inbox-B8c9Kj_z.js} +2 -2
- package/dist/admin/assets/{inbox-CukkApHK.js.map → inbox-B8c9Kj_z.js.map} +1 -1
- package/dist/admin/assets/{inbox-CWq1hHqK.js → inbox-nfR2K7f6.js} +2 -2
- package/dist/admin/assets/{inbox-CWq1hHqK.js.map → inbox-nfR2K7f6.js.map} +1 -1
- package/dist/admin/assets/index-Bbb2ER3l.css +2 -0
- package/dist/admin/assets/index-C6FrF34G.js +4 -0
- package/dist/admin/assets/index-C6FrF34G.js.map +1 -0
- package/dist/admin/assets/{key-round-DnE-2gr0.js → key-round-CFJ61iCd.js} +2 -2
- package/dist/admin/assets/{key-round-DnE-2gr0.js.map → key-round-CFJ61iCd.js.map} +1 -1
- package/dist/admin/assets/languages-eh7U4anI.js +2 -0
- package/dist/admin/assets/languages-eh7U4anI.js.map +1 -0
- package/dist/admin/assets/layers-mGfmMsKE.js +2 -0
- package/dist/admin/assets/layers-mGfmMsKE.js.map +1 -0
- package/dist/admin/assets/link-2-off-DbZzRN_w.js +2 -0
- package/dist/admin/assets/link-2-off-DbZzRN_w.js.map +1 -0
- package/dist/admin/assets/{link-DPniuk00.js → link-b2pFaN-A.js} +2 -2
- package/dist/admin/assets/{link-DPniuk00.js.map → link-b2pFaN-A.js.map} +1 -1
- package/dist/admin/assets/{locales-DFLQ17ow.js → locales-oBD0q1pN.js} +2 -2
- package/dist/admin/assets/{locales-DFLQ17ow.js.map → locales-oBD0q1pN.js.map} +1 -1
- package/dist/admin/assets/{media-CKTs3j83.js → media-5ks_0Rre.js} +2 -2
- package/dist/admin/assets/{media-CKTs3j83.js.map → media-5ks_0Rre.js.map} +1 -1
- package/dist/admin/assets/media-BzivlEh2.js +2 -0
- package/dist/admin/assets/{media-CUfV74JZ.js.map → media-BzivlEh2.js.map} +1 -1
- package/dist/admin/assets/network-Cj04CpFi.js +2 -0
- package/dist/admin/assets/network-Cj04CpFi.js.map +1 -0
- package/dist/admin/assets/{pencil-BqAgKC9v.js → pencil-C7eD5mlZ.js} +2 -2
- package/dist/admin/assets/{pencil-BqAgKC9v.js.map → pencil-C7eD5mlZ.js.map} +1 -1
- package/dist/admin/assets/{plus-DLRcpVhJ.js → plus-BDZHQBlU.js} +2 -2
- package/dist/admin/assets/{plus-DLRcpVhJ.js.map → plus-BDZHQBlU.js.map} +1 -1
- package/dist/admin/assets/{preload-helper-BHNBIF88.js → preload-helper-CJNJ4S3k.js} +2 -2
- package/dist/admin/assets/{preload-helper-BHNBIF88.js.map → preload-helper-CJNJ4S3k.js.map} +1 -1
- package/dist/admin/assets/publishing-ChHkrwvr.js +2 -0
- package/dist/admin/assets/{publishing-DaaE8PaU.js.map → publishing-ChHkrwvr.js.map} +1 -1
- package/dist/admin/assets/{radio-group-CA3XlQcc.js → radio-group-DSK5LPaM.js} +2 -2
- package/dist/admin/assets/{radio-group-CA3XlQcc.js.map → radio-group-DSK5LPaM.js.map} +1 -1
- package/dist/admin/assets/{rotate-ccw-CfrkcgYa.js → rotate-ccw-DCWwb4Ga.js} +2 -2
- package/dist/admin/assets/{rotate-ccw-CfrkcgYa.js.map → rotate-ccw-DCWwb4Ga.js.map} +1 -1
- package/dist/admin/assets/{schedules-hJQkjEhN.js → schedules-DXrBSypK.js} +2 -2
- package/dist/admin/assets/{schedules-hJQkjEhN.js.map → schedules-DXrBSypK.js.map} +1 -1
- package/dist/admin/assets/{schema--xSR7cf9.js → schema-B7L9OJrX.js} +2 -2
- package/dist/admin/assets/{schema--xSR7cf9.js.map → schema-B7L9OJrX.js.map} +1 -1
- package/dist/admin/assets/{scroll-text-BA9yh7Za.js → scroll-text-BLThe4Ki.js} +2 -2
- package/dist/admin/assets/{scroll-text-BA9yh7Za.js.map → scroll-text-BLThe4Ki.js.map} +1 -1
- package/dist/admin/assets/{file-text-BJkQMeQO.js → search-CcJQqtRw.js} +2 -2
- package/dist/admin/assets/search-CcJQqtRw.js.map +1 -0
- package/dist/admin/assets/{searchSchemas-C-nQbyhd.js → searchSchemas-BdwAFaVO.js} +2 -2
- package/dist/admin/assets/{searchSchemas-C-nQbyhd.js.map → searchSchemas-BdwAFaVO.js.map} +1 -1
- package/dist/admin/assets/{sections-D0OZozD2.js → sections-TIN_Nl1b.js} +2 -2
- package/dist/admin/assets/{sections-D0OZozD2.js.map → sections-TIN_Nl1b.js.map} +1 -1
- package/dist/admin/assets/sections-iNOzo7pL.js +2 -0
- package/dist/admin/assets/sections-iNOzo7pL.js.map +1 -0
- package/dist/admin/assets/{select-De48SQdZ.js → select-DyD7qIkJ.js} +2 -2
- package/dist/admin/assets/{select-De48SQdZ.js.map → select-DyD7qIkJ.js.map} +1 -1
- package/dist/admin/assets/{send-NuSmLVSD.js → send-CbP0Rc9_.js} +2 -2
- package/dist/admin/assets/{send-NuSmLVSD.js.map → send-CbP0Rc9_.js.map} +1 -1
- package/dist/admin/assets/seo-uMm2fGPB.js +2 -0
- package/dist/admin/assets/seo-uMm2fGPB.js.map +1 -0
- package/dist/admin/assets/settings-CJVORrIV.js +2 -0
- package/dist/admin/assets/settings-CJVORrIV.js.map +1 -0
- package/dist/admin/assets/skeleton-B5GJgv7y.js +42 -0
- package/dist/admin/assets/skeleton-B5GJgv7y.js.map +1 -0
- package/dist/admin/assets/snapshots-DkyyrWX3.js +2 -0
- package/dist/admin/assets/{snapshots-BQeFK5cj.js.map → snapshots-DkyyrWX3.js.map} +1 -1
- package/dist/admin/assets/{sparkles-B-0I44es.js → sparkles-D88gf3vX.js} +2 -2
- package/dist/admin/assets/{sparkles-B-0I44es.js.map → sparkles-D88gf3vX.js.map} +1 -1
- package/dist/admin/assets/src-B2qcqjNM.js +4 -0
- package/dist/admin/assets/src-B2qcqjNM.js.map +1 -0
- package/dist/admin/assets/{stableId-rPnYVF-A.js → stableId-DlNKh3HM.js} +2 -2
- package/dist/admin/assets/{stableId-rPnYVF-A.js.map → stableId-DlNKh3HM.js.map} +1 -1
- package/dist/admin/assets/{store-Bqd9L9DK.js → store-jUWyOXDG.js} +2 -2
- package/dist/admin/assets/{store-Bqd9L9DK.js.map → store-jUWyOXDG.js.map} +1 -1
- package/dist/admin/assets/{switch-JmfyFAQG.js → switch-DbIrjP3h.js} +2 -2
- package/dist/admin/assets/{switch-JmfyFAQG.js.map → switch-DbIrjP3h.js.map} +1 -1
- package/dist/admin/assets/table-DQ5ILlSh.js +56 -0
- package/dist/admin/assets/table-DQ5ILlSh.js.map +1 -0
- package/dist/admin/assets/tabs-BJhfL-lw.js +5 -0
- package/dist/admin/assets/tabs-BJhfL-lw.js.map +1 -0
- package/dist/admin/assets/{textarea-eatLAWE_.js → textarea-FwSvwmyT.js} +2 -2
- package/dist/admin/assets/{textarea-eatLAWE_.js.map → textarea-FwSvwmyT.js.map} +1 -1
- package/dist/admin/assets/theme-C0Tv6M38.js +2 -0
- package/dist/admin/assets/theme-C0Tv6M38.js.map +1 -0
- package/dist/admin/assets/{titles-Bi2fWNN4.js → titles-uL13bpi8.js} +2 -2
- package/dist/admin/assets/{titles-Bi2fWNN4.js.map → titles-uL13bpi8.js.map} +1 -1
- package/dist/admin/assets/{tokens-C2E7swse.js → tokens-C-20gLAT.js} +2 -2
- package/dist/admin/assets/{tokens-C2E7swse.js.map → tokens-C-20gLAT.js.map} +1 -1
- package/dist/admin/assets/{tooltip-Bji9fmSr.js → tooltip-BK5FqmJ0.js} +2 -2
- package/dist/admin/assets/{tooltip-Bji9fmSr.js.map → tooltip-BK5FqmJ0.js.map} +1 -1
- package/dist/admin/assets/{undo-2-BU-uqm7e.js → undo-2-CLy2zklL.js} +2 -2
- package/dist/admin/assets/{undo-2-BU-uqm7e.js.map → undo-2-CLy2zklL.js.map} +1 -1
- package/dist/admin/assets/{useAuditLabels-D5nmbXqN.js → useAuditLabels-BJF7M5RY.js} +2 -2
- package/dist/admin/assets/{useAuditLabels-D5nmbXqN.js.map → useAuditLabels-BJF7M5RY.js.map} +1 -1
- package/dist/admin/assets/{useConfirmTarget-Y7_8kVX5.js → useConfirmTarget-BUvlrGSW.js} +2 -2
- package/dist/admin/assets/{useConfirmTarget-Y7_8kVX5.js.map → useConfirmTarget-BUvlrGSW.js.map} +1 -1
- package/dist/admin/assets/{useContentSchema-DJTqjlUk.js → useContentSchema-BCdLYsKa.js} +2 -2
- package/dist/admin/assets/{useContentSchema-DJTqjlUk.js.map → useContentSchema-BCdLYsKa.js.map} +1 -1
- package/dist/admin/assets/{useCursorPager-CiJroOo7.js → useCursorPager-CzB6NDOB.js} +2 -2
- package/dist/admin/assets/{useCursorPager-CiJroOo7.js.map → useCursorPager-CzB6NDOB.js.map} +1 -1
- package/dist/admin/assets/{useDiscardGuard-t_N3l9TQ.js → useDiscardGuard-CGFgC3xY.js} +2 -2
- package/dist/admin/assets/{useDiscardGuard-t_N3l9TQ.js.map → useDiscardGuard-CGFgC3xY.js.map} +1 -1
- package/dist/admin/assets/useMediaPickerDialog-BOkGEyG7.js +2 -0
- package/dist/admin/assets/useMediaPickerDialog-BOkGEyG7.js.map +1 -0
- package/dist/admin/assets/useMediaUploader-D1ouV8jc.js +2 -0
- package/dist/admin/assets/useMediaUploader-D1ouV8jc.js.map +1 -0
- package/dist/admin/assets/{usePaletteItems-BVjWCdow.js → usePaletteItems-neKzBlys.js} +2 -2
- package/dist/admin/assets/{usePaletteItems-BVjWCdow.js.map → usePaletteItems-neKzBlys.js.map} +1 -1
- package/dist/admin/assets/{usePlaces-CnSOZwtO.js → usePlaces-6iTN15Xy.js} +2 -2
- package/dist/admin/assets/{usePlaces-CnSOZwtO.js.map → usePlaces-6iTN15Xy.js.map} +1 -1
- package/dist/admin/assets/{useQueries-BRolGUiw.js → useQueries-CYfcxI0Q.js} +2 -2
- package/dist/admin/assets/{useQueries-BRolGUiw.js.map → useQueries-CYfcxI0Q.js.map} +1 -1
- package/dist/admin/assets/useResolvedScheme-D0moXjaP.js +2 -0
- package/dist/admin/assets/{searchSchema-Ypl6gq9g.js.map → useResolvedScheme-D0moXjaP.js.map} +1 -1
- package/dist/admin/assets/useSchemaScopeAccess-WQKmj8_3.js +2 -0
- package/dist/admin/assets/useSchemaScopeAccess-WQKmj8_3.js.map +1 -0
- package/dist/admin/assets/{users-CbErtM7w.js → users-B01EH47o.js} +2 -2
- package/dist/admin/assets/{users-CbErtM7w.js.map → users-B01EH47o.js.map} +1 -1
- package/dist/admin/assets/{users-bDGYpSXg.js → users-BgTNDPXx.js} +2 -2
- package/dist/admin/assets/{users-bDGYpSXg.js.map → users-BgTNDPXx.js.map} +1 -1
- package/dist/admin/assets/{zod-Gtw4e0qi.js → zod-CS0EvbwG.js} +2 -2
- package/dist/admin/assets/{zod-Gtw4e0qi.js.map → zod-CS0EvbwG.js.map} +1 -1
- package/dist/admin/index.html +57 -57
- package/dist/admin/theme-init.js +23 -21
- package/dist/cli.js +19 -9
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/{public-B7fFHMNr.d.ts → public-BSbOGbK-.d.ts} +19 -3
- package/dist/{worker-BAmQPoTJ.js → worker-i1rY2TIZ.js} +1894 -612
- package/dist/worker-i1rY2TIZ.js.map +1 -0
- package/package.json +5 -5
- package/dist/admin/assets/Body-CCOZq2q9.js +0 -2
- package/dist/admin/assets/Builder-BZy9mphs.js +0 -2
- package/dist/admin/assets/Builder-BZy9mphs.js.map +0 -1
- package/dist/admin/assets/Form-Doc0JEzF.js +0 -2
- package/dist/admin/assets/InsertMenuItem-DJXglsEd.js +0 -2
- package/dist/admin/assets/InsertMenuItem-DJXglsEd.js.map +0 -1
- package/dist/admin/assets/RichTextField-B21FqrT5.js +0 -144
- package/dist/admin/assets/Structure-C7dr00Bp.js +0 -2
- package/dist/admin/assets/Thumbnail-CTpKlrxR.js +0 -2
- package/dist/admin/assets/Thumbnail-CTpKlrxR.js.map +0 -1
- package/dist/admin/assets/arrow-up-DwMJqZVV.js +0 -2
- package/dist/admin/assets/arrow-up-DwMJqZVV.js.map +0 -1
- package/dist/admin/assets/chevron-left-D259hWJn.js +0 -2
- package/dist/admin/assets/circle-x-c1RwmC6j.js +0 -2
- package/dist/admin/assets/circle-x-c1RwmC6j.js.map +0 -1
- package/dist/admin/assets/cn-M4_WHiur.js +0 -2
- package/dist/admin/assets/cn-M4_WHiur.js.map +0 -1
- package/dist/admin/assets/coerce-DWI5sgLW.js +0 -2
- package/dist/admin/assets/constants-CgUdkWmf.js +0 -2
- package/dist/admin/assets/content-D25ainqV.js +0 -3
- package/dist/admin/assets/content-D25ainqV.js.map +0 -1
- package/dist/admin/assets/develop-qF10t9tM.js +0 -11
- package/dist/admin/assets/develop-qF10t9tM.js.map +0 -1
- package/dist/admin/assets/dropdown-menu-Cl20LCvY.js +0 -2
- package/dist/admin/assets/dropdown-menu-Cl20LCvY.js.map +0 -1
- package/dist/admin/assets/external-link-DtJAaOWk.js +0 -2
- package/dist/admin/assets/external-link-DtJAaOWk.js.map +0 -1
- package/dist/admin/assets/file-text-BJkQMeQO.js.map +0 -1
- package/dist/admin/assets/followPlural-Cuocm4Sv.js +0 -2
- package/dist/admin/assets/index-C9uJxg4J.js +0 -4
- package/dist/admin/assets/index-C9uJxg4J.js.map +0 -1
- package/dist/admin/assets/index-Cz3C-LxB.css +0 -2
- package/dist/admin/assets/link-2-off-Cbpvb3w4.js +0 -2
- package/dist/admin/assets/link-2-off-Cbpvb3w4.js.map +0 -1
- package/dist/admin/assets/list-CkHF_4xh.js +0 -2
- package/dist/admin/assets/list-CkHF_4xh.js.map +0 -1
- package/dist/admin/assets/media-CUfV74JZ.js +0 -2
- package/dist/admin/assets/network-Fri1cjMt.js +0 -2
- package/dist/admin/assets/network-Fri1cjMt.js.map +0 -1
- package/dist/admin/assets/publishing-DaaE8PaU.js +0 -2
- package/dist/admin/assets/search--71HzPvh.js +0 -2
- package/dist/admin/assets/search--71HzPvh.js.map +0 -1
- package/dist/admin/assets/searchSchema-Ypl6gq9g.js +0 -2
- package/dist/admin/assets/sections-BBN2JErq.js +0 -2
- package/dist/admin/assets/sections-BBN2JErq.js.map +0 -1
- package/dist/admin/assets/settings-CYXU73dP.js +0 -2
- package/dist/admin/assets/settings-CYXU73dP.js.map +0 -1
- package/dist/admin/assets/skeleton-CCwdO9oq.js +0 -42
- package/dist/admin/assets/skeleton-CCwdO9oq.js.map +0 -1
- package/dist/admin/assets/snapshots-BQeFK5cj.js +0 -2
- package/dist/admin/assets/src-CQu-YxUM.js +0 -4
- package/dist/admin/assets/src-CQu-YxUM.js.map +0 -1
- package/dist/admin/assets/table-BEDv9IRM.js +0 -56
- package/dist/admin/assets/table-BEDv9IRM.js.map +0 -1
- package/dist/admin/assets/tabs-DSS1c-Kf.js +0 -5
- package/dist/admin/assets/tabs-DSS1c-Kf.js.map +0 -1
- package/dist/admin/assets/useMediaUploader-CLHmKCwq.js +0 -2
- package/dist/admin/assets/useMediaUploader-CLHmKCwq.js.map +0 -1
- package/dist/admin/assets/useSchemaScopeAccess-bd8RQxuu.js +0 -2
- package/dist/admin/assets/useSchemaScopeAccess-bd8RQxuu.js.map +0 -1
- package/dist/worker-BAmQPoTJ.js.map +0 -1
|
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { createCipheriv, createDecipheriv, createHash, createHmac, hkdfSync, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { createReadStream, createWriteStream, existsSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
6
|
-
import Fastify from "fastify";
|
|
6
|
+
import Fastify, { LogController } from "fastify";
|
|
7
7
|
import { AliasNode, AndNode, BinaryOperationNode, ColumnNode, ColumnUpdateNode, IdentifierNode, InsertQueryNode, Kysely, ListNode, MysqlAdapter, MysqlIntrospector, MysqlQueryCompiler, OnDuplicateKeyNode, OperationNodeTransformer, OperatorNode, OrNode, OrderByItemNode, ParensNode, PostgresDialect, PrimitiveValueListNode, RawNode, ReferenceNode, SelectAllNode, SelectModifierNode, SelectQueryNode, SelectionNode, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler, TableNode, UpdateQueryNode, ValueListNode, ValueNode, ValuesNode, WhereNode, sql } from "kysely";
|
|
8
8
|
import pg from "pg";
|
|
9
9
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -1857,9 +1857,10 @@ const MYSQL_TABLES = {
|
|
|
1857
1857
|
"is_primary",
|
|
1858
1858
|
"version",
|
|
1859
1859
|
"created_at",
|
|
1860
|
-
"updated_at"
|
|
1860
|
+
"updated_at",
|
|
1861
|
+
"seo_defaults"
|
|
1861
1862
|
],
|
|
1862
|
-
nullable: [],
|
|
1863
|
+
nullable: ["seo_defaults"],
|
|
1863
1864
|
timestamps: ["created_at", "updated_at"],
|
|
1864
1865
|
uniqueKeys: [
|
|
1865
1866
|
{
|
|
@@ -2401,7 +2402,7 @@ const lockingKeySelect = (table, key, node, extra) => {
|
|
|
2401
2402
|
limit: node.limit
|
|
2402
2403
|
}));
|
|
2403
2404
|
};
|
|
2404
|
-
const keysOf$
|
|
2405
|
+
const keysOf$2 = (rows, key, bySubquery) => rows.map((row) => bySubquery ? [Object.values(row)[0]] : key.map((_, index) => row[`__shapio_key_${index}`]));
|
|
2405
2406
|
const updatePlan = (node, compile) => {
|
|
2406
2407
|
const table = tableOf(node.table, "UPDATE");
|
|
2407
2408
|
if (node.from || node.joins?.length) throw new Error(`MySQL: UPDATE … FROM/JOIN on ${table} cannot use RETURNING`);
|
|
@@ -2409,7 +2410,7 @@ const updatePlan = (node, compile) => {
|
|
|
2409
2410
|
const bySubquery = keySubquery(node.where, key) !== void 0;
|
|
2410
2411
|
const lock = compile(lockingKeySelect(table, key, node, []));
|
|
2411
2412
|
return (context) => context.atomically(async () => {
|
|
2412
|
-
const keys = keysOf$
|
|
2413
|
+
const keys = keysOf$2((await context.run(lock)).rows, key, bySubquery);
|
|
2413
2414
|
if (keys.length === 0) return resultOf$2([], 0);
|
|
2414
2415
|
await context.run(compile(Object.freeze({
|
|
2415
2416
|
...node,
|
|
@@ -2429,7 +2430,7 @@ const deletePlan = (node, compile) => {
|
|
|
2429
2430
|
const lock = compile(lockingKeySelect(table, key, node, node.returning.selections));
|
|
2430
2431
|
return (context) => context.atomically(async () => {
|
|
2431
2432
|
const locked = (await context.run(lock)).rows;
|
|
2432
|
-
const keys = keysOf$
|
|
2433
|
+
const keys = keysOf$2(locked, key, false);
|
|
2433
2434
|
if (keys.length === 0) return resultOf$2([], 0);
|
|
2434
2435
|
await context.run(compile(Object.freeze({
|
|
2435
2436
|
...node,
|
|
@@ -5150,7 +5151,7 @@ const NODE_KEYS = /* @__PURE__ */ new Set([
|
|
|
5150
5151
|
"marks"
|
|
5151
5152
|
]);
|
|
5152
5153
|
const MAX_PROBLEMS = 20;
|
|
5153
|
-
const isRecord$
|
|
5154
|
+
const isRecord$14 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5154
5155
|
const fail = (walk, path, message) => {
|
|
5155
5156
|
if (walk.problems.length < MAX_PROBLEMS) walk.problems.push({
|
|
5156
5157
|
path,
|
|
@@ -5158,7 +5159,7 @@ const fail = (walk, path, message) => {
|
|
|
5158
5159
|
});
|
|
5159
5160
|
};
|
|
5160
5161
|
const checkAttrs = (walk, path, rules, raw) => {
|
|
5161
|
-
if (raw !== void 0 && !isRecord$
|
|
5162
|
+
if (raw !== void 0 && !isRecord$14(raw)) return fail(walk, `${path}/attrs`, "must be an object");
|
|
5162
5163
|
const given = raw ?? {};
|
|
5163
5164
|
const attrs = {};
|
|
5164
5165
|
for (const key of Object.keys(given)) if (!rules || !Object.hasOwn(rules, key)) fail(walk, `${path}/attrs/${key}`, "is not an allowed attribute");
|
|
@@ -5180,7 +5181,7 @@ const checkMarks = (walk, path, raw, allowed) => {
|
|
|
5180
5181
|
const marks = [];
|
|
5181
5182
|
raw.forEach((mark, index) => {
|
|
5182
5183
|
const at = `${path}/marks/${index}`;
|
|
5183
|
-
if (!isRecord$
|
|
5184
|
+
if (!isRecord$14(mark) || typeof mark.type !== "string" || !Object.hasOwn(MARK_RULES, mark.type)) {
|
|
5184
5185
|
fail(walk, at, "is not an allowed mark");
|
|
5185
5186
|
return;
|
|
5186
5187
|
}
|
|
@@ -5201,7 +5202,7 @@ const matchesAllow = (allow, type) => {
|
|
|
5201
5202
|
};
|
|
5202
5203
|
const checkNode = (walk, raw, path, depth, context) => {
|
|
5203
5204
|
if (depth > 40) return fail(walk, path, "is nested too deeply");
|
|
5204
|
-
if (!isRecord$
|
|
5205
|
+
if (!isRecord$14(raw) || typeof raw.type !== "string") return fail(walk, path, "must be a node with a type");
|
|
5205
5206
|
const { type } = raw;
|
|
5206
5207
|
const rule = Object.hasOwn(NODE_RULES, type) ? NODE_RULES[type] : void 0;
|
|
5207
5208
|
if (!rule) return fail(walk, path, `"${type}" is not an allowed node`);
|
|
@@ -5254,7 +5255,7 @@ const validateRichText = (value) => {
|
|
|
5254
5255
|
problems: [],
|
|
5255
5256
|
textLength: 0
|
|
5256
5257
|
};
|
|
5257
|
-
if (!isRecord$
|
|
5258
|
+
if (!isRecord$14(value)) return {
|
|
5258
5259
|
ok: false,
|
|
5259
5260
|
problems: [{
|
|
5260
5261
|
path: "",
|
|
@@ -5305,6 +5306,83 @@ const richTextMediaIds = (document) => {
|
|
|
5305
5306
|
return [...ids];
|
|
5306
5307
|
};
|
|
5307
5308
|
//#endregion
|
|
5309
|
+
//#region ../../packages/schema/dist/resolve-viddCEYd.js
|
|
5310
|
+
/**
|
|
5311
|
+
* The built-in SEO component (plan seo-fields). It is ordinary schema data, created through the change planner
|
|
5312
|
+
* and always shared with every site; what makes it "the" SEO component is its stable ID, never its API ID, so
|
|
5313
|
+
* a renamed or extended copy is still recognised and an unrelated `seo` component (an import) never is.
|
|
5314
|
+
*/
|
|
5315
|
+
const SEO_COMPONENT_ID = "f3905a00-d2a3-4c62-899a-c894bfd6cb9b";
|
|
5316
|
+
/** Stable field IDs of the built-in's fields. Resolver and admin look fields up by these. */
|
|
5317
|
+
const SEO_FIELD_IDS = {
|
|
5318
|
+
title: "3fe294a9-6af9-4019-8b32-43010822e45a",
|
|
5319
|
+
description: "6d1917f3-94a2-4391-ba1f-a98d14577b93",
|
|
5320
|
+
image: "d27c9354-4afc-4583-93b4-367214a7714a",
|
|
5321
|
+
canonical: "17b374b6-cd85-444e-8403-20c7458ed29e",
|
|
5322
|
+
noindex: "e8346f99-dc3c-4b5c-906f-4854e03c9a57"
|
|
5323
|
+
};
|
|
5324
|
+
/** The built-in editor for a field holding the SEO component (preview and counters in the admin). */
|
|
5325
|
+
const SEO_EDITOR_ID = "seoEditor";
|
|
5326
|
+
/** A title template holds the placeholder exactly once, e.g. `%s · Acme`. */
|
|
5327
|
+
const SEO_TITLE_TEMPLATE_PATTERN = /^(?:(?!%s)[\s\S])*%s(?:(?!%s)[\s\S])*$/;
|
|
5328
|
+
/** A Twitter/X handle with its `@`. */
|
|
5329
|
+
const SEO_TWITTER_HANDLE_PATTERN = /^@[A-Za-z0-9_]{1,15}$/;
|
|
5330
|
+
/** No defaults saved yet. */
|
|
5331
|
+
const EMPTY_SEO_DEFAULTS = {
|
|
5332
|
+
locales: {},
|
|
5333
|
+
imageId: null,
|
|
5334
|
+
twitterHandle: null
|
|
5335
|
+
};
|
|
5336
|
+
const isValidTitleTemplate = (template) => SEO_TITLE_TEMPLATE_PATTERN.test(template);
|
|
5337
|
+
/**
|
|
5338
|
+
* One locale's effective default texts: each key from the first locale of the chain that sets it (the
|
|
5339
|
+
* requested locale, then its fallbacks, then the default locale).
|
|
5340
|
+
*/
|
|
5341
|
+
const seoDefaultsForLocale = (defaults, localeChain) => {
|
|
5342
|
+
const pick = (key) => localeChain.map((code) => defaults.locales[code]?.[key]).find((value) => value !== void 0);
|
|
5343
|
+
const siteName = pick("siteName");
|
|
5344
|
+
const titleTemplate = pick("titleTemplate");
|
|
5345
|
+
const description = pick("description");
|
|
5346
|
+
return {
|
|
5347
|
+
...siteName !== void 0 ? { siteName } : {},
|
|
5348
|
+
...titleTemplate !== void 0 ? { titleTemplate } : {},
|
|
5349
|
+
...description !== void 0 ? { description } : {}
|
|
5350
|
+
};
|
|
5351
|
+
};
|
|
5352
|
+
/** Applies a title template (`%s` replaced once); an invalid template leaves the title as is. */
|
|
5353
|
+
const applyTitleTemplate = (title, template) => template !== void 0 && isValidTitleTemplate(template) ? template.replace("%s", () => title) : title;
|
|
5354
|
+
/** The API IDs the built-in component is created with. */
|
|
5355
|
+
const DEFAULT_SEO_KEYS = {
|
|
5356
|
+
title: "title",
|
|
5357
|
+
description: "description",
|
|
5358
|
+
image: "image",
|
|
5359
|
+
canonical: "canonical",
|
|
5360
|
+
noindex: "noindex"
|
|
5361
|
+
};
|
|
5362
|
+
const isRecord$13 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5363
|
+
const text = (value) => typeof value === "string" && value.trim() !== "" ? value : null;
|
|
5364
|
+
/**
|
|
5365
|
+
* The effective SEO values of one delivered entry, in the same shape as the raw value (plan seo-fields): the
|
|
5366
|
+
* title is the entry's SEO title, else its own title, through the site's template (the bare site name when
|
|
5367
|
+
* neither exists, never templated); description and image fall back to the site's defaults; canonical stays
|
|
5368
|
+
* the entry's value or null; noindex is a boolean. Other keys (fields a user added) pass through. Pure.
|
|
5369
|
+
*/
|
|
5370
|
+
const resolveSeo = (raw, context) => {
|
|
5371
|
+
const keys = context.keys ?? DEFAULT_SEO_KEYS;
|
|
5372
|
+
const output = isRecord$13(raw) ? { ...raw } : {};
|
|
5373
|
+
const value = (key) => keys[key] === void 0 ? void 0 : output[keys[key]];
|
|
5374
|
+
const set = (key, next) => {
|
|
5375
|
+
if (keys[key] !== void 0) output[keys[key]] = next;
|
|
5376
|
+
};
|
|
5377
|
+
const base = text(value("title")) ?? text(context.fallbackTitle);
|
|
5378
|
+
set("title", base !== null ? applyTitleTemplate(base, context.defaults.titleTemplate) : context.defaults.siteName ?? null);
|
|
5379
|
+
set("description", text(value("description")) ?? context.defaults.description ?? null);
|
|
5380
|
+
set("image", value("image") ?? context.image ?? null);
|
|
5381
|
+
set("canonical", text(value("canonical")));
|
|
5382
|
+
set("noindex", value("noindex") === true);
|
|
5383
|
+
return output;
|
|
5384
|
+
};
|
|
5385
|
+
//#endregion
|
|
5308
5386
|
//#region ../../packages/schema/dist/index.js
|
|
5309
5387
|
var import_pluralize = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
5310
5388
|
(function(root, pluralize) {
|
|
@@ -5811,7 +5889,7 @@ const STORAGE_FAMILY = {
|
|
|
5811
5889
|
* defaulted properties may be omitted. `normalizeDefinition` turns input into the normalized types below,
|
|
5812
5890
|
* which are what Shapio stores, hashes and diffs.
|
|
5813
5891
|
*/
|
|
5814
|
-
const closed$
|
|
5892
|
+
const closed$3$1 = { additionalProperties: false };
|
|
5815
5893
|
const MODEL_KINDS = ["collection", "singleton"];
|
|
5816
5894
|
const DEFINITION_KINDS = [...MODEL_KINDS, "component"];
|
|
5817
5895
|
const SORT_DIRECTIONS = ["asc", "desc"];
|
|
@@ -5827,7 +5905,7 @@ const EditorChoiceInputSchema = Type.Object({
|
|
|
5827
5905
|
maxLength: 128
|
|
5828
5906
|
}),
|
|
5829
5907
|
options: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
|
|
5830
|
-
}, closed$
|
|
5908
|
+
}, closed$3$1);
|
|
5831
5909
|
const FieldInputSchema = Type.Object({
|
|
5832
5910
|
id: Type.Optional(Type.String()),
|
|
5833
5911
|
apiKey: Type.String(),
|
|
@@ -5850,7 +5928,7 @@ const FieldInputSchema = Type.Object({
|
|
|
5850
5928
|
/** Type-specific settings, validated against SETTINGS_SCHEMAS[type]. */
|
|
5851
5929
|
settings: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
5852
5930
|
editor: Type.Optional(EditorChoiceInputSchema)
|
|
5853
|
-
}, closed$
|
|
5931
|
+
}, closed$3$1);
|
|
5854
5932
|
const FieldGroupSchema = Type.Object({
|
|
5855
5933
|
id: Type.String({
|
|
5856
5934
|
minLength: 1,
|
|
@@ -5858,7 +5936,7 @@ const FieldGroupSchema = Type.Object({
|
|
|
5858
5936
|
}),
|
|
5859
5937
|
label: Label,
|
|
5860
5938
|
fieldIds: Type.Array(Type.String())
|
|
5861
|
-
}, closed$
|
|
5939
|
+
}, closed$3$1);
|
|
5862
5940
|
const ModelDisplayInputSchema = Type.Object({
|
|
5863
5941
|
/** Field whose value labels an entry in lists, pickers and relation summaries. */
|
|
5864
5942
|
titleFieldId: Type.Optional(Type.String()),
|
|
@@ -5867,7 +5945,7 @@ const ModelDisplayInputSchema = Type.Object({
|
|
|
5867
5945
|
defaultSort: Type.Optional(Type.Object({
|
|
5868
5946
|
fieldId: Type.String(),
|
|
5869
5947
|
direction: Type.Enum(SORT_DIRECTIONS)
|
|
5870
|
-
}, closed$
|
|
5948
|
+
}, closed$3$1)),
|
|
5871
5949
|
/** Editor layout: named sections of the entry form. */
|
|
5872
5950
|
groups: Type.Optional(Type.Array(FieldGroupSchema)),
|
|
5873
5951
|
/** Fields rendered as blocks in the entry document's canvas, in order. Default: see `effectiveLayout`. */
|
|
@@ -5876,11 +5954,11 @@ const ModelDisplayInputSchema = Type.Object({
|
|
|
5876
5954
|
coverFieldId: Type.Optional(Type.String()),
|
|
5877
5955
|
/** Properties shown as chips in the document's strip. Default: the first non-empty ones. */
|
|
5878
5956
|
stripFieldIds: Type.Optional(Type.Array(Type.String()))
|
|
5879
|
-
}, closed$
|
|
5957
|
+
}, closed$3$1);
|
|
5880
5958
|
const ComponentDisplayInputSchema = Type.Object({
|
|
5881
5959
|
titleFieldId: Type.Optional(Type.String()),
|
|
5882
5960
|
groups: Type.Optional(Type.Array(FieldGroupSchema))
|
|
5883
|
-
}, closed$
|
|
5961
|
+
}, closed$3$1);
|
|
5884
5962
|
const definitionBase = {
|
|
5885
5963
|
id: Type.Optional(Type.String()),
|
|
5886
5964
|
apiKey: Type.String(),
|
|
@@ -5900,7 +5978,7 @@ const ModelDefinitionInputSchema = Type.Object({
|
|
|
5900
5978
|
/** Entries have separate draft and published states. Default true. */
|
|
5901
5979
|
draftAndPublish: Type.Optional(Type.Boolean()),
|
|
5902
5980
|
display: Type.Optional(ModelDisplayInputSchema)
|
|
5903
|
-
}, closed$
|
|
5981
|
+
}, closed$3$1);
|
|
5904
5982
|
const ComponentDefinitionInputSchema = Type.Object({
|
|
5905
5983
|
...definitionBase,
|
|
5906
5984
|
kind: Type.Literal("component"),
|
|
@@ -5913,7 +5991,7 @@ const ComponentDefinitionInputSchema = Type.Object({
|
|
|
5913
5991
|
maxLength: 64
|
|
5914
5992
|
})),
|
|
5915
5993
|
display: Type.Optional(ComponentDisplayInputSchema)
|
|
5916
|
-
}, closed$
|
|
5994
|
+
}, closed$3$1);
|
|
5917
5995
|
const DefinitionInputSchema = Type.Union([ModelDefinitionInputSchema, ComponentDefinitionInputSchema]);
|
|
5918
5996
|
const isComponentDefinition = (definition) => definition.kind === "component";
|
|
5919
5997
|
const isModelDefinition = (definition) => definition.kind !== "component";
|
|
@@ -5936,7 +6014,7 @@ const LocaleInputSchema = Type.Object({
|
|
|
5936
6014
|
* enum values...). Each schema is closed (`additionalProperties: false`) so a typo in a hand-edited schema
|
|
5937
6015
|
* file is reported instead of ignored. Defaults are applied by `normalizeDefinition`.
|
|
5938
6016
|
*/
|
|
5939
|
-
const closed$
|
|
6017
|
+
const closed$2$1 = { additionalProperties: false };
|
|
5940
6018
|
const NonNegativeInt = Type.Integer({ minimum: 0 });
|
|
5941
6019
|
const pattern = (regex) => Type.String({ pattern: regex.source });
|
|
5942
6020
|
const lengthRules = {
|
|
@@ -5964,7 +6042,7 @@ const EnumValueSchema = Type.Object({
|
|
|
5964
6042
|
minLength: 1,
|
|
5965
6043
|
maxLength: 200
|
|
5966
6044
|
})
|
|
5967
|
-
}, closed$
|
|
6045
|
+
}, closed$2$1);
|
|
5968
6046
|
const SETTINGS_SCHEMAS = {
|
|
5969
6047
|
string: Type.Object({
|
|
5970
6048
|
...lengthRules,
|
|
@@ -5972,20 +6050,20 @@ const SETTINGS_SCHEMAS = {
|
|
|
5972
6050
|
minLength: 1,
|
|
5973
6051
|
maxLength: 500
|
|
5974
6052
|
}))
|
|
5975
|
-
}, closed$
|
|
5976
|
-
text: Type.Object(lengthRules, closed$
|
|
6053
|
+
}, closed$2$1),
|
|
6054
|
+
text: Type.Object(lengthRules, closed$2$1),
|
|
5977
6055
|
richtext: Type.Object({
|
|
5978
6056
|
formatVersion: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
5979
6057
|
maxLength: Type.Optional(Type.Integer({ minimum: 1 }))
|
|
5980
|
-
}, closed$
|
|
6058
|
+
}, closed$2$1),
|
|
5981
6059
|
number: Type.Object({
|
|
5982
6060
|
min: Type.Optional(Type.Number()),
|
|
5983
6061
|
max: Type.Optional(Type.Number())
|
|
5984
|
-
}, closed$
|
|
6062
|
+
}, closed$2$1),
|
|
5985
6063
|
integer: Type.Object({
|
|
5986
6064
|
min: Type.Optional(Type.Integer()),
|
|
5987
6065
|
max: Type.Optional(Type.Integer())
|
|
5988
|
-
}, closed$
|
|
6066
|
+
}, closed$2$1),
|
|
5989
6067
|
decimal: Type.Object({
|
|
5990
6068
|
precision: Type.Optional(Type.Integer({
|
|
5991
6069
|
minimum: 1,
|
|
@@ -5997,68 +6075,70 @@ const SETTINGS_SCHEMAS = {
|
|
|
5997
6075
|
})),
|
|
5998
6076
|
min: Type.Optional(pattern(DECIMAL_PATTERN)),
|
|
5999
6077
|
max: Type.Optional(pattern(DECIMAL_PATTERN))
|
|
6000
|
-
}, closed$
|
|
6078
|
+
}, closed$2$1),
|
|
6001
6079
|
biginteger: Type.Object({
|
|
6002
6080
|
min: Type.Optional(pattern(BIGINTEGER_PATTERN)),
|
|
6003
6081
|
max: Type.Optional(pattern(BIGINTEGER_PATTERN))
|
|
6004
|
-
}, closed$
|
|
6005
|
-
boolean: Type.Object({}, closed$
|
|
6082
|
+
}, closed$2$1),
|
|
6083
|
+
boolean: Type.Object({}, closed$2$1),
|
|
6006
6084
|
date: Type.Object({
|
|
6007
6085
|
min: Type.Optional(pattern(DATE_PATTERN)),
|
|
6008
6086
|
max: Type.Optional(pattern(DATE_PATTERN))
|
|
6009
|
-
}, closed$
|
|
6087
|
+
}, closed$2$1),
|
|
6010
6088
|
datetime: Type.Object({
|
|
6011
6089
|
min: Type.Optional(pattern(DATETIME_PATTERN)),
|
|
6012
6090
|
max: Type.Optional(pattern(DATETIME_PATTERN))
|
|
6013
|
-
}, closed$
|
|
6091
|
+
}, closed$2$1),
|
|
6014
6092
|
time: Type.Object({
|
|
6015
6093
|
min: Type.Optional(pattern(TIME_PATTERN)),
|
|
6016
6094
|
max: Type.Optional(pattern(TIME_PATTERN))
|
|
6017
|
-
}, closed$
|
|
6095
|
+
}, closed$2$1),
|
|
6018
6096
|
enum: Type.Object({
|
|
6019
6097
|
values: Type.Array(EnumValueSchema, {
|
|
6020
6098
|
minItems: 1,
|
|
6021
6099
|
maxItems: 500
|
|
6022
6100
|
}),
|
|
6023
6101
|
multiple: Type.Optional(Type.Boolean())
|
|
6024
|
-
}, closed$
|
|
6025
|
-
json: Type.Object({}, closed$
|
|
6102
|
+
}, closed$2$1),
|
|
6103
|
+
json: Type.Object({}, closed$2$1),
|
|
6026
6104
|
slug: Type.Object({
|
|
6027
6105
|
...lengthRules,
|
|
6028
6106
|
sourceFieldId: Type.Optional(Type.String())
|
|
6029
|
-
}, closed$
|
|
6030
|
-
email: Type.Object({}, closed$
|
|
6107
|
+
}, closed$2$1),
|
|
6108
|
+
email: Type.Object({}, closed$2$1),
|
|
6031
6109
|
url: Type.Object({ protocols: Type.Optional(Type.Array(Type.Enum([
|
|
6032
6110
|
"http",
|
|
6033
6111
|
"https",
|
|
6034
6112
|
"mailto",
|
|
6035
6113
|
"tel"
|
|
6036
|
-
]), { minItems: 1 })) }, closed$
|
|
6037
|
-
uid: Type.Object(lengthRules, closed$
|
|
6114
|
+
]), { minItems: 1 })) }, closed$2$1),
|
|
6115
|
+
uid: Type.Object(lengthRules, closed$2$1),
|
|
6038
6116
|
media: Type.Object({
|
|
6039
6117
|
multiple: Type.Optional(Type.Boolean()),
|
|
6040
6118
|
allowedKinds: Type.Optional(Type.Array(Type.Enum(MEDIA_KINDS), { minItems: 1 })),
|
|
6041
6119
|
...countRules
|
|
6042
|
-
}, closed$
|
|
6120
|
+
}, closed$2$1),
|
|
6043
6121
|
relation: Type.Object({
|
|
6044
6122
|
target: Type.String(),
|
|
6045
6123
|
cardinality: Type.Enum(RELATION_CARDINALITIES),
|
|
6046
6124
|
...countRules
|
|
6047
|
-
}, closed$
|
|
6125
|
+
}, closed$2$1),
|
|
6048
6126
|
component: Type.Object({
|
|
6049
6127
|
component: Type.String(),
|
|
6050
6128
|
repeatable: Type.Optional(Type.Boolean()),
|
|
6051
6129
|
...countRules
|
|
6052
|
-
}, closed$
|
|
6130
|
+
}, closed$2$1),
|
|
6053
6131
|
dynamiczone: Type.Object({
|
|
6054
6132
|
components: Type.Array(Type.String(), {
|
|
6055
6133
|
minItems: 1,
|
|
6056
6134
|
maxItems: 100
|
|
6057
6135
|
}),
|
|
6058
6136
|
...countRules
|
|
6059
|
-
}, closed$
|
|
6137
|
+
}, closed$2$1)
|
|
6060
6138
|
};
|
|
6061
|
-
|
|
6139
|
+
/** Whether a field holds the built-in SEO component (by stable ID, whatever its API ID or editor). */
|
|
6140
|
+
const isSeoField = (field) => field.type === "component" && field.settings.component === "f3905a00-d2a3-4c62-899a-c894bfd6cb9b" && !field.settings.repeatable;
|
|
6141
|
+
const closed$1$1 = { additionalProperties: false };
|
|
6062
6142
|
const Placeholder = Type.Optional(Type.String({ maxLength: 200 }));
|
|
6063
6143
|
const isMultipleEnum = (field) => field.type === "enum" && field.settings.multiple;
|
|
6064
6144
|
const entries = [
|
|
@@ -6070,7 +6150,7 @@ const entries = [
|
|
|
6070
6150
|
"url",
|
|
6071
6151
|
"uid"
|
|
6072
6152
|
],
|
|
6073
|
-
optionsSchema: Type.Object({ placeholder: Placeholder }, closed$
|
|
6153
|
+
optionsSchema: Type.Object({ placeholder: Placeholder }, closed$1$1)
|
|
6074
6154
|
},
|
|
6075
6155
|
{
|
|
6076
6156
|
id: "textarea",
|
|
@@ -6081,7 +6161,7 @@ const entries = [
|
|
|
6081
6161
|
minimum: 2,
|
|
6082
6162
|
maximum: 40
|
|
6083
6163
|
}))
|
|
6084
|
-
}, closed$
|
|
6164
|
+
}, closed$1$1)
|
|
6085
6165
|
},
|
|
6086
6166
|
{
|
|
6087
6167
|
id: "color",
|
|
@@ -6089,12 +6169,12 @@ const entries = [
|
|
|
6089
6169
|
optionsSchema: Type.Object({
|
|
6090
6170
|
format: Type.Optional(Type.Enum(["hex", "rgb"])),
|
|
6091
6171
|
presets: Type.Optional(Type.Array(Type.String({ maxLength: 32 }), { maxItems: 50 }))
|
|
6092
|
-
}, closed$
|
|
6172
|
+
}, closed$1$1)
|
|
6093
6173
|
},
|
|
6094
6174
|
{
|
|
6095
6175
|
id: "richText",
|
|
6096
6176
|
dataTypes: ["richtext"],
|
|
6097
|
-
optionsSchema: Type.Object({ toolbar: Type.Optional(Type.Enum(["full", "minimal"])) }, closed$
|
|
6177
|
+
optionsSchema: Type.Object({ toolbar: Type.Optional(Type.Enum(["full", "minimal"])) }, closed$1$1)
|
|
6098
6178
|
},
|
|
6099
6179
|
{
|
|
6100
6180
|
id: "numberInput",
|
|
@@ -6107,17 +6187,17 @@ const entries = [
|
|
|
6107
6187
|
optionsSchema: Type.Object({
|
|
6108
6188
|
placeholder: Placeholder,
|
|
6109
6189
|
step: Type.Optional(Type.Number({ exclusiveMinimum: 0 }))
|
|
6110
|
-
}, closed$
|
|
6190
|
+
}, closed$1$1)
|
|
6111
6191
|
},
|
|
6112
6192
|
{
|
|
6113
6193
|
id: "toggle",
|
|
6114
6194
|
dataTypes: ["boolean"],
|
|
6115
|
-
optionsSchema: Type.Object({}, closed$
|
|
6195
|
+
optionsSchema: Type.Object({}, closed$1$1)
|
|
6116
6196
|
},
|
|
6117
6197
|
{
|
|
6118
6198
|
id: "checkbox",
|
|
6119
6199
|
dataTypes: ["boolean"],
|
|
6120
|
-
optionsSchema: Type.Object({}, closed$
|
|
6200
|
+
optionsSchema: Type.Object({}, closed$1$1)
|
|
6121
6201
|
},
|
|
6122
6202
|
{
|
|
6123
6203
|
id: "segmented",
|
|
@@ -6125,35 +6205,35 @@ const entries = [
|
|
|
6125
6205
|
optionsSchema: Type.Object({
|
|
6126
6206
|
trueLabel: Type.Optional(Type.String({ maxLength: 50 })),
|
|
6127
6207
|
falseLabel: Type.Optional(Type.String({ maxLength: 50 }))
|
|
6128
|
-
}, closed$
|
|
6208
|
+
}, closed$1$1),
|
|
6129
6209
|
supports: (field) => !isMultipleEnum(field)
|
|
6130
6210
|
},
|
|
6131
6211
|
{
|
|
6132
6212
|
id: "select",
|
|
6133
6213
|
dataTypes: ["enum"],
|
|
6134
|
-
optionsSchema: Type.Object({ placeholder: Placeholder }, closed$
|
|
6214
|
+
optionsSchema: Type.Object({ placeholder: Placeholder }, closed$1$1)
|
|
6135
6215
|
},
|
|
6136
6216
|
{
|
|
6137
6217
|
id: "radio",
|
|
6138
6218
|
dataTypes: ["enum"],
|
|
6139
|
-
optionsSchema: Type.Object({ layout: Type.Optional(Type.Enum(["vertical", "horizontal"])) }, closed$
|
|
6219
|
+
optionsSchema: Type.Object({ layout: Type.Optional(Type.Enum(["vertical", "horizontal"])) }, closed$1$1),
|
|
6140
6220
|
supports: (field) => !isMultipleEnum(field)
|
|
6141
6221
|
},
|
|
6142
6222
|
{
|
|
6143
6223
|
id: "checkboxGroup",
|
|
6144
6224
|
dataTypes: ["enum"],
|
|
6145
|
-
optionsSchema: Type.Object({ layout: Type.Optional(Type.Enum(["vertical", "horizontal"])) }, closed$
|
|
6225
|
+
optionsSchema: Type.Object({ layout: Type.Optional(Type.Enum(["vertical", "horizontal"])) }, closed$1$1),
|
|
6146
6226
|
supports: isMultipleEnum
|
|
6147
6227
|
},
|
|
6148
6228
|
{
|
|
6149
6229
|
id: "datePicker",
|
|
6150
6230
|
dataTypes: ["date"],
|
|
6151
|
-
optionsSchema: Type.Object({}, closed$
|
|
6231
|
+
optionsSchema: Type.Object({}, closed$1$1)
|
|
6152
6232
|
},
|
|
6153
6233
|
{
|
|
6154
6234
|
id: "dateTimePicker",
|
|
6155
6235
|
dataTypes: ["datetime"],
|
|
6156
|
-
optionsSchema: Type.Object({ display: Type.Optional(Type.Enum(["local", "utc"])) }, closed$
|
|
6236
|
+
optionsSchema: Type.Object({ display: Type.Optional(Type.Enum(["local", "utc"])) }, closed$1$1)
|
|
6157
6237
|
},
|
|
6158
6238
|
{
|
|
6159
6239
|
id: "timePicker",
|
|
@@ -6161,12 +6241,12 @@ const entries = [
|
|
|
6161
6241
|
optionsSchema: Type.Object({ stepMinutes: Type.Optional(Type.Integer({
|
|
6162
6242
|
minimum: 1,
|
|
6163
6243
|
maximum: 60
|
|
6164
|
-
})) }, closed$
|
|
6244
|
+
})) }, closed$1$1)
|
|
6165
6245
|
},
|
|
6166
6246
|
{
|
|
6167
6247
|
id: "slugInput",
|
|
6168
6248
|
dataTypes: ["slug"],
|
|
6169
|
-
optionsSchema: Type.Object({ placeholder: Placeholder }, closed$
|
|
6249
|
+
optionsSchema: Type.Object({ placeholder: Placeholder }, closed$1$1)
|
|
6170
6250
|
},
|
|
6171
6251
|
{
|
|
6172
6252
|
id: "jsonEditor",
|
|
@@ -6174,27 +6254,33 @@ const entries = [
|
|
|
6174
6254
|
optionsSchema: Type.Object({ rows: Type.Optional(Type.Integer({
|
|
6175
6255
|
minimum: 3,
|
|
6176
6256
|
maximum: 80
|
|
6177
|
-
})) }, closed$
|
|
6257
|
+
})) }, closed$1$1)
|
|
6178
6258
|
},
|
|
6179
6259
|
{
|
|
6180
6260
|
id: "mediaPicker",
|
|
6181
6261
|
dataTypes: ["media"],
|
|
6182
|
-
optionsSchema: Type.Object({}, closed$
|
|
6262
|
+
optionsSchema: Type.Object({}, closed$1$1)
|
|
6183
6263
|
},
|
|
6184
6264
|
{
|
|
6185
6265
|
id: "relationPicker",
|
|
6186
6266
|
dataTypes: ["relation"],
|
|
6187
|
-
optionsSchema: Type.Object({ allowInlineCreate: Type.Optional(Type.Boolean()) }, closed$
|
|
6267
|
+
optionsSchema: Type.Object({ allowInlineCreate: Type.Optional(Type.Boolean()) }, closed$1$1)
|
|
6188
6268
|
},
|
|
6189
6269
|
{
|
|
6190
6270
|
id: "componentEditor",
|
|
6191
6271
|
dataTypes: ["component"],
|
|
6192
|
-
optionsSchema: Type.Object({ collapsed: Type.Optional(Type.Boolean()) }, closed$
|
|
6272
|
+
optionsSchema: Type.Object({ collapsed: Type.Optional(Type.Boolean()) }, closed$1$1)
|
|
6273
|
+
},
|
|
6274
|
+
{
|
|
6275
|
+
id: SEO_EDITOR_ID,
|
|
6276
|
+
dataTypes: ["component"],
|
|
6277
|
+
optionsSchema: Type.Object({}, closed$1$1),
|
|
6278
|
+
supports: isSeoField
|
|
6193
6279
|
},
|
|
6194
6280
|
{
|
|
6195
6281
|
id: "dynamicZoneEditor",
|
|
6196
6282
|
dataTypes: ["dynamiczone"],
|
|
6197
|
-
optionsSchema: Type.Object({ collapsed: Type.Optional(Type.Boolean()) }, closed$
|
|
6283
|
+
optionsSchema: Type.Object({ collapsed: Type.Optional(Type.Boolean()) }, closed$1$1)
|
|
6198
6284
|
}
|
|
6199
6285
|
];
|
|
6200
6286
|
const EDITOR_CATALOGUE = new Map(entries.map((entry) => [entry.id, entry]));
|
|
@@ -6595,7 +6681,10 @@ const RESERVED_TYPE_NAMES = [
|
|
|
6595
6681
|
"SnapshotChangeKind",
|
|
6596
6682
|
"SnapshotChangeLocale",
|
|
6597
6683
|
"SnapshotChangePage",
|
|
6598
|
-
"SnapshotInfo"
|
|
6684
|
+
"SnapshotInfo",
|
|
6685
|
+
"SiteInfo",
|
|
6686
|
+
"SiteSeoDefaults",
|
|
6687
|
+
"SiteSeoLocale"
|
|
6599
6688
|
];
|
|
6600
6689
|
/**
|
|
6601
6690
|
* Model and component API keys the admin uses as static URL segments (`/content/new`,
|
|
@@ -6629,7 +6718,8 @@ const RESERVED_FIELD_API_KEYS = [
|
|
|
6629
6718
|
const RESERVED_QUERY_NAMES = [
|
|
6630
6719
|
"_schemaVersion",
|
|
6631
6720
|
"_changes",
|
|
6632
|
-
"_snapshot"
|
|
6721
|
+
"_snapshot",
|
|
6722
|
+
"_site"
|
|
6633
6723
|
];
|
|
6634
6724
|
/** Every name a definition contributes to the GraphQL schema. */
|
|
6635
6725
|
const generatedNames = (definition) => {
|
|
@@ -8007,7 +8097,9 @@ const BUILT_IN_THEME_KEYS = [
|
|
|
8007
8097
|
"shapio",
|
|
8008
8098
|
"classic",
|
|
8009
8099
|
"murdered-out",
|
|
8010
|
-
"
|
|
8100
|
+
"forest",
|
|
8101
|
+
"snowed",
|
|
8102
|
+
"butter"
|
|
8011
8103
|
];
|
|
8012
8104
|
/**
|
|
8013
8105
|
* A token value an extension may use: a hex colour or a colour function whose arguments are numbers, units
|
|
@@ -8117,6 +8209,85 @@ const checkThemeContrast = (tokens, variant) => {
|
|
|
8117
8209
|
};
|
|
8118
8210
|
return [...THEME_TEXT_PAIRS.map((pair) => measure(pair, 4.5)), ...uiPairs.map((pair) => measure(pair, 3))];
|
|
8119
8211
|
};
|
|
8212
|
+
/**
|
|
8213
|
+
* The built-in SEO component as first created. All fields are public and none has a length limit: the 70 and
|
|
8214
|
+
* 160 character marks are admin-only counters (`SEO_TITLE_SOFT_MAX`), never validation.
|
|
8215
|
+
*/
|
|
8216
|
+
const seoComponentDefinition = () => normalizeDefinition({
|
|
8217
|
+
id: SEO_COMPONENT_ID,
|
|
8218
|
+
kind: "component",
|
|
8219
|
+
apiKey: "seo",
|
|
8220
|
+
label: "SEO",
|
|
8221
|
+
description: "Search and social metadata: title, description, image, canonical URL and indexing.",
|
|
8222
|
+
category: "Built-in",
|
|
8223
|
+
icon: "search",
|
|
8224
|
+
fields: [
|
|
8225
|
+
{
|
|
8226
|
+
id: SEO_FIELD_IDS.title,
|
|
8227
|
+
apiKey: "title",
|
|
8228
|
+
label: "Title",
|
|
8229
|
+
type: "string"
|
|
8230
|
+
},
|
|
8231
|
+
{
|
|
8232
|
+
id: SEO_FIELD_IDS.description,
|
|
8233
|
+
apiKey: "description",
|
|
8234
|
+
label: "Description",
|
|
8235
|
+
type: "text"
|
|
8236
|
+
},
|
|
8237
|
+
{
|
|
8238
|
+
id: SEO_FIELD_IDS.image,
|
|
8239
|
+
apiKey: "image",
|
|
8240
|
+
label: "Social image",
|
|
8241
|
+
type: "media",
|
|
8242
|
+
settings: {
|
|
8243
|
+
multiple: false,
|
|
8244
|
+
allowedKinds: ["image"]
|
|
8245
|
+
}
|
|
8246
|
+
},
|
|
8247
|
+
{
|
|
8248
|
+
id: SEO_FIELD_IDS.canonical,
|
|
8249
|
+
apiKey: "canonical",
|
|
8250
|
+
label: "Canonical URL",
|
|
8251
|
+
type: "url",
|
|
8252
|
+
settings: { protocols: ["https", "http"] }
|
|
8253
|
+
},
|
|
8254
|
+
{
|
|
8255
|
+
id: SEO_FIELD_IDS.noindex,
|
|
8256
|
+
apiKey: "noindex",
|
|
8257
|
+
label: "Hide from search engines",
|
|
8258
|
+
type: "boolean"
|
|
8259
|
+
}
|
|
8260
|
+
],
|
|
8261
|
+
display: { titleFieldId: SEO_FIELD_IDS.title }
|
|
8262
|
+
});
|
|
8263
|
+
const closed$12 = { additionalProperties: false };
|
|
8264
|
+
/** Media asset IDs are UUIDs. */
|
|
8265
|
+
const ASSET_ID_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
8266
|
+
/** One locale's default texts. Every key is optional: a missing one falls back along the locale chain. */
|
|
8267
|
+
const SeoLocaleDefaultsSchema = Type.Object({
|
|
8268
|
+
siteName: Type.Optional(Type.String({
|
|
8269
|
+
minLength: 1,
|
|
8270
|
+
maxLength: 200
|
|
8271
|
+
})),
|
|
8272
|
+
titleTemplate: Type.Optional(Type.String({
|
|
8273
|
+
minLength: 2,
|
|
8274
|
+
maxLength: 200,
|
|
8275
|
+
pattern: SEO_TITLE_TEMPLATE_PATTERN.source
|
|
8276
|
+
})),
|
|
8277
|
+
description: Type.Optional(Type.String({
|
|
8278
|
+
minLength: 1,
|
|
8279
|
+
maxLength: 1e3
|
|
8280
|
+
}))
|
|
8281
|
+
}, closed$12);
|
|
8282
|
+
/**
|
|
8283
|
+
* A site's SEO defaults (`sites.seo_defaults`): texts per locale code, the default social image (a public
|
|
8284
|
+
* image asset of the same site) and the Twitter handle for the whole site.
|
|
8285
|
+
*/
|
|
8286
|
+
const SeoDefaultsSchema = Type.Object({
|
|
8287
|
+
locales: Type.Record(Type.String({ pattern: LOCALE_CODE_PATTERN.source }), SeoLocaleDefaultsSchema, { additionalProperties: false }),
|
|
8288
|
+
imageId: Type.Union([Type.String({ pattern: ASSET_ID_PATTERN.source }), Type.Null()]),
|
|
8289
|
+
twitterHandle: Type.Union([Type.String({ pattern: SEO_TWITTER_HANDLE_PATTERN.source }), Type.Null()])
|
|
8290
|
+
}, closed$12);
|
|
8120
8291
|
//#endregion
|
|
8121
8292
|
//#region src/content/model.ts
|
|
8122
8293
|
const modelNotFound = (key) => new AppError(404, "MODEL_NOT_FOUND", `No content model "${key}"`, { modelKey: key });
|
|
@@ -10214,7 +10385,7 @@ const getPermissionsVersion = async (trx = db) => {
|
|
|
10214
10385
|
return (await trx.selectFrom("system_versions").select("permissions_version").executeTakeFirstOrThrow()).permissions_version;
|
|
10215
10386
|
};
|
|
10216
10387
|
/** Call in the same transaction as every permission change, so caches reload exactly when it commits. */
|
|
10217
|
-
const bumpPermissionsVersion = (trx = db) => trx.updateTable("system_versions").set((eb) => ({
|
|
10388
|
+
const bumpPermissionsVersion$2 = (trx = db) => trx.updateTable("system_versions").set((eb) => ({
|
|
10218
10389
|
permissions_version: eb("permissions_version", "+", eb.lit(1)),
|
|
10219
10390
|
updated_at: currentTimestamp()
|
|
10220
10391
|
})).execute();
|
|
@@ -10264,7 +10435,8 @@ const SITE_ACTIONS = [
|
|
|
10264
10435
|
"deployments.manage",
|
|
10265
10436
|
"deployments.trigger",
|
|
10266
10437
|
"changes.manage",
|
|
10267
|
-
"changes.ship"
|
|
10438
|
+
"changes.ship",
|
|
10439
|
+
"site.settings"
|
|
10268
10440
|
];
|
|
10269
10441
|
const GLOBAL_ACTIONS = [...NETWORK_ACTIONS, ...SITE_ACTIONS];
|
|
10270
10442
|
const EMPTY_MASK$1 = {
|
|
@@ -10360,7 +10532,7 @@ const ensureSystemRoles = async (database) => {
|
|
|
10360
10532
|
})), trx);
|
|
10361
10533
|
changed ||= added.length > 0;
|
|
10362
10534
|
}
|
|
10363
|
-
if (changed) await bumpPermissionsVersion(trx);
|
|
10535
|
+
if (changed) await bumpPermissionsVersion$2(trx);
|
|
10364
10536
|
});
|
|
10365
10537
|
};
|
|
10366
10538
|
//#endregion
|
|
@@ -10642,6 +10814,26 @@ const countContents = async (siteId, trx = db) => {
|
|
|
10642
10814
|
};
|
|
10643
10815
|
};
|
|
10644
10816
|
const deleteById$3 = (id, trx = db) => trx.deleteFrom("sites").where("id", "=", id).executeTakeFirst();
|
|
10817
|
+
/** A site's SEO defaults (null until first saved) and the version an update must name. */
|
|
10818
|
+
const findSeoDefaults = (id, trx = db) => trx.selectFrom("sites").select([
|
|
10819
|
+
"id",
|
|
10820
|
+
"key",
|
|
10821
|
+
"name",
|
|
10822
|
+
"version",
|
|
10823
|
+
"seo_defaults"
|
|
10824
|
+
]).where("id", "=", id).executeTakeFirst();
|
|
10825
|
+
/** Optimistic update of the SEO defaults: applies only at `expectedVersion`; bumps the version. */
|
|
10826
|
+
const updateSeoDefaultsIfVersion = (id, expectedVersion, seoDefaults, trx = db) => trx.updateTable("sites").set((eb) => ({
|
|
10827
|
+
seo_defaults: JSON.stringify(seoDefaults),
|
|
10828
|
+
version: eb("version", "+", 1),
|
|
10829
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
10830
|
+
})).where("id", "=", id).where("version", "=", expectedVersion).returning([
|
|
10831
|
+
"id",
|
|
10832
|
+
"key",
|
|
10833
|
+
"name",
|
|
10834
|
+
"version",
|
|
10835
|
+
"seo_defaults"
|
|
10836
|
+
]).executeTakeFirst();
|
|
10645
10837
|
//#endregion
|
|
10646
10838
|
//#region src/services/roleAssignments.ts
|
|
10647
10839
|
const assignmentKey = (assignment) => `${assignment.roleId}:${assignment.siteId ?? "*"}`;
|
|
@@ -11949,8 +12141,9 @@ const groupByRole = (grants) => {
|
|
|
11949
12141
|
};
|
|
11950
12142
|
/**
|
|
11951
12143
|
* Every role's grants, held in memory and reloaded when `system_versions.permissions_version` moves.
|
|
11952
|
-
* The version is
|
|
11953
|
-
*
|
|
12144
|
+
* The version is checked on every lookup: read here, or read once by the request (`versions`, see
|
|
12145
|
+
* `KnownVersions`). That durable check is what makes a permission change visible to every instance from the
|
|
12146
|
+
* next request (NOTIFY would only be an optimisation). Lookups on the pool share one reload; a
|
|
11954
12147
|
* lookup inside a transaction reads (and, when stale, reloads) through that transaction alone, never
|
|
11955
12148
|
* through a reload running on another request's transaction.
|
|
11956
12149
|
*/
|
|
@@ -11969,21 +12162,21 @@ const createPermissionCache = (database) => {
|
|
|
11969
12162
|
const sharedReload = (version) => reloading ??= reload(version, database).finally(() => {
|
|
11970
12163
|
reloading = void 0;
|
|
11971
12164
|
});
|
|
11972
|
-
const current = async (executor) => {
|
|
11973
|
-
const version = await getPermissionsVersion(executor ?? database);
|
|
12165
|
+
const current = async (executor, versions) => {
|
|
12166
|
+
const version = versions?.permissionsVersion ?? await getPermissionsVersion(executor ?? database);
|
|
11974
12167
|
if (snapshot && snapshot.version === version) return snapshot;
|
|
11975
12168
|
const loaded = await (executor ? reload(version, executor) : sharedReload(version));
|
|
11976
12169
|
if (!snapshot || loaded.version >= snapshot.version) snapshot = loaded;
|
|
11977
12170
|
return loaded;
|
|
11978
12171
|
};
|
|
11979
12172
|
return {
|
|
11980
|
-
getGrants: async (roleIds, executor) => {
|
|
12173
|
+
getGrants: async (roleIds, executor, versions) => {
|
|
11981
12174
|
if (roleIds.length === 0) return [];
|
|
11982
|
-
const { byRole } = await current(executor);
|
|
12175
|
+
const { byRole } = await current(executor, versions);
|
|
11983
12176
|
return roleIds.flatMap((roleId) => byRole.get(roleId) ?? []);
|
|
11984
12177
|
},
|
|
11985
|
-
getSiteAppRoleIds: async (siteId, audience, executor) => {
|
|
11986
|
-
const { bindings } = await current(executor);
|
|
12178
|
+
getSiteAppRoleIds: async (siteId, audience, executor, versions) => {
|
|
12179
|
+
const { bindings } = await current(executor, versions);
|
|
11987
12180
|
return bindings.get(bindingKey(siteId, audience)) ?? [];
|
|
11988
12181
|
}
|
|
11989
12182
|
};
|
|
@@ -12074,7 +12267,7 @@ const APP_ACTIONS = new Set(APP_CONTENT_ACTIONS);
|
|
|
12074
12267
|
* outside a site holds no role). Both are delivery audiences: they see `public: false` fields only where a
|
|
12075
12268
|
* grant names them.
|
|
12076
12269
|
*/
|
|
12077
|
-
const rolesOf = async (principal, grants, executor) => {
|
|
12270
|
+
const rolesOf = async (principal, grants, executor, versions) => {
|
|
12078
12271
|
switch (principal.kind) {
|
|
12079
12272
|
case "system": return "all";
|
|
12080
12273
|
case "admin": return {
|
|
@@ -12095,13 +12288,13 @@ const rolesOf = async (principal, grants, executor) => {
|
|
|
12095
12288
|
actions: READ_ONLY$1
|
|
12096
12289
|
};
|
|
12097
12290
|
case "appUser": return {
|
|
12098
|
-
roleIds: [...await grants.getSiteAppRoleIds(principal.siteId, "authenticated", executor), ...principal.roleIds],
|
|
12291
|
+
roleIds: [...await grants.getSiteAppRoleIds(principal.siteId, "authenticated", executor, versions), ...principal.roleIds],
|
|
12099
12292
|
networkRoleIds: [],
|
|
12100
12293
|
audience: "delivery",
|
|
12101
12294
|
actions: APP_ACTIONS
|
|
12102
12295
|
};
|
|
12103
12296
|
case "anonymous": return {
|
|
12104
|
-
roleIds: principal.siteId === null ? [] : await grants.getSiteAppRoleIds(principal.siteId, "public", executor),
|
|
12297
|
+
roleIds: principal.siteId === null ? [] : await grants.getSiteAppRoleIds(principal.siteId, "public", executor, versions),
|
|
12105
12298
|
networkRoleIds: [],
|
|
12106
12299
|
audience: "delivery",
|
|
12107
12300
|
actions: APP_ACTIONS
|
|
@@ -12121,58 +12314,64 @@ const actsOnSite = (principal, siteId) => {
|
|
|
12121
12314
|
};
|
|
12122
12315
|
/**
|
|
12123
12316
|
* The one permission evaluator (ADR 0005): principal → roles → grants → Policy, for REST and GraphQL.
|
|
12124
|
-
* Deny by default at every step.
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
|
|
12134
|
-
|
|
12135
|
-
|
|
12136
|
-
|
|
12137
|
-
|
|
12138
|
-
|
|
12139
|
-
|
|
12140
|
-
|
|
12141
|
-
|
|
12142
|
-
|
|
12143
|
-
|
|
12144
|
-
|
|
12145
|
-
|
|
12146
|
-
|
|
12147
|
-
|
|
12148
|
-
|
|
12149
|
-
|
|
12150
|
-
|
|
12151
|
-
|
|
12152
|
-
|
|
12153
|
-
|
|
12154
|
-
|
|
12155
|
-
|
|
12317
|
+
* Deny by default at every step. `atVersions` gives the same evaluator for one request, whose caches are
|
|
12318
|
+
* checked against the versions the request read once instead of read again on every call.
|
|
12319
|
+
*/
|
|
12320
|
+
const createPermissionEvaluator = ({ grants, fields }) => {
|
|
12321
|
+
const at = (versions) => ({
|
|
12322
|
+
evaluate: async (principal, request, executor) => {
|
|
12323
|
+
const roles = await rolesOf(principal, grants, executor, versions);
|
|
12324
|
+
if (roles === "all") return ALLOW_ALL_POLICY;
|
|
12325
|
+
if (roles.actions !== "all" && !roles.actions.has(request.action)) return DENIED_POLICY;
|
|
12326
|
+
let roleIds = roles.roleIds;
|
|
12327
|
+
if (NETWORK_CONTENT_ACTIONS.has(request.action)) {
|
|
12328
|
+
const modelSite = await fields.getModelSite(request.modelId, executor, versions);
|
|
12329
|
+
roleIds = modelSite !== null && actsOnSite(principal, modelSite) ? roles.roleIds : roles.networkRoleIds;
|
|
12330
|
+
}
|
|
12331
|
+
const held = await grants.getGrants(roleIds, executor, versions);
|
|
12332
|
+
if (held.length === 0) return DENIED_POLICY;
|
|
12333
|
+
const modelFields = roles.audience === "delivery" ? await fields.getModelFields(request.modelId, executor, versions) : void 0;
|
|
12334
|
+
return buildPolicy(held, request, roles.audience, modelFields);
|
|
12335
|
+
},
|
|
12336
|
+
canPerform: async (principal, action, executor) => {
|
|
12337
|
+
const roles = await rolesOf(principal, grants, executor, versions);
|
|
12338
|
+
if (roles === "all") return true;
|
|
12339
|
+
if (roles.audience === "delivery") return false;
|
|
12340
|
+
const roleIds = NETWORK_ACTION_SET.has(action) ? roles.networkRoleIds : roles.roleIds;
|
|
12341
|
+
return allowsGlobalAction(await grants.getGrants(roleIds, executor, versions), action);
|
|
12342
|
+
},
|
|
12343
|
+
canPerformOnSite: async (principal, action, siteId, executor) => {
|
|
12344
|
+
const roles = await rolesOf(principal, grants, executor, versions);
|
|
12345
|
+
if (roles === "all") return true;
|
|
12346
|
+
if (roles.audience === "delivery") return false;
|
|
12347
|
+
const counted = !NETWORK_ACTION_SET.has(action) || SITE_GRANTABLE_ACTIONS.has(action) && actsOnSite(principal, siteId) ? roles.roleIds : roles.networkRoleIds;
|
|
12348
|
+
return allowsGlobalAction(await grants.getGrants(counted, executor, versions), action);
|
|
12349
|
+
},
|
|
12350
|
+
atVersions: at
|
|
12351
|
+
});
|
|
12352
|
+
return at(void 0);
|
|
12353
|
+
};
|
|
12156
12354
|
//#endregion
|
|
12157
12355
|
//#region src/schema/fieldVisibility.ts
|
|
12158
12356
|
/**
|
|
12159
12357
|
* The permission evaluator's view of field visibility (`public` flags), served from the active schema.
|
|
12160
|
-
* Uses the durable version check on every call
|
|
12161
|
-
*
|
|
12162
|
-
*
|
|
12358
|
+
* Uses the durable version check on every call (or the version the request read once, `versions`), so a
|
|
12359
|
+
* field made non-public is hidden on every instance from the next request, whether or not it got the
|
|
12360
|
+
* notification. Deprecated fields are reported as non-public: they are hidden from the API unless a role
|
|
12361
|
+
* names them explicitly. Components are not delivery models.
|
|
12163
12362
|
*/
|
|
12164
12363
|
const createSchemaFieldVisibility = (registry) => {
|
|
12165
|
-
const activeModel = async (modelId, executor) => {
|
|
12166
|
-
const active = (await registry.getSnapshot(executor)).byId.get(modelId);
|
|
12364
|
+
const activeModel = async (modelId, executor, versions) => {
|
|
12365
|
+
const active = (await registry.getSnapshot(executor, versions)).byId.get(modelId);
|
|
12167
12366
|
return active && active.definition.kind !== "component" ? active.definition : void 0;
|
|
12168
12367
|
};
|
|
12169
12368
|
return {
|
|
12170
|
-
getModelFields: async (modelId, executor) => (await activeModel(modelId, executor))?.fields.map((field) => ({
|
|
12369
|
+
getModelFields: async (modelId, executor, versions) => (await activeModel(modelId, executor, versions))?.fields.map((field) => ({
|
|
12171
12370
|
id: field.id,
|
|
12172
12371
|
public: field.public && !field.deprecated
|
|
12173
12372
|
})),
|
|
12174
|
-
hasModel: async (modelId, executor) => await activeModel(modelId, executor) !== void 0,
|
|
12175
|
-
getModelSite: async (modelId, executor) => (await registry.getSnapshot(executor)).scopeOf(modelId) ?? null
|
|
12373
|
+
hasModel: async (modelId, executor, versions) => await activeModel(modelId, executor, versions) !== void 0,
|
|
12374
|
+
getModelSite: async (modelId, executor, versions) => (await registry.getSnapshot(executor, versions)).scopeOf(modelId) ?? null
|
|
12176
12375
|
};
|
|
12177
12376
|
};
|
|
12178
12377
|
//#endregion
|
|
@@ -12214,8 +12413,8 @@ const createSchemaRegistry = ({ db, log }) => {
|
|
|
12214
12413
|
}
|
|
12215
12414
|
};
|
|
12216
12415
|
return {
|
|
12217
|
-
getSnapshot: async (executor) => {
|
|
12218
|
-
const version = await getSchemaVersion(executor ?? db);
|
|
12416
|
+
getSnapshot: async (executor, versions) => {
|
|
12417
|
+
const version = versions?.schemaVersion ?? await getSchemaVersion(executor ?? db);
|
|
12219
12418
|
if (cached && cached.version >= version) return cached;
|
|
12220
12419
|
return executor ? readSnapshot(executor) : loadAtLeast(version);
|
|
12221
12420
|
},
|
|
@@ -12645,6 +12844,25 @@ const selectFields = (model, mask, selected) => {
|
|
|
12645
12844
|
const allowed = model.definition.fields.filter((field) => maskAllows(mask, field));
|
|
12646
12845
|
return selected ? allowed.filter((field) => selected.some((choice) => choice.id === field.id)) : allowed;
|
|
12647
12846
|
};
|
|
12847
|
+
/**
|
|
12848
|
+
* Renders a delivered rich-text value's HTML on demand (GraphQL's `RichText.html` resolver). A symbol key, so
|
|
12849
|
+
* `JSON.stringify` never writes it: REST responses carry `html` only when the request asked for it.
|
|
12850
|
+
*/
|
|
12851
|
+
const RICH_TEXT_HTML = Symbol("shapio.richTextHtml");
|
|
12852
|
+
const projectRichText = (document, mode, options) => {
|
|
12853
|
+
const render = () => renderRichTextHtml(document, (id) => options.mediaAssets?.get(id)?.url);
|
|
12854
|
+
const { doc, ...envelope } = document;
|
|
12855
|
+
const shaped = mode === "json" ? { ...document } : mode === "html" ? {
|
|
12856
|
+
...envelope,
|
|
12857
|
+
html: render()
|
|
12858
|
+
} : {
|
|
12859
|
+
...envelope,
|
|
12860
|
+
doc,
|
|
12861
|
+
html: render()
|
|
12862
|
+
};
|
|
12863
|
+
shaped[RICH_TEXT_HTML] = render;
|
|
12864
|
+
return shaped;
|
|
12865
|
+
};
|
|
12648
12866
|
const isRecord$8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12649
12867
|
const isListField = (field) => field.type === "relation" && field.settings.cardinality === "many" || field.type === "media" && field.settings.multiple || field.type === "component" && field.settings.repeatable || field.type === "dynamiczone";
|
|
12650
12868
|
const projectRelation = (field, value, options, topLevel) => {
|
|
@@ -12694,10 +12912,7 @@ const projectValue = (field, value, options, topLevel) => {
|
|
|
12694
12912
|
...projected
|
|
12695
12913
|
}] : [];
|
|
12696
12914
|
});
|
|
12697
|
-
case "richtext": return options.
|
|
12698
|
-
...value,
|
|
12699
|
-
html: renderRichTextHtml(value, (id) => options.mediaAssets?.get(id)?.url)
|
|
12700
|
-
} : value;
|
|
12915
|
+
case "richtext": return options.richText ? projectRichText(value, options.richText, options) : value;
|
|
12701
12916
|
case "media": {
|
|
12702
12917
|
if (!options.mediaAssets) return value;
|
|
12703
12918
|
const assets = (Array.isArray(value) ? value : [value]).flatMap((id) => {
|
|
@@ -12774,7 +12989,6 @@ const toEntryData = (model, data) => data === void 0 ? void 0 : projectData(data
|
|
|
12774
12989
|
fields: model.definition.fields.filter((field) => !field.deprecated),
|
|
12775
12990
|
visibleTargets: null,
|
|
12776
12991
|
populated: /* @__PURE__ */ new Map(),
|
|
12777
|
-
richTextHtml: false,
|
|
12778
12992
|
mediaAssets: null
|
|
12779
12993
|
});
|
|
12780
12994
|
const reject = (message, details) => {
|
|
@@ -12973,11 +13187,11 @@ const unknownKeyMessage = (path) => {
|
|
|
12973
13187
|
if (segments[0] === "themes" && segments.length === 4) return `unknown theme token "${key}"; the tokens are listed in documentation/extensions.md`;
|
|
12974
13188
|
return `unknown key "${key}"`;
|
|
12975
13189
|
};
|
|
12976
|
-
const keysOf = (value) => isRecord$6(value) ? Object.keys(value) : [];
|
|
13190
|
+
const keysOf$1 = (value) => isRecord$6(value) ? Object.keys(value) : [];
|
|
12977
13191
|
/** Rules TypeBox cannot express (key patterns, uniqueness, reserved names); tolerant of a malformed config. */
|
|
12978
13192
|
const findNamingProblems = (config) => {
|
|
12979
13193
|
const problems = [];
|
|
12980
|
-
for (const key of keysOf(config.hooks)) if (!HOOK_MODEL_KEY_PATTERN.test(key)) problems.push({
|
|
13194
|
+
for (const key of keysOf$1(config.hooks)) if (!HOOK_MODEL_KEY_PATTERN.test(key)) problems.push({
|
|
12981
13195
|
path: `/hooks/${key}`,
|
|
12982
13196
|
message: "must be a model API ID, a model UUID or \"*\""
|
|
12983
13197
|
});
|
|
@@ -12994,7 +13208,7 @@ const findNamingProblems = (config) => {
|
|
|
12994
13208
|
});
|
|
12995
13209
|
prefixes.add(route.prefix);
|
|
12996
13210
|
});
|
|
12997
|
-
for (const name of keysOf(config.services)) if (!SERVICE_NAME_PATTERN.test(name)) problems.push({
|
|
13211
|
+
for (const name of keysOf$1(config.services)) if (!SERVICE_NAME_PATTERN.test(name)) problems.push({
|
|
12998
13212
|
path: `/services/${name}`,
|
|
12999
13213
|
message: "must be a JavaScript identifier"
|
|
13000
13214
|
});
|
|
@@ -13002,7 +13216,7 @@ const findNamingProblems = (config) => {
|
|
|
13002
13216
|
path: `/services/${name}`,
|
|
13003
13217
|
message: `"${name}" is one of Shapio's own services`
|
|
13004
13218
|
});
|
|
13005
|
-
for (const name of keysOf(config.jobs)) if (!JOB_NAME_PATTERN.test(name)) problems.push({
|
|
13219
|
+
for (const name of keysOf$1(config.jobs)) if (!JOB_NAME_PATTERN.test(name)) problems.push({
|
|
13006
13220
|
path: `/jobs/${name}`,
|
|
13007
13221
|
message: "must start with a letter and use letters, digits, \".\", \"_\" or \"-\""
|
|
13008
13222
|
});
|
|
@@ -13323,6 +13537,20 @@ const SYSTEM_ATTRIBUTES = [
|
|
|
13323
13537
|
"createdAt",
|
|
13324
13538
|
"updatedAt"
|
|
13325
13539
|
];
|
|
13540
|
+
/**
|
|
13541
|
+
* Delivery shapes of a rich-text value (ADR 0003): `json` the stored document `{ format, version, doc }`,
|
|
13542
|
+
* `html` `{ format, version, html }` (sanitized HTML rendered from the document), `both` the document plus
|
|
13543
|
+
* `html`.
|
|
13544
|
+
*/
|
|
13545
|
+
const RICH_TEXT_MODES = [
|
|
13546
|
+
"json",
|
|
13547
|
+
"html",
|
|
13548
|
+
"both"
|
|
13549
|
+
];
|
|
13550
|
+
/** What a delivery read returns for rich text when the request does not say (`?richText=`). */
|
|
13551
|
+
const DEFAULT_RICH_TEXT_MODE = "json";
|
|
13552
|
+
/** `?seo=`: SEO fields as stored, or merged with the site's SEO defaults (plan seo-fields). */
|
|
13553
|
+
const SEO_MODES = ["raw", "resolved"];
|
|
13326
13554
|
const ENTRY_LIST_STATUSES = [
|
|
13327
13555
|
"draft",
|
|
13328
13556
|
"published",
|
|
@@ -13599,7 +13827,7 @@ const compileAuthorCondition = (adminUserId, dialect = contentDialect()) => sql`
|
|
|
13599
13827
|
const compileSearch = (fieldId, text, dialect = contentDialect()) => containsInsensitiveExpression(fieldId, text, dialect);
|
|
13600
13828
|
const SNAPSHOT_CTE = "content_snapshot";
|
|
13601
13829
|
/** The FROM item for a source, aliased `alias`, and the CTE it needs (snapshots only). */
|
|
13602
|
-
const sourceOf$
|
|
13830
|
+
const sourceOf$2 = (source, modelId, siteId, dialect) => {
|
|
13603
13831
|
if (source.kind === "heads") return {
|
|
13604
13832
|
cte: null,
|
|
13605
13833
|
table: sql.table("entry_heads")
|
|
@@ -13648,20 +13876,46 @@ const entryScope = (plan, dialect) => plan.orderedByEntry === true && plan.condi
|
|
|
13648
13876
|
* alone. A snapshot keeps the join: the publication log still lists entries deleted since.
|
|
13649
13877
|
*/
|
|
13650
13878
|
const countFrom = (plan, table, where, joined) => plan.source.kind === "heads" && plan.conditions.length === 0 ? sql`from ${table} h where ${where}` : joined;
|
|
13651
|
-
|
|
13652
|
-
|
|
13653
|
-
|
|
13879
|
+
const HEAD_COLUMNS$1 = sql`h.entry_id, h.locale, h.data, h.version, h.revision_id, h.updated_at,
|
|
13880
|
+
h.autosaved_at, e.created_at, e.updated_at as entry_updated_at, e.created_by_admin_id, e.owner_app_user_id`;
|
|
13881
|
+
/** The parts every statement over a plan's heads shares. */
|
|
13882
|
+
const headQueryParts = (plan, dialect) => {
|
|
13883
|
+
const { cte, table } = sourceOf$2(plan.source, plan.modelId, plan.siteId, dialect);
|
|
13654
13884
|
const where = whereOf(plan, table, dialect);
|
|
13655
13885
|
const from = sql`from ${table} h join entries e on e.id = h.entry_id and e.deleted_at is null where ${where}`;
|
|
13656
|
-
const prefix = cte ?? sql``;
|
|
13657
13886
|
const orderBy = plan.orderBy.length > 0 ? sql` order by ${sql.join([...plan.orderBy])}` : sql``;
|
|
13658
13887
|
const limit = plan.limit !== void 0 ? sql` limit ${plan.limit}` : sql``;
|
|
13659
13888
|
const offset = plan.offset ? sql` offset ${plan.offset}` : sql``;
|
|
13660
13889
|
return {
|
|
13661
|
-
|
|
13662
|
-
|
|
13663
|
-
|
|
13664
|
-
|
|
13890
|
+
prefix: cte ?? sql``,
|
|
13891
|
+
rows: sql`${from}${entryScope(plan, dialect)}${orderBy}${limit}${offset}`,
|
|
13892
|
+
countFrom: countFrom(plan, table, where, from)
|
|
13893
|
+
};
|
|
13894
|
+
};
|
|
13895
|
+
/** The SELECT for a plan: rows, plus the matching COUNT for pagination. */
|
|
13896
|
+
const compileHeadQuery = (plan, dialect = contentDialect()) => {
|
|
13897
|
+
const { prefix, rows, countFrom: counted } = headQueryParts(plan, dialect);
|
|
13898
|
+
return {
|
|
13899
|
+
rows: sql`${prefix}select ${HEAD_COLUMNS$1}
|
|
13900
|
+
${rows}`,
|
|
13901
|
+
count: sql`${prefix}select count(*) as total ${counted}`
|
|
13902
|
+
};
|
|
13903
|
+
};
|
|
13904
|
+
/**
|
|
13905
|
+
* One delivery read as a single statement (plan delivery-perf): the page's heads with, on every row, the
|
|
13906
|
+
* matching total and the site's current publication sequence (`publication_state.last_seq`), so all three
|
|
13907
|
+
* come from one snapshot of the database on every dialect without a transaction. The uncorrelated
|
|
13908
|
+
* subqueries run once per statement. An empty page carries no row, so `meta` reads the total and the
|
|
13909
|
+
* sequence alone, for callers that read it in the same transaction as `rows`.
|
|
13910
|
+
*/
|
|
13911
|
+
const compileHeadPage = (plan, options, dialect = contentDialect()) => {
|
|
13912
|
+
const { prefix, rows, countFrom: counted } = headQueryParts(plan, dialect);
|
|
13913
|
+
const total = options.total ? sql`(select count(*) ${counted})` : sql`null`;
|
|
13914
|
+
const seq = sql`(select ps.last_seq from publication_state ps where ps.site_id = ${dialect.uuid(plan.siteId)})`;
|
|
13915
|
+
return {
|
|
13916
|
+
rows: sql`${prefix}select ${HEAD_COLUMNS$1}, ${total} as page_total, ${seq} as page_seq
|
|
13917
|
+
${rows}`,
|
|
13918
|
+
meta: sql`${prefix}select ${total} as page_total, ${seq} as page_seq`
|
|
13665
13919
|
};
|
|
13666
13920
|
};
|
|
13667
13921
|
//#endregion
|
|
@@ -13753,6 +14007,8 @@ const TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
13753
14007
|
"populate",
|
|
13754
14008
|
"locale",
|
|
13755
14009
|
"snapshot",
|
|
14010
|
+
"richText",
|
|
14011
|
+
"seo",
|
|
13756
14012
|
"q",
|
|
13757
14013
|
"status",
|
|
13758
14014
|
"author"
|
|
@@ -13911,6 +14167,27 @@ const parseAdminFilters = (tree, context) => {
|
|
|
13911
14167
|
...author !== void 0 ? { author } : {}
|
|
13912
14168
|
};
|
|
13913
14169
|
};
|
|
14170
|
+
const RICH_TEXT_MODE_SET = new Set(RICH_TEXT_MODES);
|
|
14171
|
+
/** `?richText=json|html|both` on delivery and preview reads; the default there, nothing on admin reads. */
|
|
14172
|
+
const parseRichText = (tree, context) => {
|
|
14173
|
+
const value = single(tree.richText, "richText");
|
|
14174
|
+
if (!context.allowRichText) {
|
|
14175
|
+
if (value !== void 0) throw queryInvalid("richText is only available on the delivery and preview APIs");
|
|
14176
|
+
return {};
|
|
14177
|
+
}
|
|
14178
|
+
if (value !== void 0 && !RICH_TEXT_MODE_SET.has(value)) throw queryInvalid(`"richText" must be one of ${RICH_TEXT_MODES.join(", ")}`);
|
|
14179
|
+
return { richText: value ?? "json" };
|
|
14180
|
+
};
|
|
14181
|
+
const SEO_MODE_SET = new Set(SEO_MODES);
|
|
14182
|
+
/** `?seo=raw|resolved` on delivery and preview reads; `resolved` needs a readable SEO field on the model. */
|
|
14183
|
+
const parseSeo = (tree, context) => {
|
|
14184
|
+
const value = single(tree.seo, "seo");
|
|
14185
|
+
if (value === void 0) return {};
|
|
14186
|
+
if (!context.allowSeo) throw queryInvalid("seo is only available on the delivery and preview APIs");
|
|
14187
|
+
if (!SEO_MODE_SET.has(value)) throw queryInvalid(`"seo" must be one of ${SEO_MODES.join(", ")}`);
|
|
14188
|
+
if (value === "resolved" && !context.model.fields.some((field) => !field.deprecated && isSeoField(field) && context.isReadable(field))) throw queryInvalid(`"${context.model.apiKey}" has no SEO field to resolve`);
|
|
14189
|
+
return { seo: value };
|
|
14190
|
+
};
|
|
13914
14191
|
const parseContentQuery = (tree, context) => {
|
|
13915
14192
|
for (const key of Object.keys(tree)) if (!TOP_LEVEL_KEYS.has(key)) throw queryInvalid(`Unknown query parameter "${key}"`);
|
|
13916
14193
|
const filters = tree.filters;
|
|
@@ -13932,6 +14209,8 @@ const parseContentQuery = (tree, context) => {
|
|
|
13932
14209
|
populate: parsePopulate(context, tree.populate),
|
|
13933
14210
|
locale,
|
|
13934
14211
|
snapshot,
|
|
14212
|
+
...parseRichText(tree, context),
|
|
14213
|
+
...parseSeo(tree, context),
|
|
13935
14214
|
...parseAdminFilters(tree, context)
|
|
13936
14215
|
};
|
|
13937
14216
|
};
|
|
@@ -14060,6 +14339,29 @@ const outdatedSharedLocales = (model, heads) => model.localized ? heads.filter((
|
|
|
14060
14339
|
*/
|
|
14061
14340
|
const runHeadQuery = async (query, executor = db) => (await query.execute(executor)).rows;
|
|
14062
14341
|
const runCountQuery = async (query, executor = db) => Number((await query.execute(executor)).rows[0]?.total ?? 0);
|
|
14342
|
+
const toPageMeta = (row) => ({
|
|
14343
|
+
total: row.page_total === null ? null : Number(row.page_total),
|
|
14344
|
+
seq: row.page_seq === null ? null : Number(row.page_seq)
|
|
14345
|
+
});
|
|
14346
|
+
/**
|
|
14347
|
+
* Runs a head page compiled by `compileHeadPage`: the rows, and the total and sequence carried on them (null
|
|
14348
|
+
* when the page is empty; then `runHeadPageMeta` in the same transaction reads them).
|
|
14349
|
+
*/
|
|
14350
|
+
const runHeadPage = async (query, executor = db) => {
|
|
14351
|
+
const result = (await query.execute(executor)).rows;
|
|
14352
|
+
const [first] = result;
|
|
14353
|
+
return {
|
|
14354
|
+
rows: result.map(({ page_total: _total, page_seq: _seq, ...row }) => row),
|
|
14355
|
+
meta: first ? toPageMeta(first) : null
|
|
14356
|
+
};
|
|
14357
|
+
};
|
|
14358
|
+
const runHeadPageMeta = async (query, executor = db) => {
|
|
14359
|
+
const [row] = (await query.execute(executor)).rows;
|
|
14360
|
+
return row ? toPageMeta(row) : {
|
|
14361
|
+
total: null,
|
|
14362
|
+
seq: null
|
|
14363
|
+
};
|
|
14364
|
+
};
|
|
14063
14365
|
/** Runs `fn` in one REPEATABLE READ, read-only transaction: a response reads one consistent moment. */
|
|
14064
14366
|
const withConsistentRead = (fn, database = db) => database.transaction().setIsolationLevel("repeatable read").setAccessMode("read only").execute(fn);
|
|
14065
14367
|
//#endregion
|
|
@@ -14105,6 +14407,51 @@ const populateRelations = async (env, rows, fields, tree) => {
|
|
|
14105
14407
|
return populated;
|
|
14106
14408
|
};
|
|
14107
14409
|
//#endregion
|
|
14410
|
+
//#region src/content/seo.ts
|
|
14411
|
+
const EXPECTED_TYPES = {
|
|
14412
|
+
title: /* @__PURE__ */ new Set(["string", "text"]),
|
|
14413
|
+
description: /* @__PURE__ */ new Set(["text", "string"]),
|
|
14414
|
+
image: /* @__PURE__ */ new Set(["media"]),
|
|
14415
|
+
canonical: /* @__PURE__ */ new Set(["url", "string"]),
|
|
14416
|
+
noindex: /* @__PURE__ */ new Set(["boolean"])
|
|
14417
|
+
};
|
|
14418
|
+
const keysOf = (model) => {
|
|
14419
|
+
const component = model.components.get(SEO_COMPONENT_ID)?.definition;
|
|
14420
|
+
const keys = {};
|
|
14421
|
+
for (const [key, id] of Object.entries(SEO_FIELD_IDS)) {
|
|
14422
|
+
const field = component?.fields.find((candidate) => candidate.id === id && !candidate.deprecated);
|
|
14423
|
+
const single = field?.type !== "media" || !field.settings.multiple;
|
|
14424
|
+
if (field && single && EXPECTED_TYPES[key].has(field.type)) keys[key] = field.apiKey;
|
|
14425
|
+
}
|
|
14426
|
+
return keys;
|
|
14427
|
+
};
|
|
14428
|
+
/** The entry's own title, only when the caller may read the title field (never a hidden value). */
|
|
14429
|
+
const fallbackTitleOf = (model, policy, row) => {
|
|
14430
|
+
const field = effectiveTitleField(model.definition);
|
|
14431
|
+
if (!field || !maskAllows(policy.readMask, field)) return null;
|
|
14432
|
+
const value = row.data[field.id];
|
|
14433
|
+
return typeof value === "string" ? value : null;
|
|
14434
|
+
};
|
|
14435
|
+
/** The locale whose defaults apply: the head's for localized models, the requested one otherwise. */
|
|
14436
|
+
const defaultsLocaleOf = (env, model, row) => model.definition.localized ? row.locale : env.locale ?? env.snapshot.defaultLocale;
|
|
14437
|
+
/** Whether the selected fields include an SEO field (only those are resolved). */
|
|
14438
|
+
const seoFieldsOf = (fields) => fields.filter(isSeoField);
|
|
14439
|
+
/** Replaces each SEO field's projected value with its resolved value. */
|
|
14440
|
+
const resolveSeoFields = (env, options, model, policy, fields, row, data) => {
|
|
14441
|
+
const seoFields = seoFieldsOf(fields);
|
|
14442
|
+
if (seoFields.length === 0) return data;
|
|
14443
|
+
const keys = keysOf(model);
|
|
14444
|
+
const context = {
|
|
14445
|
+
defaults: seoDefaultsForLocale(options.defaults, fallbackChain(env.snapshot, defaultsLocaleOf(env, model, row))),
|
|
14446
|
+
image: options.image,
|
|
14447
|
+
fallbackTitle: fallbackTitleOf(model, policy, row),
|
|
14448
|
+
keys
|
|
14449
|
+
};
|
|
14450
|
+
const output = { ...data };
|
|
14451
|
+
for (const field of seoFields) output[field.apiKey] = resolveSeo(data[field.apiKey], context);
|
|
14452
|
+
return output;
|
|
14453
|
+
};
|
|
14454
|
+
//#endregion
|
|
14108
14455
|
//#region src/content/read.ts
|
|
14109
14456
|
/** Evaluated through the read's executor: a read inside a transaction never asks the pool for a second connection. */
|
|
14110
14457
|
const readPolicy = (env, modelId) => env.permissions.evaluate(env.actor, {
|
|
@@ -14131,33 +14478,85 @@ const fetchHeadsByIds = async (env, model, policy, ids, locales) => {
|
|
|
14131
14478
|
/**
|
|
14132
14479
|
* Relation targets the caller may see in these rows: for delivery, only targets that are published (at the
|
|
14133
14480
|
* same snapshot) and readable under their model's policy, so a published entry never leaks the ID of a
|
|
14134
|
-
* draft (brief §10). Admin reads see every ID (null).
|
|
14481
|
+
* draft (brief §10). Admin reads see every ID (null). The heads read are returned for populate.
|
|
14135
14482
|
*/
|
|
14136
|
-
const
|
|
14137
|
-
|
|
14483
|
+
const relationTargets = async (env, model, rows, fields) => {
|
|
14484
|
+
const fetched = /* @__PURE__ */ new Map();
|
|
14485
|
+
if (env.audience === "admin") return {
|
|
14486
|
+
visible: null,
|
|
14487
|
+
fetched
|
|
14488
|
+
};
|
|
14138
14489
|
const visible = /* @__PURE__ */ new Set();
|
|
14139
14490
|
const targets = targetsByModel(model, rows.map((row) => row.data), fields);
|
|
14140
14491
|
for (const [targetModelId, ids] of targets) {
|
|
14141
14492
|
const target = resolveModelById(env.snapshot, targetModelId);
|
|
14142
14493
|
if (!target) continue;
|
|
14143
14494
|
const policy = await readPolicy(env, targetModelId);
|
|
14144
|
-
|
|
14495
|
+
const found = await fetchHeadsByIds(env, target, policy, [...ids]);
|
|
14496
|
+
found.forEach((row) => visible.add(row.entry_id));
|
|
14497
|
+
fetched.set(targetModelId, {
|
|
14498
|
+
ids,
|
|
14499
|
+
rows: found
|
|
14500
|
+
});
|
|
14145
14501
|
}
|
|
14146
|
-
return
|
|
14502
|
+
return {
|
|
14503
|
+
visible,
|
|
14504
|
+
fetched
|
|
14505
|
+
};
|
|
14506
|
+
};
|
|
14507
|
+
/**
|
|
14508
|
+
* `fetchHeadsByIds` for populate, answered from the heads relation visibility already read when it asked for
|
|
14509
|
+
* every one of these IDs under the same policy and locale scope (populate's default), else from the database.
|
|
14510
|
+
*/
|
|
14511
|
+
const fetchReusing = (fetched) => async (env, model, policy, ids) => {
|
|
14512
|
+
const known = fetched.get(model.definition.id);
|
|
14513
|
+
if (known && ids.every((id) => known.ids.has(id))) {
|
|
14514
|
+
const wanted = new Set(ids);
|
|
14515
|
+
return known.rows.filter((row) => wanted.has(row.entry_id));
|
|
14516
|
+
}
|
|
14517
|
+
return fetchHeadsByIds(env, model, policy, ids);
|
|
14518
|
+
};
|
|
14519
|
+
/**
|
|
14520
|
+
* Whether projecting these rows reads anything more (relation targets, populated entries, asset views). When
|
|
14521
|
+
* not, a delivery read is complete in the one statement that fetched the rows (plan delivery-perf).
|
|
14522
|
+
*/
|
|
14523
|
+
const needsFollowUpReads = (env, model, rows, query, policy) => {
|
|
14524
|
+
if (query.populate.size > 0) return true;
|
|
14525
|
+
const fields = selectFields(model, policy.readMask, query.fields);
|
|
14526
|
+
const documents = rows.map((row) => row.data);
|
|
14527
|
+
return env.audience === "delivery" && targetsByModel(model, documents, fields).size > 0 || env.media !== void 0 && (mediaIdsOf(model, documents, fields).size > 0 || seoImageIdOf(env, fields) !== void 0);
|
|
14147
14528
|
};
|
|
14529
|
+
/** The site's default social image a `?seo=resolved` read needs (when the response carries an SEO field). */
|
|
14530
|
+
const seoImageIdOf = (env, fields) => env.seo?.imageId && seoFieldsOf(fields).length > 0 ? env.seo.imageId : void 0;
|
|
14531
|
+
/** A default SEO image is shown only while it is a public image (never through a signed URL). */
|
|
14532
|
+
const isPublicImage = (view) => view.visibility === "public" && view.mimeType.startsWith("image/");
|
|
14148
14533
|
/**
|
|
14149
14534
|
* Asset views for the media a response shows (media fields and rich-text images). Private assets get signed,
|
|
14150
14535
|
* expiring URLs (package G); the read mask already decided whether the caller may see the field at all.
|
|
14151
14536
|
* Delivery views carry only what a site needs, never storage keys or internal attribution.
|
|
14152
14537
|
*/
|
|
14153
14538
|
const loadMediaViews = async (env, model, rows, fields) => {
|
|
14154
|
-
if (!env.media) return
|
|
14539
|
+
if (!env.media) return {
|
|
14540
|
+
byId: null,
|
|
14541
|
+
seoImage: null
|
|
14542
|
+
};
|
|
14155
14543
|
const ids = mediaIdsOf(model, rows.map((row) => row.data), fields);
|
|
14156
|
-
|
|
14544
|
+
const seoImageId = seoImageIdOf(env, fields);
|
|
14545
|
+
if (seoImageId !== void 0) ids.add(seoImageId);
|
|
14546
|
+
if (ids.size === 0) return {
|
|
14547
|
+
byId: /* @__PURE__ */ new Map(),
|
|
14548
|
+
seoImage: null
|
|
14549
|
+
};
|
|
14157
14550
|
const assets = await findLiveManyOnSite(env.siteId, [...ids], env.executor);
|
|
14158
14551
|
const views = await toAssetViews(env.media, assets, env.executor);
|
|
14159
|
-
|
|
14552
|
+
const shaped = (view) => env.audience === "delivery" ? toDeliveryAsset(view) : view;
|
|
14553
|
+
const seoImage = views.find((view) => view.id === seoImageId && isPublicImage(view));
|
|
14554
|
+
return {
|
|
14555
|
+
byId: new Map(views.map((view) => [view.id, shaped(view)])),
|
|
14556
|
+
seoImage: seoImage ? shaped(seoImage) : null
|
|
14557
|
+
};
|
|
14160
14558
|
};
|
|
14559
|
+
/** An asset as delivery shows it: what a site needs, never storage keys or internal attribution. */
|
|
14161
14560
|
const toDeliveryAsset = (view) => ({
|
|
14162
14561
|
id: view.id,
|
|
14163
14562
|
filename: view.filename,
|
|
@@ -14189,27 +14588,33 @@ const systemAttributes = (env, row) => ({
|
|
|
14189
14588
|
});
|
|
14190
14589
|
const projectRows = async (env, model, policy, rows, query) => {
|
|
14191
14590
|
const fields = selectFields(model, policy.readMask, query.fields);
|
|
14192
|
-
const visibleTargets = await
|
|
14591
|
+
const { visible: visibleTargets, fetched } = await relationTargets(env, model, rows, fields);
|
|
14193
14592
|
const populateEnv = {
|
|
14194
14593
|
...env,
|
|
14195
14594
|
project: projectRows,
|
|
14196
|
-
fetch:
|
|
14595
|
+
fetch: fetchReusing(fetched),
|
|
14197
14596
|
policyFor: readPolicy,
|
|
14198
14597
|
system: systemAttributes
|
|
14199
14598
|
};
|
|
14200
14599
|
const populated = await populateRelations(populateEnv, rows, fields, query.populate);
|
|
14201
|
-
const
|
|
14202
|
-
return rows.map((row) =>
|
|
14203
|
-
row,
|
|
14204
|
-
data: projectData(row.data, {
|
|
14600
|
+
const media = await loadMediaViews(env, model, rows, fields);
|
|
14601
|
+
return rows.map((row) => {
|
|
14602
|
+
const data = projectData(row.data, {
|
|
14205
14603
|
model,
|
|
14206
14604
|
fields,
|
|
14207
14605
|
visibleTargets,
|
|
14208
14606
|
populated,
|
|
14209
|
-
|
|
14210
|
-
mediaAssets
|
|
14211
|
-
})
|
|
14212
|
-
|
|
14607
|
+
...env.audience === "delivery" ? { richText: env.richText ?? "json" } : {},
|
|
14608
|
+
mediaAssets: media.byId
|
|
14609
|
+
});
|
|
14610
|
+
return {
|
|
14611
|
+
row,
|
|
14612
|
+
data: env.seo ? resolveSeoFields(env, {
|
|
14613
|
+
defaults: env.seo,
|
|
14614
|
+
image: media.seoImage
|
|
14615
|
+
}, model, policy, fields, row, data) : data
|
|
14616
|
+
};
|
|
14617
|
+
});
|
|
14213
14618
|
};
|
|
14214
14619
|
//#endregion
|
|
14215
14620
|
//#region src/services/contentAccess.ts
|
|
@@ -14419,6 +14824,8 @@ const parseQueryFor = (context, model, policy, rawQuery, options) => parseConten
|
|
|
14419
14824
|
isReadable: (field) => maskAllows(policy.readMask, field),
|
|
14420
14825
|
allowSnapshot: options.allowSnapshot,
|
|
14421
14826
|
allowAdminFilters: options.allowAdminFilters ?? false,
|
|
14827
|
+
allowRichText: options.allowRichText ?? false,
|
|
14828
|
+
allowSeo: options.allowSeo ?? false,
|
|
14422
14829
|
resolveModel: (modelId) => resolveModelById(context.snapshot, modelId)?.definition
|
|
14423
14830
|
});
|
|
14424
14831
|
/** The admin content list: drafts, filtered/sorted/paginated by the same compiler as delivery. */
|
|
@@ -15439,19 +15846,19 @@ const toSiteRef = (row) => ({
|
|
|
15439
15846
|
id: row.id,
|
|
15440
15847
|
key: row.key
|
|
15441
15848
|
});
|
|
15442
|
-
const siteNotFound = () => new AppError(404, "SITE_NOT_FOUND", "Site not found");
|
|
15849
|
+
const siteNotFound$1 = () => new AppError(404, "SITE_NOT_FOUND", "Site not found");
|
|
15443
15850
|
/** A site's summary by ID (`me` for a site the admin holds no role on: they see its name, nothing else). */
|
|
15444
15851
|
/** How many sites the instance has, whatever roles the caller holds (one: per-site schema UI is moot). */
|
|
15445
15852
|
const countSites = () => count();
|
|
15446
15853
|
const getSiteSummary = async (siteId) => {
|
|
15447
15854
|
const site = await findById$12(siteId);
|
|
15448
|
-
if (!site) throw siteNotFound();
|
|
15855
|
+
if (!site) throw siteNotFound$1();
|
|
15449
15856
|
return toSiteSummary(site);
|
|
15450
15857
|
};
|
|
15451
15858
|
/** The reference of a site by ID (jobs and deferred work carry the ID of the site they belong to). */
|
|
15452
15859
|
const getSiteRef = async (siteId, executor = db) => {
|
|
15453
15860
|
const site = await findById$12(siteId, executor);
|
|
15454
|
-
if (!site) throw siteNotFound();
|
|
15861
|
+
if (!site) throw siteNotFound$1();
|
|
15455
15862
|
return toSiteRef(site);
|
|
15456
15863
|
};
|
|
15457
15864
|
/**
|
|
@@ -15465,7 +15872,7 @@ const listAccessibleSites = async (principal) => {
|
|
|
15465
15872
|
const listSites$1 = async (principal) => (await listAccessibleSites(principal)).map(toSiteView);
|
|
15466
15873
|
const getSite$1 = async (principal, id) => {
|
|
15467
15874
|
const site = (await listAccessibleSites(principal)).find((row) => row.id === id);
|
|
15468
|
-
if (!site) throw siteNotFound();
|
|
15875
|
+
if (!site) throw siteNotFound$1();
|
|
15469
15876
|
return toSiteView(site);
|
|
15470
15877
|
};
|
|
15471
15878
|
/** Every site, primary first (`shapio sites list`, which runs on the server host without a principal). */
|
|
@@ -15501,7 +15908,7 @@ const createSite$1 = async (context, input, database = db) => {
|
|
|
15501
15908
|
/** Optimistic: fails with VERSION_CONFLICT when the site changed since `expectedVersion` was read. */
|
|
15502
15909
|
const renameSite = async (context, id, input) => db.transaction().execute(async (trx) => {
|
|
15503
15910
|
const current = await findById$12(id, trx);
|
|
15504
|
-
if (!current) throw siteNotFound();
|
|
15911
|
+
if (!current) throw siteNotFound$1();
|
|
15505
15912
|
const updated = await renameIfVersion(id, input.expectedVersion, input.name.trim(), trx);
|
|
15506
15913
|
if (!updated) throw new AppError(409, "VERSION_CONFLICT", "The site changed since you loaded it", {
|
|
15507
15914
|
expectedVersion: input.expectedVersion,
|
|
@@ -15529,7 +15936,7 @@ const renameSite = async (context, id, input) => db.transaction().execute(async
|
|
|
15529
15936
|
const deleteSite$1 = async (context, id) => {
|
|
15530
15937
|
await db.transaction().execute(async (trx) => {
|
|
15531
15938
|
const site = await lockById$4(id, trx);
|
|
15532
|
-
if (!site) throw siteNotFound();
|
|
15939
|
+
if (!site) throw siteNotFound$1();
|
|
15533
15940
|
if (site.is_primary) throw new AppError(409, "SITE_IS_PRIMARY", "The primary site cannot be deleted");
|
|
15534
15941
|
await lockSchema(trx);
|
|
15535
15942
|
const contents = {
|
|
@@ -15558,24 +15965,171 @@ const deleteSite$1 = async (context, id) => {
|
|
|
15558
15965
|
});
|
|
15559
15966
|
});
|
|
15560
15967
|
};
|
|
15968
|
+
/** The site a lookup names, read on its own (a request whose state was read without its site). */
|
|
15969
|
+
const findSite = async (lookup) => {
|
|
15970
|
+
switch (lookup.by) {
|
|
15971
|
+
case "id": return findById$12(lookup.id).then((row) => row && toSiteRef(row));
|
|
15972
|
+
case "key": return findByKey(lookup.key).then((row) => row && toSiteRef(row));
|
|
15973
|
+
case "primary": return toSiteRef(await findPrimary());
|
|
15974
|
+
}
|
|
15975
|
+
};
|
|
15561
15976
|
/**
|
|
15562
15977
|
* Which site a request is about (sites plan §H): the credential's site, else the site the request names,
|
|
15563
15978
|
* else the primary site. A request that names a different site than its credential is refused
|
|
15564
15979
|
* (403 `SITE_MISMATCH`), never redirected; an unknown key is 404 `SITE_NOT_FOUND`.
|
|
15980
|
+
* `read`: where the site is read; a request passes its once-per-request state read
|
|
15981
|
+
* (`repositories/requestState.ts`), which reads the site with the versions in one statement.
|
|
15565
15982
|
*/
|
|
15566
|
-
const resolveSite = async ({ credentialSiteId, requestedKey }) => {
|
|
15983
|
+
const resolveSite = async ({ credentialSiteId, requestedKey }, read = findSite) => {
|
|
15567
15984
|
if (credentialSiteId !== void 0) {
|
|
15568
|
-
const site = await
|
|
15569
|
-
|
|
15985
|
+
const site = await read({
|
|
15986
|
+
by: "id",
|
|
15987
|
+
id: credentialSiteId
|
|
15988
|
+
});
|
|
15989
|
+
if (!site) throw siteNotFound$1();
|
|
15570
15990
|
if (requestedKey !== void 0 && requestedKey !== site.key) throw new AppError(403, "SITE_MISMATCH", `This credential belongs to site "${site.key}", not "${requestedKey}"`);
|
|
15571
|
-
return
|
|
15991
|
+
return site;
|
|
15572
15992
|
}
|
|
15573
15993
|
if (requestedKey !== void 0) {
|
|
15574
|
-
const site = await
|
|
15994
|
+
const site = await read({
|
|
15995
|
+
by: "key",
|
|
15996
|
+
key: requestedKey
|
|
15997
|
+
});
|
|
15575
15998
|
if (!site) throw new AppError(404, "SITE_NOT_FOUND", `No site has the key "${requestedKey}"`);
|
|
15576
|
-
return
|
|
15999
|
+
return site;
|
|
15577
16000
|
}
|
|
15578
|
-
|
|
16001
|
+
const primary = await read({ by: "primary" });
|
|
16002
|
+
if (!primary) throw new Error("No primary site");
|
|
16003
|
+
return primary;
|
|
16004
|
+
};
|
|
16005
|
+
//#endregion
|
|
16006
|
+
//#region src/permissions/memo.ts
|
|
16007
|
+
/**
|
|
16008
|
+
* One request's evaluator, memoized per (principal, action, model): a request touches the same models many
|
|
16009
|
+
* times (REST: the read, relation visibility, populate, usage; GraphQL: lists, relation batches,
|
|
16010
|
+
* localizations), and evaluates each policy once (ADR 0005). Calls for another principal bypass the memo.
|
|
16011
|
+
* A policy still being evaluated is shared only on the executor that evaluates it: one evaluated inside a
|
|
16012
|
+
* read transaction is never awaited from another transaction. Once settled, every executor reuses it.
|
|
16013
|
+
*/
|
|
16014
|
+
const memoizePermissions = (base, principal) => {
|
|
16015
|
+
const settled = /* @__PURE__ */ new Map();
|
|
16016
|
+
const pooled = /* @__PURE__ */ new Map();
|
|
16017
|
+
const byTransaction = /* @__PURE__ */ new WeakMap();
|
|
16018
|
+
const pendingFor = (executor) => {
|
|
16019
|
+
if (!executor) return pooled;
|
|
16020
|
+
let pending = byTransaction.get(executor);
|
|
16021
|
+
if (!pending) {
|
|
16022
|
+
pending = /* @__PURE__ */ new Map();
|
|
16023
|
+
byTransaction.set(executor, pending);
|
|
16024
|
+
}
|
|
16025
|
+
return pending;
|
|
16026
|
+
};
|
|
16027
|
+
return {
|
|
16028
|
+
evaluate: (caller, request, executor) => {
|
|
16029
|
+
if (caller !== principal) return base.evaluate(caller, request, executor);
|
|
16030
|
+
const key = `${request.action}:${request.modelId}`;
|
|
16031
|
+
const done = settled.get(key);
|
|
16032
|
+
if (done) return Promise.resolve(done);
|
|
16033
|
+
const pending = pendingFor(executor);
|
|
16034
|
+
let policy = pending.get(key);
|
|
16035
|
+
if (!policy) {
|
|
16036
|
+
policy = base.evaluate(caller, request, executor);
|
|
16037
|
+
pending.set(key, policy);
|
|
16038
|
+
policy.then((value) => settled.set(key, value), () => void 0);
|
|
16039
|
+
}
|
|
16040
|
+
return policy;
|
|
16041
|
+
},
|
|
16042
|
+
canPerform: (caller, action, executor) => base.canPerform(caller, action, executor),
|
|
16043
|
+
canPerformOnSite: (caller, action, siteId, executor) => base.canPerformOnSite(caller, action, siteId, executor)
|
|
16044
|
+
};
|
|
16045
|
+
};
|
|
16046
|
+
//#endregion
|
|
16047
|
+
//#region src/repositories/requestState.ts
|
|
16048
|
+
/**
|
|
16049
|
+
* Everything a request checks before its work, in one statement: the durable schema and permissions versions
|
|
16050
|
+
* (`system_versions`, one row) and, when asked, the request's site. Read once per request on the pool, before
|
|
16051
|
+
* any transaction; caches compare against these versions for the rest of the request.
|
|
16052
|
+
*/
|
|
16053
|
+
const read = async (lookup, trx = db) => {
|
|
16054
|
+
if (!lookup) {
|
|
16055
|
+
const row = await trx.selectFrom("system_versions").select(["schema_version", "permissions_version"]).executeTakeFirstOrThrow();
|
|
16056
|
+
return {
|
|
16057
|
+
schemaVersion: row.schema_version,
|
|
16058
|
+
permissionsVersion: row.permissions_version,
|
|
16059
|
+
site: void 0
|
|
16060
|
+
};
|
|
16061
|
+
}
|
|
16062
|
+
const row = await trx.selectFrom("system_versions").leftJoin("sites", (join) => {
|
|
16063
|
+
switch (lookup.by) {
|
|
16064
|
+
case "id": return join.on("sites.id", "=", lookup.id);
|
|
16065
|
+
case "key": return join.on("sites.key", "=", lookup.key);
|
|
16066
|
+
case "primary": return join.on("sites.is_primary", "=", true);
|
|
16067
|
+
}
|
|
16068
|
+
}).select([
|
|
16069
|
+
"system_versions.schema_version",
|
|
16070
|
+
"system_versions.permissions_version",
|
|
16071
|
+
"sites.id as site_id",
|
|
16072
|
+
"sites.key as site_key"
|
|
16073
|
+
]).executeTakeFirstOrThrow();
|
|
16074
|
+
return {
|
|
16075
|
+
schemaVersion: row.schema_version,
|
|
16076
|
+
permissionsVersion: row.permissions_version,
|
|
16077
|
+
site: row.site_id !== null && row.site_key !== null ? {
|
|
16078
|
+
id: row.site_id,
|
|
16079
|
+
key: row.site_key
|
|
16080
|
+
} : void 0
|
|
16081
|
+
};
|
|
16082
|
+
};
|
|
16083
|
+
//#endregion
|
|
16084
|
+
//#region src/services/requestState.ts
|
|
16085
|
+
/** One statement on the pool: the schema and permissions versions, and the request's site when asked. */
|
|
16086
|
+
const readRequestState = async (lookup) => {
|
|
16087
|
+
const row = await read(lookup);
|
|
16088
|
+
return {
|
|
16089
|
+
versions: {
|
|
16090
|
+
schemaVersion: row.schemaVersion,
|
|
16091
|
+
permissionsVersion: row.permissionsVersion
|
|
16092
|
+
},
|
|
16093
|
+
site: row.site
|
|
16094
|
+
};
|
|
16095
|
+
};
|
|
16096
|
+
//#endregion
|
|
16097
|
+
//#region src/plugins/requestState.ts
|
|
16098
|
+
/**
|
|
16099
|
+
* The durable schema and permissions versions this request checks its caches against, read once on the pool
|
|
16100
|
+
* before any transaction (ADR 0002, ADR 0005): a change committed after the read applies from the next
|
|
16101
|
+
* request. The first caller reads; site resolution (an `onRequest` hook, before any other caller on a site
|
|
16102
|
+
* route) passes its site lookup so the site is read in the same statement.
|
|
16103
|
+
*/
|
|
16104
|
+
const getRequestState = (request, lookup) => {
|
|
16105
|
+
request.requestState ??= readRequestState(lookup);
|
|
16106
|
+
return request.requestState;
|
|
16107
|
+
};
|
|
16108
|
+
/** Reads the request's site with its state, or on its own when the state was already read without it. */
|
|
16109
|
+
const readRequestSite = async (request, lookup) => request.requestState ? findSite(lookup) : (await getRequestState(request, lookup)).site;
|
|
16110
|
+
/**
|
|
16111
|
+
* The server's evaluator at the request's versions. Inside a transaction before the state was read (no route
|
|
16112
|
+
* does that today), the unbound evaluator checks the versions itself through the transaction, so no pooled
|
|
16113
|
+
* connection is asked for while the transaction holds one.
|
|
16114
|
+
*/
|
|
16115
|
+
const boundPermissions = async (request, executor) => {
|
|
16116
|
+
const base = request.server.permissions;
|
|
16117
|
+
if (executor && !request.requestState) return base;
|
|
16118
|
+
const { versions } = await getRequestState(request);
|
|
16119
|
+
return base.atVersions?.(versions) ?? base;
|
|
16120
|
+
};
|
|
16121
|
+
/**
|
|
16122
|
+
* The request's evaluator: checked against the request's versions, and memoized per (principal, action,
|
|
16123
|
+
* model) for the request (`permissions/memo.ts`), for REST as for GraphQL. Create it after site resolution,
|
|
16124
|
+
* which settles the principal.
|
|
16125
|
+
*/
|
|
16126
|
+
const getRequestPermissions = (request) => {
|
|
16127
|
+
request.requestPermissions ??= memoizePermissions({
|
|
16128
|
+
evaluate: async (principal, policyRequest, executor) => (await boundPermissions(request, executor)).evaluate(principal, policyRequest, executor),
|
|
16129
|
+
canPerform: async (principal, action, executor) => (await boundPermissions(request, executor)).canPerform(principal, action, executor),
|
|
16130
|
+
canPerformOnSite: async (principal, action, siteId, executor) => (await boundPermissions(request, executor)).canPerformOnSite(principal, action, siteId, executor)
|
|
16131
|
+
}, request.principal);
|
|
16132
|
+
return request.requestPermissions;
|
|
15579
16133
|
};
|
|
15580
16134
|
//#endregion
|
|
15581
16135
|
//#region src/plugins/siteResolution.ts
|
|
@@ -15654,7 +16208,7 @@ const siteResolutionPlugin = fp(async (app, { apiPrefix }) => {
|
|
|
15654
16208
|
const site = await resolveSite({
|
|
15655
16209
|
credentialSiteId: await credentialSiteOf(request),
|
|
15656
16210
|
requestedKey
|
|
15657
|
-
});
|
|
16211
|
+
}, (lookup) => readRequestSite(request, lookup));
|
|
15658
16212
|
request.site = site;
|
|
15659
16213
|
request.principal = principalForSite(request.principal, site.id);
|
|
15660
16214
|
if (request.principal.kind === "admin" && request.principal.roleIds.length === 0 && request.routeOptions.config?.siteAccess !== "unassigned") throw new AppError(403, "SITE_FORBIDDEN", "You have no role on this site");
|
|
@@ -15821,7 +16375,8 @@ const revokeApiToken = async (context, permissions, id) => {
|
|
|
15821
16375
|
const resolveApiToken = async (value, now = /* @__PURE__ */ new Date()) => {
|
|
15822
16376
|
const row = await findLiveByHash$1(hashToken(value));
|
|
15823
16377
|
if (!row || row.expires_at !== null && row.expires_at.getTime() <= now.getTime()) return;
|
|
15824
|
-
|
|
16378
|
+
const staleBefore = /* @__PURE__ */ new Date(now.getTime() - LAST_SEEN_WRITE_INTERVAL_MS);
|
|
16379
|
+
if (row.last_used_at === null || row.last_used_at.getTime() < staleBefore.getTime()) await touchLastUsed$1(row.id, now, staleBefore);
|
|
15825
16380
|
return {
|
|
15826
16381
|
kind: "token",
|
|
15827
16382
|
tokenId: row.id,
|
|
@@ -15906,7 +16461,7 @@ const adminSessionPlugin = fp(async (app, { urls, secureCookies }) => {
|
|
|
15906
16461
|
app.decorate("requireGlobalPermission", (action) => {
|
|
15907
16462
|
return async function requireGlobalPermission(request, reply) {
|
|
15908
16463
|
await requireAdmin.call(this, request, reply);
|
|
15909
|
-
if (!await
|
|
16464
|
+
if (!await getRequestPermissions(request).canPerform(request.principal, action)) throw new AppError(403, "FORBIDDEN", `Your role does not allow ${action}`);
|
|
15910
16465
|
};
|
|
15911
16466
|
});
|
|
15912
16467
|
}, {
|
|
@@ -17059,10 +17614,12 @@ const toErrorResponse = (error) => {
|
|
|
17059
17614
|
};
|
|
17060
17615
|
/** The single error handler: consistent `{ error: { code, message, details? } }` bodies and logging. */
|
|
17061
17616
|
const errorHandlerPlugin = fp(async (app) => {
|
|
17617
|
+
app.decorateRequest("errorCode", void 0);
|
|
17062
17618
|
app.setErrorHandler((error, request, reply) => {
|
|
17063
17619
|
const { statusCode, body } = toErrorResponse(error);
|
|
17620
|
+
request.errorCode = body.error.code;
|
|
17064
17621
|
if (statusCode >= 500) request.log.error({ err: error }, "request failed");
|
|
17065
|
-
else request.log.
|
|
17622
|
+
else request.log.debug({
|
|
17066
17623
|
statusCode,
|
|
17067
17624
|
code: body.error.code
|
|
17068
17625
|
}, "request rejected");
|
|
@@ -17073,6 +17630,7 @@ const errorHandlerPlugin = fp(async (app) => {
|
|
|
17073
17630
|
code: "NOT_FOUND",
|
|
17074
17631
|
message: `Route ${request.method} ${request.url} not found`
|
|
17075
17632
|
} };
|
|
17633
|
+
request.errorCode = body.error.code;
|
|
17076
17634
|
return reply.code(404).send(body);
|
|
17077
17635
|
});
|
|
17078
17636
|
}, { name: "shapio-error-handler" });
|
|
@@ -17157,6 +17715,14 @@ const gracefulClosePlugin = fp(async (app) => {
|
|
|
17157
17715
|
});
|
|
17158
17716
|
});
|
|
17159
17717
|
//#endregion
|
|
17718
|
+
//#region src/constants/graphql.ts
|
|
17719
|
+
/** A site's GraphQL schema is dropped from memory when no request has used it for this long. */
|
|
17720
|
+
const GRAPHQL_SITE_SCHEMA_IDLE_MS = 6e5;
|
|
17721
|
+
/** `/api/graphql/playground?query=`: the longest operation the page opens with (it travels in the URL). */
|
|
17722
|
+
const PLAYGROUND_QUERY_MAX_LENGTH = 8192;
|
|
17723
|
+
/** `/api/graphql/playground?theme=`: GraphiQL's two themes. */
|
|
17724
|
+
const PLAYGROUND_THEMES = ["light", "dark"];
|
|
17725
|
+
//#endregion
|
|
17160
17726
|
//#region src/helpers/vary.ts
|
|
17161
17727
|
/**
|
|
17162
17728
|
* Adds header names to the reply's `Vary` without dropping the ones already there (`Origin`, set by
|
|
@@ -17803,12 +18369,15 @@ const startSchemaChangeListener = ({ connectionString, registry, log }) => creat
|
|
|
17803
18369
|
//#region src/plugins/schemaSnapshot.ts
|
|
17804
18370
|
/**
|
|
17805
18371
|
* The schema registry and per-request snapshot pinning (ADR 0002). The pin is lazy: requests that never
|
|
17806
|
-
* touch the schema (health, static admin) pay nothing; the first call in a request
|
|
18372
|
+
* touch the schema (health, static admin) pay nothing; the first call in a request checks the cache against
|
|
18373
|
+
* the request's schema version (`getRequestState`, read once per request).
|
|
17807
18374
|
*/
|
|
17808
18375
|
const schemaSnapshotPlugin = fp(async (app, { registry, listen, contentPorts }) => {
|
|
17809
18376
|
app.decorate("schemaRegistry", registry);
|
|
17810
18377
|
app.decorate("schemaContent", contentPorts);
|
|
17811
18378
|
app.decorateRequest("schemaSnapshotPin", null);
|
|
18379
|
+
app.decorateRequest("requestState", null);
|
|
18380
|
+
app.decorateRequest("requestPermissions", null);
|
|
17812
18381
|
let listener;
|
|
17813
18382
|
if (listen) app.addHook("onReady", async () => {
|
|
17814
18383
|
listener = startSchemaChangeListener({
|
|
@@ -17827,7 +18396,7 @@ const schemaSnapshotPlugin = fp(async (app, { registry, listen, contentPorts })
|
|
|
17827
18396
|
});
|
|
17828
18397
|
/** The full schema (every site's definitions and the shared ones) this request is pinned to. */
|
|
17829
18398
|
const getRequestNetworkSchema = (request) => {
|
|
17830
|
-
request.schemaSnapshotPin ??= request.server.schemaRegistry.getSnapshot();
|
|
18399
|
+
request.schemaSnapshotPin ??= getRequestState(request).then(({ versions }) => request.server.schemaRegistry.getSnapshot(void 0, versions));
|
|
17831
18400
|
return request.schemaSnapshotPin;
|
|
17832
18401
|
};
|
|
17833
18402
|
/**
|
|
@@ -17996,42 +18565,137 @@ const executeOperation = async (schema, prepared, input, context) => execute({
|
|
|
17996
18565
|
contextValue: context
|
|
17997
18566
|
});
|
|
17998
18567
|
//#endregion
|
|
17999
|
-
//#region src/
|
|
18000
|
-
|
|
18001
|
-
|
|
18002
|
-
|
|
18003
|
-
|
|
18004
|
-
|
|
18005
|
-
|
|
18006
|
-
|
|
18007
|
-
|
|
18008
|
-
|
|
18009
|
-
|
|
18010
|
-
|
|
18011
|
-
|
|
18012
|
-
|
|
18013
|
-
|
|
18014
|
-
|
|
18015
|
-
|
|
18016
|
-
|
|
18568
|
+
//#region src/services/siteSeo.ts
|
|
18569
|
+
/**
|
|
18570
|
+
* A site's SEO defaults (plan seo-fields): default texts per locale, a default social image and the Twitter
|
|
18571
|
+
* handle, stored on `sites.seo_defaults`. Edited by `site.settings`; delivered on `GET /api/site` and merged
|
|
18572
|
+
* into SEO fields by `?seo=resolved` reads.
|
|
18573
|
+
*/
|
|
18574
|
+
const SITE_UPDATED_EVENT = "site.updated";
|
|
18575
|
+
const siteNotFound = () => new AppError(404, "SITE_NOT_FOUND", "Site not found");
|
|
18576
|
+
/** Stored defaults, or the empty ones when none were saved (or the stored value no longer fits the schema). */
|
|
18577
|
+
const readDefaults = (stored) => stored !== null && Value.Check(SeoDefaultsSchema, stored) ? stored : EMPTY_SEO_DEFAULTS;
|
|
18578
|
+
/** The defaults a resolved delivery read merges in. */
|
|
18579
|
+
const loadSeoDefaults = async (siteId, executor) => readDefaults((await findSeoDefaults(siteId, executor))?.seo_defaults ?? null);
|
|
18580
|
+
/** The caller reads something on this site (the same rule as its content: deny by default). */
|
|
18581
|
+
const assertReadsSite = async (context) => {
|
|
18582
|
+
for (const { definition } of context.snapshot.definitions) {
|
|
18583
|
+
if (!isModelDefinition(definition)) continue;
|
|
18584
|
+
if ((await context.permissions.evaluate(context.actor, {
|
|
18585
|
+
action: "read",
|
|
18586
|
+
modelId: definition.id
|
|
18587
|
+
})).allowed) return;
|
|
18017
18588
|
}
|
|
18018
|
-
|
|
18589
|
+
throw context.actor.kind === "anonymous" ? new AppError(401, "UNAUTHENTICATED", "This site needs a delivery token") : new AppError(403, "FORBIDDEN", "You may not read content on this site");
|
|
18019
18590
|
};
|
|
18020
|
-
|
|
18021
|
-
|
|
18022
|
-
|
|
18023
|
-
|
|
18024
|
-
|
|
18025
|
-
|
|
18026
|
-
|
|
18591
|
+
/**
|
|
18592
|
+
* Reading the defaults needs read access to some content of the site (editors see the template in the entry's
|
|
18593
|
+
* search-result preview); changing them needs `site.settings` (the route checks it).
|
|
18594
|
+
*/
|
|
18595
|
+
const getSiteSeo$1 = async (context) => {
|
|
18596
|
+
await assertReadsSite(context);
|
|
18597
|
+
const row = await findSeoDefaults(context.site.id, context.db);
|
|
18598
|
+
if (!row) throw siteNotFound();
|
|
18599
|
+
return {
|
|
18600
|
+
version: row.version,
|
|
18601
|
+
seo: readDefaults(row.seo_defaults)
|
|
18602
|
+
};
|
|
18603
|
+
};
|
|
18604
|
+
const invalid$1 = (code, message, details) => new AppError(400, code, message, details);
|
|
18605
|
+
const assertKnownLocales = (snapshot, seo) => {
|
|
18606
|
+
const known = new Set(snapshot.locales.map((locale) => locale.code));
|
|
18607
|
+
const unknown = Object.keys(seo.locales).filter((code) => !known.has(code));
|
|
18608
|
+
if (unknown.length > 0) throw invalid$1("UNKNOWN_LOCALE", `Unknown locale "${unknown[0]}"`, { locales: unknown });
|
|
18609
|
+
};
|
|
18610
|
+
/** The default image must be a live, public image of this site: delivery shows it to anyone. */
|
|
18611
|
+
const assertUsableImage = async (context, imageId) => {
|
|
18612
|
+
if (imageId === null) return;
|
|
18613
|
+
const [asset] = await findLiveManyOnSite(context.site.id, [imageId], context.db);
|
|
18614
|
+
if (!asset) throw invalid$1("SEO_IMAGE_INVALID", "The default image is not an asset of this site", { imageId });
|
|
18615
|
+
if (!isPublicImage({
|
|
18616
|
+
visibility: asset.visibility,
|
|
18617
|
+
mimeType: asset.mime_type
|
|
18618
|
+
})) throw invalid$1("SEO_IMAGE_INVALID", "The default image must be a public image", { imageId });
|
|
18619
|
+
};
|
|
18620
|
+
/** Without empty locale entries, so a cleared locale leaves nothing behind. */
|
|
18621
|
+
const compact = (seo) => ({
|
|
18622
|
+
...seo,
|
|
18623
|
+
locales: Object.fromEntries(Object.entries(seo.locales).filter(([, texts]) => Object.keys(texts).length > 0))
|
|
18624
|
+
});
|
|
18625
|
+
/** Optimistic: fails with VERSION_CONFLICT when the site changed since `expectedVersion` was read. */
|
|
18626
|
+
const updateSiteSeo$1 = async (context, input) => {
|
|
18627
|
+
const seo = compact(input.seo);
|
|
18628
|
+
assertKnownLocales(context.snapshot, seo);
|
|
18629
|
+
await assertUsableImage(context, seo.imageId);
|
|
18630
|
+
return context.db.transaction().execute(async (trx) => {
|
|
18631
|
+
const current = await findSeoDefaults(context.site.id, trx);
|
|
18632
|
+
if (!current) throw siteNotFound();
|
|
18633
|
+
const updated = await updateSeoDefaultsIfVersion(context.site.id, input.expectedVersion, seo, trx);
|
|
18634
|
+
if (!updated) throw new AppError(409, "VERSION_CONFLICT", "The site changed since you loaded it", {
|
|
18635
|
+
expectedVersion: input.expectedVersion,
|
|
18636
|
+
actualVersion: current.version
|
|
18637
|
+
});
|
|
18638
|
+
const metadata = {
|
|
18639
|
+
locales: Object.keys(seo.locales),
|
|
18640
|
+
imageId: seo.imageId,
|
|
18641
|
+
twitterHandle: seo.twitterHandle
|
|
18642
|
+
};
|
|
18643
|
+
await recordAudit(trx, {
|
|
18644
|
+
...context,
|
|
18645
|
+
action: "site.seo_update",
|
|
18646
|
+
target: {
|
|
18647
|
+
type: "site",
|
|
18648
|
+
id: context.site.id
|
|
18649
|
+
},
|
|
18650
|
+
metadata
|
|
18651
|
+
});
|
|
18652
|
+
await writeOutboxEvent(trx, {
|
|
18653
|
+
type: SITE_UPDATED_EVENT,
|
|
18654
|
+
aggregateType: "site",
|
|
18655
|
+
aggregateId: context.site.id,
|
|
18656
|
+
payload: {
|
|
18657
|
+
siteKey: context.site.key,
|
|
18658
|
+
changed: ["seo"]
|
|
18659
|
+
},
|
|
18660
|
+
siteId: context.site.id
|
|
18661
|
+
});
|
|
18662
|
+
return {
|
|
18663
|
+
version: updated.version,
|
|
18664
|
+
seo: readDefaults(updated.seo_defaults)
|
|
18665
|
+
};
|
|
18027
18666
|
});
|
|
18028
|
-
|
|
18667
|
+
};
|
|
18668
|
+
const deliveryImage = async (context, imageId) => {
|
|
18669
|
+
if (imageId === null || !context.media) return null;
|
|
18670
|
+
const assets = await findLiveManyOnSite(context.site.id, [imageId], context.db);
|
|
18671
|
+
const [view] = await toAssetViews(context.media, assets, context.db);
|
|
18672
|
+
return view && isPublicImage(view) ? toDeliveryAsset(view) : null;
|
|
18673
|
+
};
|
|
18674
|
+
/** `GET /api/site`: the site's key, name and SEO defaults, its default image as a delivered asset. */
|
|
18675
|
+
const getDeliverySite$1 = async (context) => {
|
|
18676
|
+
await assertReadsSite(context);
|
|
18677
|
+
const row = await findSeoDefaults(context.site.id, context.db);
|
|
18678
|
+
if (!row) throw siteNotFound();
|
|
18679
|
+
const { imageId, ...seo } = readDefaults(row.seo_defaults);
|
|
18029
18680
|
return {
|
|
18030
|
-
|
|
18031
|
-
|
|
18681
|
+
key: row.key,
|
|
18682
|
+
name: row.name,
|
|
18683
|
+
seo: {
|
|
18684
|
+
...seo,
|
|
18685
|
+
image: await deliveryImage(context, imageId)
|
|
18686
|
+
}
|
|
18032
18687
|
};
|
|
18033
18688
|
};
|
|
18034
|
-
|
|
18689
|
+
//#endregion
|
|
18690
|
+
//#region src/services/contentDeliveryReads.ts
|
|
18691
|
+
/**
|
|
18692
|
+
* Delivery-shaped reads (delivery, preview, GraphQL root fields) at one consistent moment (plan
|
|
18693
|
+
* delivery-perf). The heads, the page total and the site's publication sequence come from one statement, so
|
|
18694
|
+
* a read that needs nothing else (no relation, populate or asset follow-up) runs without a transaction.
|
|
18695
|
+
* Anything that needs a follow-up statement runs in one REPEATABLE READ, read-only transaction, so every
|
|
18696
|
+
* statement of the response sees the moment `meta.snapshot` names.
|
|
18697
|
+
*/
|
|
18698
|
+
const deliveryEnvironment = (context, executor, source, locale, richText, seo) => ({
|
|
18035
18699
|
executor,
|
|
18036
18700
|
snapshot: context.snapshot,
|
|
18037
18701
|
permissions: context.permissions,
|
|
@@ -18040,91 +18704,101 @@ const deliveryEnvironment = (context, executor, source, locale) => ({
|
|
|
18040
18704
|
audience: "delivery",
|
|
18041
18705
|
source,
|
|
18042
18706
|
locale,
|
|
18043
|
-
...context.media ? { media: context.media } : {}
|
|
18707
|
+
...context.media ? { media: context.media } : {},
|
|
18708
|
+
...richText ? { richText } : {},
|
|
18709
|
+
...seo ? { seo } : {}
|
|
18044
18710
|
});
|
|
18045
|
-
|
|
18046
|
-
const atSnapshot = async (context, query, options, read) => withConsistentRead(async (trx) => {
|
|
18047
|
-
const current = await currentSeq(context.site.id, trx);
|
|
18048
|
-
if (query.snapshot !== void 0 && options.drafts) throw queryInvalid("snapshot reads published content; it cannot be combined with drafts");
|
|
18049
|
-
if (query.snapshot !== void 0 && query.snapshot > current) throw snapshotInvalid(query.snapshot, current);
|
|
18050
|
-
const source = options.drafts ? {
|
|
18051
|
-
kind: "heads",
|
|
18052
|
-
state: "draft"
|
|
18053
|
-
} : query.snapshot !== void 0 ? {
|
|
18054
|
-
kind: "snapshot",
|
|
18055
|
-
seq: query.snapshot
|
|
18056
|
-
} : {
|
|
18057
|
-
kind: "heads",
|
|
18058
|
-
state: "published"
|
|
18059
|
-
};
|
|
18060
|
-
return read(deliveryEnvironment(context, trx, source, query.locale), query.snapshot ?? current);
|
|
18061
|
-
}, context.db);
|
|
18711
|
+
const environmentOf = (read, executor) => deliveryEnvironment(read.context, executor, read.source, read.query.locale, read.query.richText, read.seo);
|
|
18062
18712
|
/**
|
|
18063
|
-
*
|
|
18064
|
-
*
|
|
18713
|
+
* Selected top-level relation or media fields, populate, or a default SEO image to show: the read will need a
|
|
18714
|
+
* follow-up statement.
|
|
18065
18715
|
*/
|
|
18066
|
-
const
|
|
18067
|
-
|
|
18068
|
-
|
|
18069
|
-
|
|
18070
|
-
|
|
18071
|
-
|
|
18072
|
-
|
|
18073
|
-
|
|
18074
|
-
|
|
18075
|
-
|
|
18076
|
-
|
|
18077
|
-
|
|
18078
|
-
|
|
18079
|
-
|
|
18080
|
-
|
|
18716
|
+
const followsUp = (read) => read.query.populate.size > 0 || selectFields(read.model, read.policy.readMask, read.query.fields).some((field) => field.type === "relation" || field.type === "media" || Boolean(read.seo?.imageId) && isSeoField(field));
|
|
18717
|
+
/**
|
|
18718
|
+
* Loads the site's SEO defaults when the query asks for resolved SEO. Read before the heads, outside their
|
|
18719
|
+
* consistent moment: defaults are not part of a snapshot (`?snapshot=N&seo=resolved` uses today's).
|
|
18720
|
+
*/
|
|
18721
|
+
const withSeoDefaults = async (read) => read.query.seo === "resolved" ? {
|
|
18722
|
+
...read,
|
|
18723
|
+
seo: await loadSeoDefaults(read.context.site.id, read.context.db)
|
|
18724
|
+
} : read;
|
|
18725
|
+
const seqOf = (seq) => {
|
|
18726
|
+
if (seq === null) throw new Error("The site has no publication state");
|
|
18727
|
+
return seq;
|
|
18728
|
+
};
|
|
18729
|
+
/**
|
|
18730
|
+
* Runs the plan's statement on the pool when the read cannot need a follow-up, and keeps that result when
|
|
18731
|
+
* the rows indeed need none; otherwise reads again inside one REPEATABLE READ transaction. `finish` projects
|
|
18732
|
+
* the rows (through the same executor), so its own statements see the same moment.
|
|
18733
|
+
*/
|
|
18734
|
+
const readConsistently = async (read, plan, options, finish) => {
|
|
18735
|
+
const compiled = compileHeadPage(plan, options);
|
|
18736
|
+
const { db } = read.context;
|
|
18737
|
+
if (!followsUp(read)) {
|
|
18738
|
+
const env = environmentOf(read, db);
|
|
18739
|
+
const { rows, meta } = await runHeadPage(compiled.rows, db);
|
|
18740
|
+
if (meta && !needsFollowUpReads(env, read.model, rows, read.query, read.policy)) return finish({
|
|
18741
|
+
env,
|
|
18742
|
+
rows,
|
|
18743
|
+
total: meta.total,
|
|
18744
|
+
seq: seqOf(meta.seq)
|
|
18745
|
+
});
|
|
18746
|
+
}
|
|
18747
|
+
return withConsistentRead(async (trx) => {
|
|
18748
|
+
const env = environmentOf(read, trx);
|
|
18749
|
+
const page = await runHeadPage(compiled.rows, trx);
|
|
18750
|
+
const meta = page.meta ?? await runHeadPageMeta(compiled.meta, trx);
|
|
18751
|
+
return finish({
|
|
18752
|
+
env,
|
|
18753
|
+
rows: page.rows,
|
|
18754
|
+
total: meta.total,
|
|
18755
|
+
seq: seqOf(meta.seq)
|
|
18756
|
+
});
|
|
18757
|
+
}, db);
|
|
18081
18758
|
};
|
|
18082
|
-
const
|
|
18759
|
+
const localesOf = (read) => readScopeFor(read.context.snapshot, read.model.definition, read.query.locale, { fallback: read.fallback });
|
|
18760
|
+
const toEntries = async (read, env, rows) => (await projectRows(env, read.model, read.policy, rows, read.query)).map(({ row, data }) => ({
|
|
18083
18761
|
...systemAttributes(env, row),
|
|
18084
18762
|
...data
|
|
18085
18763
|
}));
|
|
18086
|
-
|
|
18087
|
-
|
|
18088
|
-
|
|
18089
|
-
|
|
18090
|
-
|
|
18091
|
-
|
|
18092
|
-
|
|
18093
|
-
const assertItemParameters = (rawQuery, allowed) => {
|
|
18094
|
-
const unsupported = [...new URLSearchParams(rawQuery).keys()].find((key) => !allowed.has(key.split("[")[0] ?? key));
|
|
18095
|
-
if (unsupported !== void 0) throw queryInvalid(`"${unsupported}" is not supported when reading one entry`);
|
|
18096
|
-
};
|
|
18097
|
-
const localesOf = (read) => readScopeFor(read.context.snapshot, read.model.definition, read.query.locale, { fallback: read.fallback });
|
|
18098
|
-
/** One page of entries in the delivery shape, with the total and the page size used. */
|
|
18099
|
-
const readEntryPage = async (read) => {
|
|
18100
|
-
const { context, env, model, policy, query } = read;
|
|
18764
|
+
/**
|
|
18765
|
+
* One page of entries in the delivery shape, with the total (`total: false` skips counting: a singleton) and
|
|
18766
|
+
* the publication sequence it was read at. `check` runs on the sequence before anything is projected.
|
|
18767
|
+
*/
|
|
18768
|
+
const readEntryPage = async (unloaded, options = { total: true }) => {
|
|
18769
|
+
const read = await withSeoDefaults(unloaded);
|
|
18770
|
+
const { context, model, policy, query } = read;
|
|
18101
18771
|
const { limit, offset } = toLimitOffset(query.page, query.pageSize);
|
|
18102
18772
|
const sort = query.sort.length > 0 ? query.sort : defaultSortTerms(model.definition, (field) => maskAllows(policy.readMask, field));
|
|
18103
|
-
const
|
|
18773
|
+
const plan = {
|
|
18104
18774
|
siteId: context.site.id,
|
|
18105
18775
|
modelId: model.definition.id,
|
|
18106
|
-
source:
|
|
18776
|
+
source: read.source,
|
|
18107
18777
|
locales: localesOf(read),
|
|
18108
18778
|
conditions: [...queryConditions(context, query, policy), ...read.conditions ?? []],
|
|
18109
18779
|
orderBy: compileOrderBy(sort),
|
|
18110
18780
|
orderedByEntry: leadsWithEntryColumn(sort),
|
|
18111
18781
|
limit,
|
|
18112
18782
|
offset
|
|
18113
|
-
});
|
|
18114
|
-
const rows = await runHeadQuery(compiled.rows, env.executor);
|
|
18115
|
-
return {
|
|
18116
|
-
entries: toEntries(env, await projectRows(env, model, policy, rows, query)),
|
|
18117
|
-
limit,
|
|
18118
|
-
count: () => runCountQuery(compiled.count, env.executor)
|
|
18119
18783
|
};
|
|
18784
|
+
return readConsistently(read, plan, { total: options.total }, async ({ env, rows, total, seq }) => {
|
|
18785
|
+
options.check?.(seq);
|
|
18786
|
+
return {
|
|
18787
|
+
entries: await toEntries(read, env, rows),
|
|
18788
|
+
limit,
|
|
18789
|
+
total: total ?? 0,
|
|
18790
|
+
seq
|
|
18791
|
+
};
|
|
18792
|
+
});
|
|
18120
18793
|
};
|
|
18121
|
-
/** One entry by ID in the delivery shape
|
|
18122
|
-
const readOneEntry = async (
|
|
18123
|
-
const
|
|
18124
|
-
const
|
|
18794
|
+
/** One entry by ID in the delivery shape (undefined when it is not there for this read), and the sequence. */
|
|
18795
|
+
const readOneEntry = async (unloaded, id, options = {}) => {
|
|
18796
|
+
const read = await withSeoDefaults(unloaded);
|
|
18797
|
+
const { context, model, policy, query } = read;
|
|
18798
|
+
const plan = {
|
|
18125
18799
|
siteId: context.site.id,
|
|
18126
18800
|
modelId: model.definition.id,
|
|
18127
|
-
source:
|
|
18801
|
+
source: read.source,
|
|
18128
18802
|
locales: localesOf(read),
|
|
18129
18803
|
conditions: [
|
|
18130
18804
|
...queryConditions(context, {
|
|
@@ -18137,77 +18811,19 @@ const readOneEntry = async (read, id) => {
|
|
|
18137
18811
|
],
|
|
18138
18812
|
orderBy: [],
|
|
18139
18813
|
limit: 1
|
|
18140
|
-
}
|
|
18141
|
-
|
|
18142
|
-
|
|
18143
|
-
|
|
18144
|
-
};
|
|
18145
|
-
/** `GET /api/content/:modelKey`: a page of a collection, or a singleton's one entry. */
|
|
18146
|
-
const listDelivery$1 = async (context, modelKey, rawQuery, options = {}) => {
|
|
18147
|
-
const { model, policy } = await readableModel(context, modelKey);
|
|
18148
|
-
const query = parseQueryFor(context, model, policy, rawQuery, { allowSnapshot: true });
|
|
18149
|
-
const locale = query.locale ?? context.snapshot.defaultLocale;
|
|
18150
|
-
const result = await atSnapshot(context, query, options, async (env, seq) => {
|
|
18151
|
-
const { entries, limit, count } = await readEntryPage({
|
|
18152
|
-
context,
|
|
18153
|
-
env,
|
|
18154
|
-
model,
|
|
18155
|
-
policy,
|
|
18156
|
-
query,
|
|
18157
|
-
fallback: options.fallback ?? true
|
|
18158
|
-
});
|
|
18159
|
-
if (model.definition.kind === "singleton") {
|
|
18160
|
-
const [entry] = entries;
|
|
18161
|
-
if (!entry) throw entryNotFound(modelKey);
|
|
18162
|
-
return {
|
|
18163
|
-
data: entry,
|
|
18164
|
-
meta: {
|
|
18165
|
-
locale,
|
|
18166
|
-
snapshot: seq
|
|
18167
|
-
}
|
|
18168
|
-
};
|
|
18169
|
-
}
|
|
18170
|
-
return {
|
|
18171
|
-
data: entries,
|
|
18172
|
-
meta: {
|
|
18173
|
-
locale,
|
|
18174
|
-
snapshot: seq,
|
|
18175
|
-
pagination: paginationMeta(query.page, limit, await count())
|
|
18176
|
-
}
|
|
18177
|
-
};
|
|
18178
|
-
});
|
|
18179
|
-
await recordUsage(context, options, model, policy, query);
|
|
18180
|
-
return result;
|
|
18181
|
-
};
|
|
18182
|
-
/** `GET /api/content/:modelKey/:id`: one published entry (404 for drafts, unknown or unreadable entries). */
|
|
18183
|
-
const getDelivery$1 = async (context, modelKey, id, rawQuery, options = {}) => {
|
|
18184
|
-
assertItemParameters(rawQuery, ITEM_PARAMETERS$1);
|
|
18185
|
-
const { model, policy } = await readableModel(context, modelKey);
|
|
18186
|
-
const query = parseQueryFor(context, model, policy, rawQuery, { allowSnapshot: true });
|
|
18187
|
-
const result = await atSnapshot(context, query, options, async (env, seq) => {
|
|
18188
|
-
const entry = await readOneEntry({
|
|
18189
|
-
context,
|
|
18190
|
-
env,
|
|
18191
|
-
model,
|
|
18192
|
-
policy,
|
|
18193
|
-
query,
|
|
18194
|
-
fallback: options.fallback ?? true
|
|
18195
|
-
}, id);
|
|
18196
|
-
if (!entry) throw entryNotFound(id);
|
|
18814
|
+
};
|
|
18815
|
+
return readConsistently(read, plan, { total: false }, async ({ env, rows, seq }) => {
|
|
18816
|
+
options.check?.(seq);
|
|
18817
|
+
const [entry] = await toEntries(read, env, rows);
|
|
18197
18818
|
return {
|
|
18198
|
-
|
|
18199
|
-
|
|
18200
|
-
locale: query.locale ?? context.snapshot.defaultLocale,
|
|
18201
|
-
snapshot: seq
|
|
18202
|
-
}
|
|
18819
|
+
entry,
|
|
18820
|
+
seq
|
|
18203
18821
|
};
|
|
18204
18822
|
});
|
|
18205
|
-
await recordUsage(context, options, model, policy, query);
|
|
18206
|
-
return result;
|
|
18207
18823
|
};
|
|
18208
18824
|
//#endregion
|
|
18209
18825
|
//#region src/services/contentBatchReads.ts
|
|
18210
|
-
const sourceOf = (options) => {
|
|
18826
|
+
const sourceOf$1 = (options) => {
|
|
18211
18827
|
if (options.drafts) return {
|
|
18212
18828
|
kind: "heads",
|
|
18213
18829
|
state: "draft"
|
|
@@ -18225,7 +18841,7 @@ const loadDeliveryEntries = async (context, modelId, ids, options) => {
|
|
|
18225
18841
|
const found = /* @__PURE__ */ new Map();
|
|
18226
18842
|
const model = resolveModelById(context.snapshot, modelId);
|
|
18227
18843
|
if (!model || ids.length === 0) return found;
|
|
18228
|
-
const env = deliveryEnvironment(context, context.db, sourceOf(options), options.locale);
|
|
18844
|
+
const env = deliveryEnvironment(context, context.db, sourceOf$1(options), options.locale);
|
|
18229
18845
|
const policy = await readPolicy(env, modelId);
|
|
18230
18846
|
const scope = readScopeFor(context.snapshot, model.definition, options.locale, { fallback: options.fallback });
|
|
18231
18847
|
const rows = await fetchHeadsByIds(env, model, policy, ids, scope);
|
|
@@ -18247,7 +18863,7 @@ const loadDeliveryLocalizations = async (context, modelId, entryIds, options) =>
|
|
|
18247
18863
|
const found = new Map(entryIds.map((id) => [id, []]));
|
|
18248
18864
|
const model = resolveModelById(context.snapshot, modelId);
|
|
18249
18865
|
if (!model || !model.definition.localized || entryIds.length === 0) return found;
|
|
18250
|
-
const source = sourceOf({
|
|
18866
|
+
const source = sourceOf$1({
|
|
18251
18867
|
...options,
|
|
18252
18868
|
locale: void 0,
|
|
18253
18869
|
fallback: false
|
|
@@ -18307,48 +18923,17 @@ const createLoaders = (siteId, content) => {
|
|
|
18307
18923
|
};
|
|
18308
18924
|
};
|
|
18309
18925
|
//#endregion
|
|
18310
|
-
//#region src/
|
|
18926
|
+
//#region src/controllers/contentContext.ts
|
|
18311
18927
|
/**
|
|
18312
|
-
* The
|
|
18313
|
-
*
|
|
18314
|
-
*
|
|
18315
|
-
*
|
|
18928
|
+
* The service context for a content request: the pinned snapshot, the principal and dependencies.
|
|
18929
|
+
* `permissions` is the request's evaluator: checked against the versions the request read once, and
|
|
18930
|
+
* memoized per (principal, action, model), so the read, relation visibility, populate and usage recording
|
|
18931
|
+
* evaluate each model's policy once.
|
|
18316
18932
|
*/
|
|
18317
|
-
const memoizePermissions = (base, principal) => {
|
|
18318
|
-
const pooled = /* @__PURE__ */ new Map();
|
|
18319
|
-
const byTransaction = /* @__PURE__ */ new WeakMap();
|
|
18320
|
-
const memoFor = (executor) => {
|
|
18321
|
-
if (!executor) return pooled;
|
|
18322
|
-
let memo = byTransaction.get(executor);
|
|
18323
|
-
if (!memo) {
|
|
18324
|
-
memo = /* @__PURE__ */ new Map();
|
|
18325
|
-
byTransaction.set(executor, memo);
|
|
18326
|
-
}
|
|
18327
|
-
return memo;
|
|
18328
|
-
};
|
|
18329
|
-
return {
|
|
18330
|
-
evaluate: (caller, request, executor) => {
|
|
18331
|
-
if (caller !== principal) return base.evaluate(caller, request, executor);
|
|
18332
|
-
const memo = memoFor(executor);
|
|
18333
|
-
const key = `${request.action}:${request.modelId}`;
|
|
18334
|
-
let policy = memo.get(key);
|
|
18335
|
-
if (!policy) {
|
|
18336
|
-
policy = base.evaluate(caller, request, executor);
|
|
18337
|
-
memo.set(key, policy);
|
|
18338
|
-
}
|
|
18339
|
-
return policy;
|
|
18340
|
-
},
|
|
18341
|
-
canPerform: (caller, action, executor) => base.canPerform(caller, action, executor),
|
|
18342
|
-
canPerformOnSite: (caller, action, siteId, executor) => base.canPerformOnSite(caller, action, siteId, executor)
|
|
18343
|
-
};
|
|
18344
|
-
};
|
|
18345
|
-
//#endregion
|
|
18346
|
-
//#region src/controllers/contentContext.ts
|
|
18347
|
-
/** The service context for a content request: the pinned snapshot, the principal and dependencies. */
|
|
18348
18933
|
const contentContextFor = async (request) => ({
|
|
18349
18934
|
db: request.server.db,
|
|
18350
18935
|
snapshot: await getRequestSchema(request),
|
|
18351
|
-
permissions: request
|
|
18936
|
+
permissions: getRequestPermissions(request),
|
|
18352
18937
|
actor: request.principal,
|
|
18353
18938
|
site: getRequestSite(request),
|
|
18354
18939
|
hooks: request.server.contentHooks,
|
|
@@ -18383,7 +18968,7 @@ const isAdminPrincipal = (principal) => principal.kind === "admin" || principal.
|
|
|
18383
18968
|
/** The context every resolver of one `/api/graphql` request receives (built after site resolution). */
|
|
18384
18969
|
const createGraphqlRequestContext = (request) => {
|
|
18385
18970
|
const site = getRequestSite(request);
|
|
18386
|
-
const permissions =
|
|
18971
|
+
const permissions = getRequestPermissions(request);
|
|
18387
18972
|
let base;
|
|
18388
18973
|
const content = async (snapshot) => {
|
|
18389
18974
|
base ??= contentContextFor(request);
|
|
@@ -18469,9 +19054,16 @@ const BOOTSTRAP = `(() => {
|
|
|
18469
19054
|
const root = document.getElementById('graphiql');
|
|
18470
19055
|
const endpoint = root.dataset.endpoint;
|
|
18471
19056
|
const csrfUrl = root.dataset.csrf;
|
|
19057
|
+
const props = { shouldPersistHeaders: false };
|
|
19058
|
+
if (root.dataset.query) {
|
|
19059
|
+
props.query = root.dataset.query;
|
|
19060
|
+
}
|
|
19061
|
+
if (root.dataset.theme) {
|
|
19062
|
+
props.forcedTheme = root.dataset.theme;
|
|
19063
|
+
}
|
|
18472
19064
|
const start = (csrfToken) => {
|
|
18473
19065
|
const fetcher = GraphiQL.createFetcher({ url: endpoint, headers: { 'x-csrf-token': csrfToken } });
|
|
18474
|
-
ReactDOM.createRoot(root).render(React.createElement(GraphiQL, {
|
|
19066
|
+
ReactDOM.createRoot(root).render(React.createElement(GraphiQL, { ...props, fetcher }));
|
|
18475
19067
|
};
|
|
18476
19068
|
fetch(csrfUrl, { credentials: 'same-origin' })
|
|
18477
19069
|
.then((response) => {
|
|
@@ -18504,9 +19096,11 @@ const loadPlaygroundAssets = () => {
|
|
|
18504
19096
|
});
|
|
18505
19097
|
return assets;
|
|
18506
19098
|
};
|
|
19099
|
+
const dataAttribute = (name, value) => value === void 0 || value === "" ? "" : ` data-${name}="${escapeHtml$1(value)}"`;
|
|
18507
19100
|
/** The playground page; `assetsPath` and the URLs are absolute paths (BASE_PATH included). */
|
|
18508
|
-
const renderPlaygroundPage = (urls) => {
|
|
19101
|
+
const renderPlaygroundPage = (urls, options = {}) => {
|
|
18509
19102
|
const asset = (file) => escapeHtml$1(`${urls.assets}/${file}`);
|
|
19103
|
+
const extra = dataAttribute("query", options.query) + dataAttribute("theme", options.theme);
|
|
18510
19104
|
return `<!doctype html>
|
|
18511
19105
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
18512
19106
|
<title>Shapio GraphQL</title>
|
|
@@ -18514,7 +19108,7 @@ const renderPlaygroundPage = (urls) => {
|
|
|
18514
19108
|
<link rel="stylesheet" href="${asset("playground.css")}">
|
|
18515
19109
|
</head>
|
|
18516
19110
|
<body>
|
|
18517
|
-
<div id="graphiql" data-endpoint="${escapeHtml$1(urls.endpoint)}" data-csrf="${escapeHtml$1(urls.csrf)}">Loading…</div>
|
|
19111
|
+
<div id="graphiql" data-endpoint="${escapeHtml$1(urls.endpoint)}" data-csrf="${escapeHtml$1(urls.csrf)}"${extra}>Loading…</div>
|
|
18518
19112
|
<script src="${asset("react.production.min.js")}"><\/script>
|
|
18519
19113
|
<script src="${asset("react-dom.production.min.js")}"><\/script>
|
|
18520
19114
|
<script src="${asset("graphiql.min.js")}"><\/script>
|
|
@@ -18524,15 +19118,21 @@ const renderPlaygroundPage = (urls) => {
|
|
|
18524
19118
|
//#endregion
|
|
18525
19119
|
//#region src/controllers/graphqlPlayground.ts
|
|
18526
19120
|
let assets;
|
|
18527
|
-
/**
|
|
19121
|
+
/**
|
|
19122
|
+
* The page is a network route; `?site=` only chooses the site its GraphQL requests name (`?site=` on the
|
|
19123
|
+
* endpoint). `?query=` opens the editor on that operation, `?theme=light|dark` fixes GraphiQL's theme.
|
|
19124
|
+
*/
|
|
18528
19125
|
const getPlaygroundPage = async (request, reply) => {
|
|
18529
19126
|
const { urls } = request.server;
|
|
18530
19127
|
const endpoint = urls.withBasePath("/api/graphql");
|
|
18531
|
-
const { site } = request.query;
|
|
19128
|
+
const { site, query, theme } = request.query;
|
|
18532
19129
|
return reply.type("text/html; charset=utf-8").send(renderPlaygroundPage({
|
|
18533
19130
|
assets: urls.withBasePath("/api/graphql/playground"),
|
|
18534
19131
|
endpoint: site === void 0 ? endpoint : `${endpoint}?${SITE_QUERY_PARAMETER}=${encodeURIComponent(site)}`,
|
|
18535
19132
|
csrf: urls.withBasePath("/api/admin/auth/csrf")
|
|
19133
|
+
}, {
|
|
19134
|
+
query,
|
|
19135
|
+
theme
|
|
18536
19136
|
}));
|
|
18537
19137
|
};
|
|
18538
19138
|
const getPlaygroundAsset = async (request, reply) => {
|
|
@@ -18590,7 +19190,11 @@ const graphqlPlaygroundRoutes = async (app) => {
|
|
|
18590
19190
|
app.get("/playground", {
|
|
18591
19191
|
...admin,
|
|
18592
19192
|
schema: {
|
|
18593
|
-
querystring: Type.Object({
|
|
19193
|
+
querystring: Type.Object({
|
|
19194
|
+
[SITE_QUERY_PARAMETER]: Type.Optional(Type.String({ pattern: SITE_KEY_PATTERN })),
|
|
19195
|
+
query: Type.Optional(Type.String({ maxLength: PLAYGROUND_QUERY_MAX_LENGTH })),
|
|
19196
|
+
theme: Type.Optional(Type.Enum(PLAYGROUND_THEMES))
|
|
19197
|
+
}),
|
|
18594
19198
|
response: { 200: Type.String() }
|
|
18595
19199
|
}
|
|
18596
19200
|
}, getPlaygroundPage);
|
|
@@ -18606,10 +19210,6 @@ const graphqlPlaygroundRoutes = async (app) => {
|
|
|
18606
19210
|
}, getPlaygroundAsset);
|
|
18607
19211
|
};
|
|
18608
19212
|
//#endregion
|
|
18609
|
-
//#region src/constants/graphql.ts
|
|
18610
|
-
/** A site's GraphQL schema is dropped from memory when no request has used it for this long. */
|
|
18611
|
-
const GRAPHQL_SITE_SCHEMA_IDLE_MS = 6e5;
|
|
18612
|
-
//#endregion
|
|
18613
19213
|
//#region src/schema/codegen/graphql/documentCache.ts
|
|
18614
19214
|
const createDocumentCache = (maxEntries = 500) => {
|
|
18615
19215
|
const bySchema = /* @__PURE__ */ new WeakMap();
|
|
@@ -18661,7 +19261,7 @@ const memoType = (cache, key, create) => {
|
|
|
18661
19261
|
* The types every Shapio GraphQL schema has. All of their names are in `RESERVED_TYPE_NAMES`
|
|
18662
19262
|
* (`@shapio/schema` naming.ts), so no model can collide with them.
|
|
18663
19263
|
*/
|
|
18664
|
-
const nonNull$
|
|
19264
|
+
const nonNull$2 = (type) => new GraphQLNonNull(type);
|
|
18665
19265
|
const createJsonScalar = () => new GraphQLScalarType({
|
|
18666
19266
|
name: "JSON",
|
|
18667
19267
|
description: "Any JSON value (JSON fields and rich-text documents).",
|
|
@@ -18716,7 +19316,7 @@ const filterType = (name, value, operators) => new GraphQLInputObjectType({
|
|
|
18716
19316
|
fields: () => {
|
|
18717
19317
|
const fields = {};
|
|
18718
19318
|
for (const operator of operators) {
|
|
18719
|
-
const type = operator === "$null" || operator === "$notNull" ? GraphQLBoolean : operator === "$in" || operator === "$nin" ? new GraphQLList(nonNull$
|
|
19319
|
+
const type = operator === "$null" || operator === "$notNull" ? GraphQLBoolean : operator === "$in" || operator === "$nin" ? new GraphQLList(nonNull$2(value)) : value;
|
|
18720
19320
|
fields[operatorFieldName(operator)] = {
|
|
18721
19321
|
type,
|
|
18722
19322
|
description: OPERATOR_DESCRIPTIONS[operator]
|
|
@@ -18740,15 +19340,15 @@ const createFilterTypes = (int53) => ({
|
|
|
18740
19340
|
const createPageInfo = () => new GraphQLObjectType({
|
|
18741
19341
|
name: "PageInfo",
|
|
18742
19342
|
fields: {
|
|
18743
|
-
page: { type: nonNull$
|
|
18744
|
-
pageSize: { type: nonNull$
|
|
18745
|
-
pageCount: { type: nonNull$
|
|
19343
|
+
page: { type: nonNull$2(GraphQLInt) },
|
|
19344
|
+
pageSize: { type: nonNull$2(GraphQLInt) },
|
|
19345
|
+
pageCount: { type: nonNull$2(GraphQLInt) },
|
|
18746
19346
|
hasNextPage: {
|
|
18747
|
-
type: nonNull$
|
|
19347
|
+
type: nonNull$2(GraphQLBoolean),
|
|
18748
19348
|
resolve: (info) => info.page < info.pageCount
|
|
18749
19349
|
},
|
|
18750
19350
|
hasPreviousPage: {
|
|
18751
|
-
type: nonNull$
|
|
19351
|
+
type: nonNull$2(GraphQLBoolean),
|
|
18752
19352
|
resolve: (info) => info.page > 1
|
|
18753
19353
|
}
|
|
18754
19354
|
}
|
|
@@ -18758,13 +19358,13 @@ const createRichText = (json) => new GraphQLObjectType({
|
|
|
18758
19358
|
description: "A rich-text document: the versioned JSON document and its sanitized HTML rendering.",
|
|
18759
19359
|
fields: {
|
|
18760
19360
|
json: {
|
|
18761
|
-
type: nonNull$
|
|
19361
|
+
type: nonNull$2(json),
|
|
18762
19362
|
description: "`{ format, version, doc }` (ProseMirror JSON)",
|
|
18763
|
-
resolve: ({ html: _html, ...document }) => document
|
|
19363
|
+
resolve: ({ html: _html, [RICH_TEXT_HTML]: _render, ...document }) => document
|
|
18764
19364
|
},
|
|
18765
19365
|
html: {
|
|
18766
|
-
type: nonNull$
|
|
18767
|
-
resolve: (value) => value.html
|
|
19366
|
+
type: nonNull$2(GraphQLString),
|
|
19367
|
+
resolve: (value) => value[RICH_TEXT_HTML]?.() ?? (typeof value.html === "string" ? value.html : "")
|
|
18768
19368
|
}
|
|
18769
19369
|
}
|
|
18770
19370
|
});
|
|
@@ -18772,36 +19372,36 @@ const createFocalPoint = () => new GraphQLObjectType({
|
|
|
18772
19372
|
name: "FocalPoint",
|
|
18773
19373
|
description: "Where the subject of an image is, as fractions of its width and height (0 to 1).",
|
|
18774
19374
|
fields: {
|
|
18775
|
-
x: { type: nonNull$
|
|
18776
|
-
y: { type: nonNull$
|
|
19375
|
+
x: { type: nonNull$2(GraphQLFloat) },
|
|
19376
|
+
y: { type: nonNull$2(GraphQLFloat) }
|
|
18777
19377
|
}
|
|
18778
19378
|
});
|
|
18779
19379
|
const createMediaVariant = () => new GraphQLObjectType({
|
|
18780
19380
|
name: "MediaVariant",
|
|
18781
19381
|
description: "A ready image variant (thumbnail, responsive width, converted format).",
|
|
18782
19382
|
fields: {
|
|
18783
|
-
name: { type: nonNull$
|
|
19383
|
+
name: { type: nonNull$2(GraphQLString) },
|
|
18784
19384
|
width: { type: GraphQLInt },
|
|
18785
19385
|
height: { type: GraphQLInt },
|
|
18786
19386
|
format: { type: GraphQLString },
|
|
18787
|
-
mimeType: { type: nonNull$
|
|
18788
|
-
url: { type: nonNull$
|
|
19387
|
+
mimeType: { type: nonNull$2(GraphQLString) },
|
|
19388
|
+
url: { type: nonNull$2(GraphQLString) }
|
|
18789
19389
|
}
|
|
18790
19390
|
});
|
|
18791
19391
|
const createMedia = (focalPoint, variant) => new GraphQLObjectType({
|
|
18792
19392
|
name: "Media",
|
|
18793
19393
|
description: "A media asset. Private assets carry signed URLs that expire at `urlExpiresAt`.",
|
|
18794
19394
|
fields: {
|
|
18795
|
-
id: { type: nonNull$
|
|
18796
|
-
filename: { type: nonNull$
|
|
18797
|
-
mimeType: { type: nonNull$
|
|
18798
|
-
sizeBytes: { type: nonNull$
|
|
19395
|
+
id: { type: nonNull$2(GraphQLID) },
|
|
19396
|
+
filename: { type: nonNull$2(GraphQLString) },
|
|
19397
|
+
mimeType: { type: nonNull$2(GraphQLString) },
|
|
19398
|
+
sizeBytes: { type: nonNull$2(GraphQLFloat) },
|
|
18799
19399
|
width: { type: GraphQLInt },
|
|
18800
19400
|
height: { type: GraphQLInt },
|
|
18801
|
-
alt: { type: nonNull$
|
|
18802
|
-
caption: { type: nonNull$
|
|
19401
|
+
alt: { type: nonNull$2(GraphQLString) },
|
|
19402
|
+
caption: { type: nonNull$2(GraphQLString) },
|
|
18803
19403
|
focalPoint: { type: focalPoint },
|
|
18804
|
-
url: { type: nonNull$
|
|
19404
|
+
url: { type: nonNull$2(GraphQLString) },
|
|
18805
19405
|
urlExpiresAt: { type: GraphQLString },
|
|
18806
19406
|
variants: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(variant))) }
|
|
18807
19407
|
}
|
|
@@ -18810,15 +19410,15 @@ const createMedia = (focalPoint, variant) => new GraphQLObjectType({
|
|
|
18810
19410
|
const createEntry$2 = () => new GraphQLObjectType({
|
|
18811
19411
|
name: "Entry",
|
|
18812
19412
|
fields: {
|
|
18813
|
-
id: { type: nonNull$
|
|
18814
|
-
locale: { type: nonNull$
|
|
18815
|
-
version: { type: nonNull$
|
|
19413
|
+
id: { type: nonNull$2(GraphQLID) },
|
|
19414
|
+
locale: { type: nonNull$2(GraphQLString) },
|
|
19415
|
+
version: { type: nonNull$2(GraphQLInt) },
|
|
18816
19416
|
status: {
|
|
18817
|
-
type: nonNull$
|
|
19417
|
+
type: nonNull$2(GraphQLString),
|
|
18818
19418
|
description: "draft, published or modified"
|
|
18819
19419
|
},
|
|
18820
|
-
createdAt: { type: nonNull$
|
|
18821
|
-
updatedAt: { type: nonNull$
|
|
19420
|
+
createdAt: { type: nonNull$2(GraphQLString) },
|
|
19421
|
+
updatedAt: { type: nonNull$2(GraphQLString) },
|
|
18822
19422
|
publishedAt: { type: GraphQLString }
|
|
18823
19423
|
}
|
|
18824
19424
|
});
|
|
@@ -19488,6 +20088,12 @@ const updateDeliveryEntry$1 = async (context, modelKey, id, input) => toResult(a
|
|
|
19488
20088
|
}));
|
|
19489
20089
|
const deleteDeliveryEntry$1 = (context, modelKey, id) => deleteEntry$1(context, modelKey, id);
|
|
19490
20090
|
//#endregion
|
|
20091
|
+
//#region src/schema/codegen/graphql/descriptions.ts
|
|
20092
|
+
const describeDefinition = ({ label, description }) => {
|
|
20093
|
+
const help = description?.trim();
|
|
20094
|
+
return help ? `${label}\n\n${help}` : label;
|
|
20095
|
+
};
|
|
20096
|
+
//#endregion
|
|
19491
20097
|
//#region src/schema/codegen/graphql/context.ts
|
|
19492
20098
|
/** An entry as a GraphQL source object. Drafts carry no publication time. */
|
|
19493
20099
|
const entryNode = (entry, scope) => ({
|
|
@@ -19627,7 +20233,7 @@ const definitionFields = (build, owner, masked) => {
|
|
|
19627
20233
|
const resolve = config.resolve;
|
|
19628
20234
|
fields[field.apiKey] = {
|
|
19629
20235
|
...config,
|
|
19630
|
-
|
|
20236
|
+
description: describeDefinition(field),
|
|
19631
20237
|
resolve: (node, args, context, info) => {
|
|
19632
20238
|
if (masked && !Object.hasOwn(node.data, field.apiKey)) {
|
|
19633
20239
|
if (field.required) throw maskedFieldError(field);
|
|
@@ -19641,7 +20247,7 @@ const definitionFields = (build, owner, masked) => {
|
|
|
19641
20247
|
};
|
|
19642
20248
|
const componentType = (build, component) => memoType(build.objects, component.id, () => new GraphQLObjectType({
|
|
19643
20249
|
name: componentTypeName(component),
|
|
19644
|
-
description: component
|
|
20250
|
+
description: describeDefinition(component),
|
|
19645
20251
|
fields: () => definitionFields(build, component, false)
|
|
19646
20252
|
}));
|
|
19647
20253
|
const SYSTEM_STRING = (name) => ({
|
|
@@ -19651,7 +20257,7 @@ const SYSTEM_STRING = (name) => ({
|
|
|
19651
20257
|
const entryType = (build, model) => memoType(build.objects, model.id, () => {
|
|
19652
20258
|
const type = new GraphQLObjectType({
|
|
19653
20259
|
name: modelTypeName(model),
|
|
19654
|
-
description: model
|
|
20260
|
+
description: describeDefinition(model),
|
|
19655
20261
|
fields: () => ({
|
|
19656
20262
|
id: {
|
|
19657
20263
|
type: new GraphQLNonNull(GraphQLID),
|
|
@@ -19728,7 +20334,7 @@ const inputFields = (build, owner) => {
|
|
|
19728
20334
|
const type = fieldInputType(build, owner, field);
|
|
19729
20335
|
if (type) fields[field.apiKey] = {
|
|
19730
20336
|
type,
|
|
19731
|
-
|
|
20337
|
+
description: describeDefinition(field)
|
|
19732
20338
|
};
|
|
19733
20339
|
}
|
|
19734
20340
|
return fields;
|
|
@@ -19788,6 +20394,172 @@ const toWriteData = (build, owner, input) => {
|
|
|
19788
20394
|
return output;
|
|
19789
20395
|
};
|
|
19790
20396
|
//#endregion
|
|
20397
|
+
//#region src/usage/restFieldReads.ts
|
|
20398
|
+
const restFieldReads = async ({ snapshot, model, policy, query, policyFor }) => {
|
|
20399
|
+
const fields = selectFields(model, policy.readMask, query.fields);
|
|
20400
|
+
const selection = query.fields === null ? "implicit" : "explicit";
|
|
20401
|
+
const reads = fields.map((field) => ({
|
|
20402
|
+
path: field.id,
|
|
20403
|
+
selection
|
|
20404
|
+
}));
|
|
20405
|
+
for (const field of fields) {
|
|
20406
|
+
if (field.type !== "relation" || !query.populate.has(field.id)) continue;
|
|
20407
|
+
const target = resolveModelById(snapshot, field.settings.target);
|
|
20408
|
+
if (!target) continue;
|
|
20409
|
+
const targetPolicy = await policyFor(target.definition.id);
|
|
20410
|
+
if (!targetPolicy.allowed) continue;
|
|
20411
|
+
for (const targetField of selectFields(target, targetPolicy.readMask, null)) reads.push({
|
|
20412
|
+
path: relationFieldPath(field.id, targetField.id),
|
|
20413
|
+
selection: "implicit"
|
|
20414
|
+
});
|
|
20415
|
+
}
|
|
20416
|
+
return reads;
|
|
20417
|
+
};
|
|
20418
|
+
//#endregion
|
|
20419
|
+
//#region src/services/contentDelivery.ts
|
|
20420
|
+
const readableModel = async (context, modelKey) => {
|
|
20421
|
+
const model = resolveModel(context.snapshot, modelKey);
|
|
20422
|
+
const policy = await context.permissions.evaluate(context.actor, {
|
|
20423
|
+
action: "read",
|
|
20424
|
+
modelId: model.definition.id
|
|
20425
|
+
});
|
|
20426
|
+
if (!policy.allowed) throw context.actor.kind === "anonymous" ? new AppError(401, "UNAUTHENTICATED", "This content needs a delivery token") : new AppError(403, "FORBIDDEN", `Your token may not read "${modelKey}"`);
|
|
20427
|
+
return {
|
|
20428
|
+
model,
|
|
20429
|
+
policy
|
|
20430
|
+
};
|
|
20431
|
+
};
|
|
20432
|
+
/** The heads a read serves: drafts (GraphQL previews), a pinned snapshot, or the published heads. */
|
|
20433
|
+
const sourceOf = (query, options) => {
|
|
20434
|
+
if (query.snapshot !== void 0 && options.drafts) throw queryInvalid("snapshot reads published content; it cannot be combined with drafts");
|
|
20435
|
+
return options.drafts ? {
|
|
20436
|
+
kind: "heads",
|
|
20437
|
+
state: "draft"
|
|
20438
|
+
} : query.snapshot !== void 0 ? {
|
|
20439
|
+
kind: "snapshot",
|
|
20440
|
+
seq: query.snapshot
|
|
20441
|
+
} : {
|
|
20442
|
+
kind: "heads",
|
|
20443
|
+
state: "published"
|
|
20444
|
+
};
|
|
20445
|
+
};
|
|
20446
|
+
/** A pinned snapshot must already exist: checked against the sequence the read itself saw. */
|
|
20447
|
+
const snapshotCheck = (query) => (current) => {
|
|
20448
|
+
if (query.snapshot !== void 0 && query.snapshot > current) throw snapshotInvalid(query.snapshot, current);
|
|
20449
|
+
};
|
|
20450
|
+
/**
|
|
20451
|
+
* Counts a successful REST read for field usage (plan developer-face §5): once per request, after the
|
|
20452
|
+
* projection, by principal. Admin users and draft reads are never counted.
|
|
20453
|
+
*/
|
|
20454
|
+
const recordUsage = async (context, options, model, policy, query) => {
|
|
20455
|
+
const principalKey = usagePrincipalKey(context.actor);
|
|
20456
|
+
if (!options.usage || options.drafts || principalKey === null) return;
|
|
20457
|
+
const reads = await restFieldReads({
|
|
20458
|
+
snapshot: context.snapshot,
|
|
20459
|
+
model,
|
|
20460
|
+
policy,
|
|
20461
|
+
query,
|
|
20462
|
+
policyFor: (modelId) => context.permissions.evaluate(context.actor, {
|
|
20463
|
+
action: "read",
|
|
20464
|
+
modelId
|
|
20465
|
+
})
|
|
20466
|
+
});
|
|
20467
|
+
options.usage.recordRequest(context.site.id, principalKey, query.snapshot ?? null);
|
|
20468
|
+
options.usage.recordFieldReads(context.site.id, principalKey, model.definition.id, reads);
|
|
20469
|
+
};
|
|
20470
|
+
/** Records usage after the response when the caller can defer it (REST), else before returning. */
|
|
20471
|
+
const afterRead = async (options, task) => {
|
|
20472
|
+
if (options.defer) {
|
|
20473
|
+
options.defer(task);
|
|
20474
|
+
return;
|
|
20475
|
+
}
|
|
20476
|
+
await task();
|
|
20477
|
+
};
|
|
20478
|
+
const ITEM_PARAMETERS$1 = /* @__PURE__ */ new Set([
|
|
20479
|
+
"fields",
|
|
20480
|
+
"populate",
|
|
20481
|
+
"locale",
|
|
20482
|
+
"snapshot",
|
|
20483
|
+
"richText",
|
|
20484
|
+
"seo"
|
|
20485
|
+
]);
|
|
20486
|
+
/** Reading one entry takes only these query parameters; anything else is refused, not ignored. */
|
|
20487
|
+
const assertItemParameters = (rawQuery, allowed) => {
|
|
20488
|
+
const unsupported = [...new URLSearchParams(rawQuery).keys()].find((key) => !allowed.has(key.split("[")[0] ?? key));
|
|
20489
|
+
if (unsupported !== void 0) throw queryInvalid(`"${unsupported}" is not supported when reading one entry`);
|
|
20490
|
+
};
|
|
20491
|
+
/** `GET /api/content/:modelKey`: a page of a collection, or a singleton's one entry. */
|
|
20492
|
+
const listDelivery$1 = async (context, modelKey, rawQuery, options = {}) => {
|
|
20493
|
+
const { model, policy } = await readableModel(context, modelKey);
|
|
20494
|
+
const query = parseQueryFor(context, model, policy, rawQuery, {
|
|
20495
|
+
allowSnapshot: true,
|
|
20496
|
+
allowRichText: true,
|
|
20497
|
+
allowSeo: true
|
|
20498
|
+
});
|
|
20499
|
+
const locale = query.locale ?? context.snapshot.defaultLocale;
|
|
20500
|
+
const singleton = model.definition.kind === "singleton";
|
|
20501
|
+
const { entries, limit, total, seq } = await readEntryPage({
|
|
20502
|
+
context,
|
|
20503
|
+
source: sourceOf(query, options),
|
|
20504
|
+
model,
|
|
20505
|
+
policy,
|
|
20506
|
+
query,
|
|
20507
|
+
fallback: options.fallback ?? true
|
|
20508
|
+
}, {
|
|
20509
|
+
total: !singleton,
|
|
20510
|
+
check: snapshotCheck(query)
|
|
20511
|
+
});
|
|
20512
|
+
const snapshot = query.snapshot ?? seq;
|
|
20513
|
+
let result;
|
|
20514
|
+
if (singleton) {
|
|
20515
|
+
const [entry] = entries;
|
|
20516
|
+
if (!entry) throw entryNotFound(modelKey);
|
|
20517
|
+
result = {
|
|
20518
|
+
data: entry,
|
|
20519
|
+
meta: {
|
|
20520
|
+
locale,
|
|
20521
|
+
snapshot
|
|
20522
|
+
}
|
|
20523
|
+
};
|
|
20524
|
+
} else result = {
|
|
20525
|
+
data: entries,
|
|
20526
|
+
meta: {
|
|
20527
|
+
locale,
|
|
20528
|
+
snapshot,
|
|
20529
|
+
pagination: paginationMeta(query.page, limit, total)
|
|
20530
|
+
}
|
|
20531
|
+
};
|
|
20532
|
+
await afterRead(options, () => recordUsage(context, options, model, policy, query));
|
|
20533
|
+
return result;
|
|
20534
|
+
};
|
|
20535
|
+
/** `GET /api/content/:modelKey/:id`: one published entry (404 for drafts, unknown or unreadable entries). */
|
|
20536
|
+
const getDelivery$1 = async (context, modelKey, id, rawQuery, options = {}) => {
|
|
20537
|
+
assertItemParameters(rawQuery, ITEM_PARAMETERS$1);
|
|
20538
|
+
const { model, policy } = await readableModel(context, modelKey);
|
|
20539
|
+
const query = parseQueryFor(context, model, policy, rawQuery, {
|
|
20540
|
+
allowSnapshot: true,
|
|
20541
|
+
allowRichText: true,
|
|
20542
|
+
allowSeo: true
|
|
20543
|
+
});
|
|
20544
|
+
const { entry, seq } = await readOneEntry({
|
|
20545
|
+
context,
|
|
20546
|
+
source: sourceOf(query, options),
|
|
20547
|
+
model,
|
|
20548
|
+
policy,
|
|
20549
|
+
query,
|
|
20550
|
+
fallback: options.fallback ?? true
|
|
20551
|
+
}, id, { check: snapshotCheck(query) });
|
|
20552
|
+
if (!entry) throw entryNotFound(id);
|
|
20553
|
+
await afterRead(options, () => recordUsage(context, options, model, policy, query));
|
|
20554
|
+
return {
|
|
20555
|
+
data: entry,
|
|
20556
|
+
meta: {
|
|
20557
|
+
locale: query.locale ?? context.snapshot.defaultLocale,
|
|
20558
|
+
snapshot: query.snapshot ?? seq
|
|
20559
|
+
}
|
|
20560
|
+
};
|
|
20561
|
+
};
|
|
20562
|
+
//#endregion
|
|
19791
20563
|
//#region src/schema/codegen/graphql/queryArgs.ts
|
|
19792
20564
|
/**
|
|
19793
20565
|
* `XFilter` and `XSort` inputs, and their translation to the REST querystring the content compiler parses
|
|
@@ -19834,7 +20606,7 @@ const createFilterInput = (model, fixed) => {
|
|
|
19834
20606
|
for (const [name, filter] of SYSTEM_FILTERS) fields[name] = { type: fixed.filters[filter] };
|
|
19835
20607
|
for (const field of filterableFields(model)) fields[field.apiKey] = {
|
|
19836
20608
|
type: fixed.filters[FILTER_TYPE_BY_DATA_TYPE[field.type]],
|
|
19837
|
-
|
|
20609
|
+
description: describeDefinition(field)
|
|
19838
20610
|
};
|
|
19839
20611
|
fields.and = {
|
|
19840
20612
|
type: new GraphQLList(new GraphQLNonNull(type)),
|
|
@@ -20133,6 +20905,65 @@ const mutationFields = (build, model) => {
|
|
|
20133
20905
|
return fields;
|
|
20134
20906
|
};
|
|
20135
20907
|
//#endregion
|
|
20908
|
+
//#region src/schema/codegen/graphql/siteFields.ts
|
|
20909
|
+
/**
|
|
20910
|
+
* `_site` (plan seo-fields): the request's site and its SEO defaults, through the same service as REST
|
|
20911
|
+
* `GET /api/site`. Underscored like `_snapshot`, so a model called `site` keeps its own root fields; the name
|
|
20912
|
+
* and the `SiteInfo`/`SiteSeo*` types are reserved in `@shapio/schema` naming.ts. SEO fields on entries stay
|
|
20913
|
+
* raw in GraphQL: merge them with `resolveSeo()` from `@shapio/client`.
|
|
20914
|
+
*/
|
|
20915
|
+
const SITE_QUERY = "_site";
|
|
20916
|
+
const nonNull$1 = (type) => new GraphQLNonNull(type);
|
|
20917
|
+
const createTypes$1 = (media) => {
|
|
20918
|
+
const locale = new GraphQLObjectType({
|
|
20919
|
+
name: "SiteSeoLocale",
|
|
20920
|
+
description: "One locale's default SEO texts; a missing one falls back along the locale chain",
|
|
20921
|
+
fields: {
|
|
20922
|
+
locale: { type: nonNull$1(GraphQLString) },
|
|
20923
|
+
siteName: { type: GraphQLString },
|
|
20924
|
+
titleTemplate: {
|
|
20925
|
+
type: GraphQLString,
|
|
20926
|
+
description: "Page titles go through it: `%s` is replaced by the title, e.g. `%s · Acme`"
|
|
20927
|
+
},
|
|
20928
|
+
description: {
|
|
20929
|
+
type: GraphQLString,
|
|
20930
|
+
description: "Used where an entry has no SEO description"
|
|
20931
|
+
}
|
|
20932
|
+
}
|
|
20933
|
+
});
|
|
20934
|
+
const seo = new GraphQLObjectType({
|
|
20935
|
+
name: "SiteSeoDefaults",
|
|
20936
|
+
fields: {
|
|
20937
|
+
locales: {
|
|
20938
|
+
type: nonNull$1(new GraphQLList(nonNull$1(locale))),
|
|
20939
|
+
resolve: (source) => Object.entries(source.locales).map(([code, texts]) => ({
|
|
20940
|
+
locale: code,
|
|
20941
|
+
...texts
|
|
20942
|
+
}))
|
|
20943
|
+
},
|
|
20944
|
+
twitterHandle: { type: GraphQLString },
|
|
20945
|
+
image: {
|
|
20946
|
+
type: media,
|
|
20947
|
+
description: "The default social image (a public image), or null"
|
|
20948
|
+
}
|
|
20949
|
+
}
|
|
20950
|
+
});
|
|
20951
|
+
return new GraphQLObjectType({
|
|
20952
|
+
name: "SiteInfo",
|
|
20953
|
+
description: "The site this request reads, and its SEO defaults",
|
|
20954
|
+
fields: {
|
|
20955
|
+
key: { type: nonNull$1(GraphQLString) },
|
|
20956
|
+
name: { type: nonNull$1(GraphQLString) },
|
|
20957
|
+
seo: { type: nonNull$1(seo) }
|
|
20958
|
+
}
|
|
20959
|
+
});
|
|
20960
|
+
};
|
|
20961
|
+
const siteQueryFields = (snapshot, fixed) => ({ [SITE_QUERY]: {
|
|
20962
|
+
type: nonNull$1(createTypes$1(fixed.media)),
|
|
20963
|
+
description: "The request's site: its key, name and SEO defaults",
|
|
20964
|
+
resolve: async (_root, _args, context) => getDeliverySite$1(await context.content(snapshot))
|
|
20965
|
+
} });
|
|
20966
|
+
//#endregion
|
|
20136
20967
|
//#region src/db/snapshotDiffQuery.ts
|
|
20137
20968
|
const POSTGRES = {
|
|
20138
20969
|
uuid: (value) => sql`${value}::uuid`,
|
|
@@ -20533,7 +21364,7 @@ const buildGraphqlSchema = (snapshot) => {
|
|
|
20533
21364
|
description: "The schema version this API reflects; it changes whenever a model changes",
|
|
20534
21365
|
resolve: () => snapshot.version
|
|
20535
21366
|
};
|
|
20536
|
-
Object.assign(query, snapshotQueryFields(snapshot));
|
|
21367
|
+
Object.assign(query, snapshotQueryFields(snapshot), siteQueryFields(snapshot, build.fixed));
|
|
20537
21368
|
const schema = new GraphQLSchema({
|
|
20538
21369
|
query: new GraphQLObjectType({
|
|
20539
21370
|
name: "Query",
|
|
@@ -20817,6 +21648,56 @@ const publishingPlugin = fp(async (app, { runtime }) => {
|
|
|
20817
21648
|
app.decorate("publishing", runtime);
|
|
20818
21649
|
}, { name: "shapio-publishing" });
|
|
20819
21650
|
//#endregion
|
|
21651
|
+
//#region src/plugins/requestLog.ts
|
|
21652
|
+
/** How the caller authenticated, as logged: the principal's kind, never its ids. */
|
|
21653
|
+
const PRINCIPAL_LABELS = {
|
|
21654
|
+
anonymous: "anonymous",
|
|
21655
|
+
admin: "admin",
|
|
21656
|
+
token: "token",
|
|
21657
|
+
appUser: "app",
|
|
21658
|
+
system: "system"
|
|
21659
|
+
};
|
|
21660
|
+
/** The concrete path without its query string, which may carry credentials (signed media URLs, OAuth). */
|
|
21661
|
+
const pathOf = (url) => {
|
|
21662
|
+
const queryStart = url.indexOf("?");
|
|
21663
|
+
return queryStart === -1 ? url : url.slice(0, queryStart);
|
|
21664
|
+
};
|
|
21665
|
+
/** Milliseconds with one decimal. */
|
|
21666
|
+
const roundMs = (ms) => Math.round(ms * 10) / 10;
|
|
21667
|
+
/**
|
|
21668
|
+
* The request's one response line. `request.principal` is undefined when the request failed before
|
|
21669
|
+
* authentication ran; `request.site` only on site routes; `request.errorCode` only when the central error
|
|
21670
|
+
* handler (plugins/errorHandler.ts) answered.
|
|
21671
|
+
*/
|
|
21672
|
+
const responseLineOf = (request, reply) => {
|
|
21673
|
+
const principal = request.principal;
|
|
21674
|
+
return {
|
|
21675
|
+
method: request.method,
|
|
21676
|
+
...request.routeOptions.url ? { route: request.routeOptions.url } : {},
|
|
21677
|
+
path: pathOf(request.url),
|
|
21678
|
+
status: reply.statusCode,
|
|
21679
|
+
ms: roundMs(reply.elapsedTime),
|
|
21680
|
+
...request.site ? { site: request.site.key } : {},
|
|
21681
|
+
...principal ? { principal: PRINCIPAL_LABELS[principal.kind] } : {},
|
|
21682
|
+
...request.errorCode ? { code: request.errorCode } : {}
|
|
21683
|
+
};
|
|
21684
|
+
};
|
|
21685
|
+
/**
|
|
21686
|
+
* Replaces Fastify's request logging (`disableRequestLogging`): one info line per response with the fields
|
|
21687
|
+
* that matter, and the request-start line (full redacted URL, host, remote address) at debug. The logger's
|
|
21688
|
+
* `reqId` binding is `request.id`, the id audit entries and extension contexts carry.
|
|
21689
|
+
*/
|
|
21690
|
+
const requestLogPlugin = fp(async (app) => {
|
|
21691
|
+
app.addHook("onRequest", async (request) => {
|
|
21692
|
+
request.log.debug({ req: request }, "incoming request");
|
|
21693
|
+
});
|
|
21694
|
+
app.addHook("onResponse", async (request, reply) => {
|
|
21695
|
+
const line = responseLineOf(request, reply);
|
|
21696
|
+
if (request.routeOptions.config?.requestLog === "debug") request.log.debug(line, "request");
|
|
21697
|
+
else request.log.info(line, "request");
|
|
21698
|
+
});
|
|
21699
|
+
}, { name: "shapio-request-log" });
|
|
21700
|
+
//#endregion
|
|
20820
21701
|
//#region src/plugins/security.ts
|
|
20821
21702
|
const CORS_METHODS = [
|
|
20822
21703
|
"GET",
|
|
@@ -21362,11 +22243,30 @@ const NOOP_USAGE_TRACKER = {
|
|
|
21362
22243
|
};
|
|
21363
22244
|
//#endregion
|
|
21364
22245
|
//#region src/plugins/usage.ts
|
|
22246
|
+
/** Starts queued recordings after the response and lets `flush` and shutdown wait for them. */
|
|
22247
|
+
const createAfterResponseQueue = () => {
|
|
22248
|
+
const pending = /* @__PURE__ */ new Set();
|
|
22249
|
+
const run = (log, task) => {
|
|
22250
|
+
const started = new Promise((resolve) => setImmediate(resolve)).then(task).catch((error) => {
|
|
22251
|
+
log.error({ err: error }, "recording field usage failed");
|
|
22252
|
+
}).finally(() => pending.delete(started));
|
|
22253
|
+
pending.add(started);
|
|
22254
|
+
};
|
|
22255
|
+
const settled = async () => {
|
|
22256
|
+
await Promise.all([...pending]);
|
|
22257
|
+
};
|
|
22258
|
+
return {
|
|
22259
|
+
run,
|
|
22260
|
+
settled
|
|
22261
|
+
};
|
|
22262
|
+
};
|
|
21365
22263
|
/**
|
|
21366
22264
|
* Field usage from delivery traffic (plan developer-face §5): one in-memory aggregator per instance,
|
|
21367
22265
|
* flushed on a timer, when it fills up, and when the app closes (so a clean shutdown loses nothing).
|
|
21368
22266
|
*/
|
|
21369
22267
|
const usagePlugin = fp(async (app, { config, db }) => {
|
|
22268
|
+
const queue = createAfterResponseQueue();
|
|
22269
|
+
app.decorate("recordUsageAfterResponse", queue.run);
|
|
21370
22270
|
if (!config.enabled) {
|
|
21371
22271
|
app.decorate("usage", NOOP_USAGE_TRACKER);
|
|
21372
22272
|
return;
|
|
@@ -21377,8 +22277,15 @@ const usagePlugin = fp(async (app, { config, db }) => {
|
|
|
21377
22277
|
flushIntervalMs: config.flushIntervalMs,
|
|
21378
22278
|
log
|
|
21379
22279
|
});
|
|
21380
|
-
app.decorate("usage",
|
|
22280
|
+
app.decorate("usage", {
|
|
22281
|
+
...aggregator,
|
|
22282
|
+
flush: async () => {
|
|
22283
|
+
await queue.settled();
|
|
22284
|
+
await aggregator.flush();
|
|
22285
|
+
}
|
|
22286
|
+
});
|
|
21381
22287
|
app.addHook("onClose", async () => {
|
|
22288
|
+
await queue.settled();
|
|
21382
22289
|
await aggregator.close();
|
|
21383
22290
|
});
|
|
21384
22291
|
}, { name: "shapio-usage" });
|
|
@@ -21616,7 +22523,7 @@ const createRole$1 = async (context, input, schema) => {
|
|
|
21616
22523
|
kind: input.kind
|
|
21617
22524
|
}, trx);
|
|
21618
22525
|
await insertPermissions$1(toRows$1(role.id, input.permissions), trx);
|
|
21619
|
-
await bumpPermissionsVersion(trx);
|
|
22526
|
+
await bumpPermissionsVersion$2(trx);
|
|
21620
22527
|
await recordAudit(trx, {
|
|
21621
22528
|
...context,
|
|
21622
22529
|
action: "role.create",
|
|
@@ -21661,7 +22568,7 @@ const updateRole$1 = async (context, id, input, schema) => db.transaction().exec
|
|
|
21661
22568
|
if (input.permissions) {
|
|
21662
22569
|
await deletePermissionsForRole$1(id, trx);
|
|
21663
22570
|
await insertPermissions$1(toRows$1(id, input.permissions), trx);
|
|
21664
|
-
await bumpPermissionsVersion(trx);
|
|
22571
|
+
await bumpPermissionsVersion$2(trx);
|
|
21665
22572
|
}
|
|
21666
22573
|
await recordAudit(trx, {
|
|
21667
22574
|
...context,
|
|
@@ -21684,7 +22591,7 @@ const deleteRole$1 = async (context, id) => {
|
|
|
21684
22591
|
if (holders.users > 0 || holders.tokens > 0) throw new AppError(409, "ROLE_IN_USE", "Remove this role from every admin and API token first", holders);
|
|
21685
22592
|
await deleteRevokedForRole(id, trx);
|
|
21686
22593
|
await deleteById$5(id, trx);
|
|
21687
|
-
await bumpPermissionsVersion(trx);
|
|
22594
|
+
await bumpPermissionsVersion$2(trx);
|
|
21688
22595
|
await recordAudit(trx, {
|
|
21689
22596
|
...context,
|
|
21690
22597
|
action: "role.delete",
|
|
@@ -21758,7 +22665,7 @@ const createAppRole$1 = async (context, input, schema) => {
|
|
|
21758
22665
|
description: input.description.trim()
|
|
21759
22666
|
}, trx);
|
|
21760
22667
|
await insertPermissions(toRows(role.id, input.permissions), trx);
|
|
21761
|
-
await bumpPermissionsVersion(trx);
|
|
22668
|
+
await bumpPermissionsVersion$2(trx);
|
|
21762
22669
|
await recordAudit(trx, {
|
|
21763
22670
|
...context,
|
|
21764
22671
|
action: "app_role.create",
|
|
@@ -21800,7 +22707,7 @@ const updateAppRole$1 = async (context, id, input, schema) => db.transaction().e
|
|
|
21800
22707
|
if (input.permissions) {
|
|
21801
22708
|
await deletePermissionsForRole(id, trx);
|
|
21802
22709
|
await insertPermissions(toRows(id, input.permissions), trx);
|
|
21803
|
-
await bumpPermissionsVersion(trx);
|
|
22710
|
+
await bumpPermissionsVersion$2(trx);
|
|
21804
22711
|
}
|
|
21805
22712
|
await recordAudit(trx, {
|
|
21806
22713
|
...context,
|
|
@@ -21825,7 +22732,7 @@ const deleteAppRole$1 = async (context, id) => {
|
|
|
21825
22732
|
const holders = await countHolders(id, trx);
|
|
21826
22733
|
if (holders > 0) throw new AppError(409, "ROLE_IN_USE", "Remove this role from every app user first", { users: holders });
|
|
21827
22734
|
await deleteById$2(id, trx);
|
|
21828
|
-
await bumpPermissionsVersion(trx);
|
|
22735
|
+
await bumpPermissionsVersion$2(trx);
|
|
21829
22736
|
await recordAudit(trx, {
|
|
21830
22737
|
...context,
|
|
21831
22738
|
action: "app_role.delete",
|
|
@@ -22317,7 +23224,7 @@ const updateAppUser$1 = async (context, id, input) => db.transaction().execute(a
|
|
|
22317
23224
|
}
|
|
22318
23225
|
});
|
|
22319
23226
|
}
|
|
22320
|
-
await bumpPermissionsVersion(trx);
|
|
23227
|
+
await bumpPermissionsVersion$2(trx);
|
|
22321
23228
|
const view = await findSummaryById(id, trx);
|
|
22322
23229
|
if (!view) throw notFound$4();
|
|
22323
23230
|
return toAdminView(view);
|
|
@@ -22339,7 +23246,7 @@ const deleteAppUserInTransaction = async (context, user, trx) => {
|
|
|
22339
23246
|
await endAllSignIns(user.id, now, "deleted", trx);
|
|
22340
23247
|
await consumeAllForUser$1("email_confirmations", user.id, now, trx);
|
|
22341
23248
|
await consumeAllForUser$1("app_password_resets", user.id, now, trx);
|
|
22342
|
-
await bumpPermissionsVersion(trx);
|
|
23249
|
+
await bumpPermissionsVersion$2(trx);
|
|
22343
23250
|
await recordAudit(trx, {
|
|
22344
23251
|
...context,
|
|
22345
23252
|
action: "app_user.delete",
|
|
@@ -23647,7 +24554,6 @@ const toInputFormat = (model, fields, data) => {
|
|
|
23647
24554
|
fields: present,
|
|
23648
24555
|
visibleTargets: null,
|
|
23649
24556
|
populated: /* @__PURE__ */ new Map(),
|
|
23650
|
-
richTextHtml: false,
|
|
23651
24557
|
mediaAssets: null
|
|
23652
24558
|
});
|
|
23653
24559
|
};
|
|
@@ -25926,9 +26832,99 @@ const createDefinitionRoutes = (category) => async (app) => {
|
|
|
25926
26832
|
}, handlers.listChanges);
|
|
25927
26833
|
};
|
|
25928
26834
|
//#endregion
|
|
26835
|
+
//#region src/services/builtinComponents.ts
|
|
26836
|
+
const existing = (context, active) => ({
|
|
26837
|
+
status: "existing",
|
|
26838
|
+
definitionId: active.definition.id,
|
|
26839
|
+
version: active.version,
|
|
26840
|
+
schemaVersion: context.snapshot.version
|
|
26841
|
+
});
|
|
26842
|
+
/** Another definition already holds the API ID in some view the shared component would join. */
|
|
26843
|
+
const conflictOf = async (context) => {
|
|
26844
|
+
const taken = context.snapshot.network.definitions.find((entry) => entry.definition.id !== "f3905a00-d2a3-4c62-899a-c894bfd6cb9b" && entry.definition.apiKey.toLowerCase() === "seo".toLowerCase());
|
|
26845
|
+
if (!taken) return;
|
|
26846
|
+
const siteKey = taken.siteId === null ? null : (await loadSiteKeys(context.db)).get(taken.siteId) ?? null;
|
|
26847
|
+
const where = siteKey === null ? "shared with all sites" : `on site "${siteKey}"`;
|
|
26848
|
+
return new AppError(409, "SEO_COMPONENT_CONFLICT", `The API ID "${taken.definition.apiKey}" is already used by the ${taken.definition.kind} "${taken.definition.label}" (${where}). Rename or delete it, then add SEO fields again.`, {
|
|
26849
|
+
definitionId: taken.definition.id,
|
|
26850
|
+
apiKey: taken.definition.apiKey,
|
|
26851
|
+
kind: taken.definition.kind,
|
|
26852
|
+
label: taken.definition.label,
|
|
26853
|
+
scope: taken.siteId === null ? "network" : "site",
|
|
26854
|
+
siteKey
|
|
26855
|
+
});
|
|
26856
|
+
};
|
|
26857
|
+
/** The SEO component belongs to another site only: it cannot be shared from here. */
|
|
26858
|
+
const elsewhere = () => new AppError(409, "SEO_COMPONENT_CONFLICT", "The SEO component is kept on another site. A network admin can share it with all sites.", {
|
|
26859
|
+
definitionId: SEO_COMPONENT_ID,
|
|
26860
|
+
scope: "site"
|
|
26861
|
+
});
|
|
26862
|
+
/**
|
|
26863
|
+
* Makes sure the built-in SEO component is in the request's view: returns it when it is (shared, or, for an
|
|
26864
|
+
* instance seeded before it was shared, the site's own), else creates it shared (needs `schema.create` on
|
|
26865
|
+
* every site). Idempotent: a concurrent create that won is returned as existing on the next call.
|
|
26866
|
+
*/
|
|
26867
|
+
const ensureSeoComponent$1 = async (context) => {
|
|
26868
|
+
const inView = context.snapshot.byId.get(SEO_COMPONENT_ID);
|
|
26869
|
+
if (inView) return existing(context, inView);
|
|
26870
|
+
await assertCanCreate(context, null);
|
|
26871
|
+
if (context.snapshot.network.byId.has("f3905a00-d2a3-4c62-899a-c894bfd6cb9b")) throw elsewhere();
|
|
26872
|
+
const conflict = await conflictOf(context);
|
|
26873
|
+
if (conflict) throw conflict;
|
|
26874
|
+
const outcome = await applyChange(context, {
|
|
26875
|
+
category: "component",
|
|
26876
|
+
definition: seoComponentDefinition(),
|
|
26877
|
+
expectedVersion: null,
|
|
26878
|
+
scope: "network"
|
|
26879
|
+
});
|
|
26880
|
+
if (outcome.status !== "activated") throw new Error(`Creating the SEO component ended ${outcome.status}`);
|
|
26881
|
+
return {
|
|
26882
|
+
status: "created",
|
|
26883
|
+
definitionId: outcome.definitionId,
|
|
26884
|
+
version: outcome.version ?? 1,
|
|
26885
|
+
schemaVersion: outcome.schemaVersion
|
|
26886
|
+
};
|
|
26887
|
+
};
|
|
26888
|
+
//#endregion
|
|
26889
|
+
//#region src/controllers/builtinComponents.ts
|
|
26890
|
+
const ensureSeoComponent = async (request, reply) => {
|
|
26891
|
+
const outcome = await ensureSeoComponent$1(await schemaContextFor(request));
|
|
26892
|
+
return reply.code(outcome.status === "created" ? 201 : 200).send(outcome);
|
|
26893
|
+
};
|
|
26894
|
+
//#endregion
|
|
26895
|
+
//#region src/routes/admin/components/builtinSchemas.ts
|
|
26896
|
+
const EnsureOutcomeSchema = Type.Object({
|
|
26897
|
+
/** `created` when this call created it (a new schema version), `existing` when it was already there. */
|
|
26898
|
+
status: Type.Enum(["existing", "created"]),
|
|
26899
|
+
definitionId: Type.String(),
|
|
26900
|
+
version: Type.Integer(),
|
|
26901
|
+
schemaVersion: Type.Integer()
|
|
26902
|
+
});
|
|
26903
|
+
const ensureSeoComponentSchema = { response: {
|
|
26904
|
+
200: EnsureOutcomeSchema,
|
|
26905
|
+
201: EnsureOutcomeSchema,
|
|
26906
|
+
403: ErrorResponseSchema,
|
|
26907
|
+
409: ErrorResponseSchema,
|
|
26908
|
+
422: ErrorResponseSchema
|
|
26909
|
+
} };
|
|
26910
|
+
//#endregion
|
|
26911
|
+
//#region src/routes/admin/components/builtin.ts
|
|
26912
|
+
/** /api/admin/components/builtin: Shapio's built-in components, created on first use (plan seo-fields). */
|
|
26913
|
+
const builtinComponentsRoutes = async (app) => {
|
|
26914
|
+
declareSiteScope(app, "site");
|
|
26915
|
+
app.post("/seo/ensure", {
|
|
26916
|
+
schema: ensureSeoComponentSchema,
|
|
26917
|
+
preHandler: app.requireAdmin,
|
|
26918
|
+
config: { audit: { action: "schema.activate" } }
|
|
26919
|
+
}, ensureSeoComponent);
|
|
26920
|
+
};
|
|
26921
|
+
//#endregion
|
|
25929
26922
|
//#region src/routes/admin/components/index.ts
|
|
25930
|
-
/** /api/admin/components: reusable components (no entries of their own). */
|
|
25931
|
-
const componentsRoutes =
|
|
26923
|
+
/** /api/admin/components: reusable components (no entries of their own), and the built-in ones. */
|
|
26924
|
+
const componentsRoutes = async (app) => {
|
|
26925
|
+
await app.register(createDefinitionRoutes("component"));
|
|
26926
|
+
await app.register(builtinComponentsRoutes, { prefix: "/builtin" });
|
|
26927
|
+
};
|
|
25932
26928
|
//#endregion
|
|
25933
26929
|
//#region src/controllers/adminContent.ts
|
|
25934
26930
|
const revisionSummary = (row) => ({
|
|
@@ -27947,6 +28943,60 @@ const adminSetupRoutes = async (app) => {
|
|
|
27947
28943
|
}, completeSetup);
|
|
27948
28944
|
};
|
|
27949
28945
|
//#endregion
|
|
28946
|
+
//#region src/controllers/siteSeo.ts
|
|
28947
|
+
const contextFor$2 = async (request) => ({
|
|
28948
|
+
...toSiteActorContext(request),
|
|
28949
|
+
db: request.server.db,
|
|
28950
|
+
snapshot: await getRequestSchema(request),
|
|
28951
|
+
permissions: getRequestPermissions(request)
|
|
28952
|
+
});
|
|
28953
|
+
const getSiteSeo = async (request) => getSiteSeo$1(await contextFor$2(request));
|
|
28954
|
+
const updateSiteSeo = async (request) => updateSiteSeo$1(await contextFor$2(request), request.body);
|
|
28955
|
+
//#endregion
|
|
28956
|
+
//#region src/routes/admin/site/schemas.ts
|
|
28957
|
+
/** The request's site's SEO defaults and the site version an update must name. */
|
|
28958
|
+
const SiteSeoSchema = Type.Object({
|
|
28959
|
+
version: Type.Integer(),
|
|
28960
|
+
seo: SeoDefaultsSchema
|
|
28961
|
+
});
|
|
28962
|
+
const getSiteSeoSchema = { response: {
|
|
28963
|
+
200: SiteSeoSchema,
|
|
28964
|
+
403: ErrorResponseSchema,
|
|
28965
|
+
404: ErrorResponseSchema
|
|
28966
|
+
} };
|
|
28967
|
+
const updateSiteSeoSchema = {
|
|
28968
|
+
body: Type.Object({
|
|
28969
|
+
expectedVersion: Type.Integer({ minimum: 1 }),
|
|
28970
|
+
seo: SeoDefaultsSchema
|
|
28971
|
+
}, { additionalProperties: false }),
|
|
28972
|
+
response: {
|
|
28973
|
+
200: SiteSeoSchema,
|
|
28974
|
+
400: ErrorResponseSchema,
|
|
28975
|
+
404: ErrorResponseSchema,
|
|
28976
|
+
409: ErrorResponseSchema
|
|
28977
|
+
}
|
|
28978
|
+
};
|
|
28979
|
+
//#endregion
|
|
28980
|
+
//#region src/routes/admin/site/index.ts
|
|
28981
|
+
/**
|
|
28982
|
+
* /api/admin/site: the request's site's own settings (plan seo-fields), named by the `Shapio-Site` header.
|
|
28983
|
+
* Reading them needs read access to some content of the site (checked by the service); changing them needs
|
|
28984
|
+
* `site.settings` on that site (a site admin needs no network role).
|
|
28985
|
+
*/
|
|
28986
|
+
const adminSiteRoutes = async (app) => {
|
|
28987
|
+
declareSiteScope(app, "site");
|
|
28988
|
+
const settings = { preHandler: app.requireGlobalPermission("site.settings") };
|
|
28989
|
+
app.get("/seo", {
|
|
28990
|
+
schema: getSiteSeoSchema,
|
|
28991
|
+
preHandler: app.requireAdmin
|
|
28992
|
+
}, getSiteSeo);
|
|
28993
|
+
app.put("/seo", {
|
|
28994
|
+
schema: updateSiteSeoSchema,
|
|
28995
|
+
config: { audit: { action: "site.seo_update" } },
|
|
28996
|
+
...settings
|
|
28997
|
+
}, updateSiteSeo);
|
|
28998
|
+
};
|
|
28999
|
+
//#endregion
|
|
27950
29000
|
//#region src/services/siteAppRoles.ts
|
|
27951
29001
|
const toView$3 = (siteId, rows) => {
|
|
27952
29002
|
const of = (audience) => rows.filter((row) => row.audience === audience).map((row) => row.role_id);
|
|
@@ -27976,7 +29026,7 @@ const setSiteAppRoles$1 = async (context, siteId, input) => db.transaction().exe
|
|
|
27976
29026
|
const before = toView$3(siteId, await listForSite$1(siteId, trx));
|
|
27977
29027
|
await replaceForAudience(siteId, "public", next.public, trx);
|
|
27978
29028
|
await replaceForAudience(siteId, "authenticated", next.authenticated, trx);
|
|
27979
|
-
await bumpPermissionsVersion(trx);
|
|
29029
|
+
await bumpPermissionsVersion$2(trx);
|
|
27980
29030
|
await recordAudit(trx, {
|
|
27981
29031
|
...context,
|
|
27982
29032
|
action: "site.app_roles_update",
|
|
@@ -28090,10 +29140,10 @@ const adminSitesRoutes = async (app) => {
|
|
|
28090
29140
|
};
|
|
28091
29141
|
//#endregion
|
|
28092
29142
|
//#region src/controllers/apiTokens.ts
|
|
28093
|
-
const listTokens = async (request) => listApiTokens(toSiteActorContext(request), request
|
|
29143
|
+
const listTokens = async (request) => listApiTokens(toSiteActorContext(request), getRequestPermissions(request));
|
|
28094
29144
|
const createToken = async (request, reply) => {
|
|
28095
29145
|
const { name, roleId, expiresAt, network } = request.body;
|
|
28096
|
-
const created = await createApiToken(toSiteActorContext(request), request
|
|
29146
|
+
const created = await createApiToken(toSiteActorContext(request), getRequestPermissions(request), {
|
|
28097
29147
|
name,
|
|
28098
29148
|
roleId,
|
|
28099
29149
|
expiresAt: expiresAt ? new Date(expiresAt) : null,
|
|
@@ -28102,7 +29152,7 @@ const createToken = async (request, reply) => {
|
|
|
28102
29152
|
return reply.code(201).send(created);
|
|
28103
29153
|
};
|
|
28104
29154
|
const revokeToken = async (request, reply) => {
|
|
28105
|
-
await revokeApiToken(toSiteActorContext(request), request
|
|
29155
|
+
await revokeApiToken(toSiteActorContext(request), getRequestPermissions(request), request.params.id);
|
|
28106
29156
|
return reply.code(204).send();
|
|
28107
29157
|
};
|
|
28108
29158
|
//#endregion
|
|
@@ -28682,6 +29732,7 @@ const adminIdentityRoutes = async (app) => {
|
|
|
28682
29732
|
await app.register(adminEditingRoutes);
|
|
28683
29733
|
await app.register(adminUsageRoutes, { prefix: "/usage" });
|
|
28684
29734
|
await app.register(adminSitesRoutes, { prefix: "/sites" });
|
|
29735
|
+
await app.register(adminSiteRoutes, { prefix: "/site" });
|
|
28685
29736
|
};
|
|
28686
29737
|
//#endregion
|
|
28687
29738
|
//#region src/services/locales.ts
|
|
@@ -29362,7 +30413,7 @@ const mediaContextFor = (request) => {
|
|
|
29362
30413
|
storage: server.mediaStorage,
|
|
29363
30414
|
urls: server.urls,
|
|
29364
30415
|
signingSecret: server.signingSecret,
|
|
29365
|
-
permissions:
|
|
30416
|
+
permissions: getRequestPermissions(request),
|
|
29366
30417
|
limits: {
|
|
29367
30418
|
maxUploadBytes: server.config.storage.maxUploadBytes,
|
|
29368
30419
|
allowedTypes: server.config.storage.allowedTypes
|
|
@@ -31317,7 +32368,8 @@ const requestJson = async (runtime, input) => {
|
|
|
31317
32368
|
status: response.status,
|
|
31318
32369
|
ok: isSuccessStatus(response.status),
|
|
31319
32370
|
json,
|
|
31320
|
-
text: response.body
|
|
32371
|
+
text: response.body,
|
|
32372
|
+
location: response.headers.location
|
|
31321
32373
|
};
|
|
31322
32374
|
};
|
|
31323
32375
|
/** A short, safe description of a failed provider response (status plus the provider's own message). */
|
|
@@ -31398,6 +32450,28 @@ const findDeploymentSince$1 = async (context, since) => {
|
|
|
31398
32450
|
return (deployment.deployment_trigger?.type === void 0 || deployment.deployment_trigger.type === "deploy_hook") && createdSince(toEpochMs(deployment.created_on), since);
|
|
31399
32451
|
});
|
|
31400
32452
|
};
|
|
32453
|
+
const isFinished = (deployment) => {
|
|
32454
|
+
const { name, status } = deployment.latest_stage ?? {};
|
|
32455
|
+
return status === "failure" || status === "canceled" || status === "skipped" || name === "deploy" && status === "success";
|
|
32456
|
+
};
|
|
32457
|
+
/** The newest unfinished deployment, whatever started it (the build a 304 from the deploy hook refers to). */
|
|
32458
|
+
const findPendingDeployment = async (context) => {
|
|
32459
|
+
return (await apiRequest$2(context, `${projectPath(context)}/deployments`)).find((deployment) => !isFinished(deployment));
|
|
32460
|
+
};
|
|
32461
|
+
/** A 304 without Location: Cloudflare already has a deployment queued for the hook's branch. */
|
|
32462
|
+
const isAlreadyQueued = (response) => response.status === 304 && !response.location;
|
|
32463
|
+
const ALREADY_QUEUED_MESSAGE = "Cloudflare: a deployment was already queued for this branch (deploy hook HTTP 304), so no new one was started";
|
|
32464
|
+
const followQueuedDeployment = async (context) => {
|
|
32465
|
+
const pending = await findPendingDeployment(context);
|
|
32466
|
+
if (!pending) return {
|
|
32467
|
+
status: "triggered",
|
|
32468
|
+
message: ALREADY_QUEUED_MESSAGE
|
|
32469
|
+
};
|
|
32470
|
+
return {
|
|
32471
|
+
...reportOf(context, pending),
|
|
32472
|
+
message: `${ALREADY_QUEUED_MESSAGE}; following it`
|
|
32473
|
+
};
|
|
32474
|
+
};
|
|
31401
32475
|
const deploymentOf$1 = (context) => {
|
|
31402
32476
|
const id = context.run.provider_ref;
|
|
31403
32477
|
return id ? apiRequest$2(context, `${projectPath(context)}/deployments/${encodeURIComponent(id)}`) : findDeploymentSince$1(context, context.run.triggered_at ?? context.run.created_at);
|
|
@@ -31436,6 +32510,7 @@ const cloudflarePagesProvider = {
|
|
|
31436
32510
|
policy: context.policy,
|
|
31437
32511
|
signal: context.signal
|
|
31438
32512
|
});
|
|
32513
|
+
if (isAlreadyQueued(response)) return followQueuedDeployment(context);
|
|
31439
32514
|
const envelope = envelopeOf(response);
|
|
31440
32515
|
if (!response.ok || envelope.success === false) throw new Error(`Cloudflare deploy hook ${describeFailure(response, errorMessageOf$2(response))}`);
|
|
31441
32516
|
const id = envelope.result?.id;
|
|
@@ -35255,7 +36330,8 @@ const WEBHOOK_EVENT_CATALOGUE = [
|
|
|
35255
36330
|
"locale.removed"
|
|
35256
36331
|
]),
|
|
35257
36332
|
...group("change_set", Object.values(CHANGE_SET_EVENTS)),
|
|
35258
|
-
...group("deployment", Object.values(DEPLOYMENT_EVENTS))
|
|
36333
|
+
...group("deployment", Object.values(DEPLOYMENT_EVENTS)),
|
|
36334
|
+
...group("site", ["site.updated"])
|
|
35259
36335
|
];
|
|
35260
36336
|
/** Sent by "Send test"; never produced by the outbox. */
|
|
35261
36337
|
const WEBHOOK_TEST_EVENT = "webhook.test";
|
|
@@ -35523,7 +36599,7 @@ const redeliver$1 = async (runtime, context, webhookId, deliveryId) => {
|
|
|
35523
36599
|
//#region src/controllers/webhooks.ts
|
|
35524
36600
|
const contextFor = (request) => ({
|
|
35525
36601
|
...toSiteActorContext(request),
|
|
35526
|
-
permissions: request
|
|
36602
|
+
permissions: getRequestPermissions(request)
|
|
35527
36603
|
});
|
|
35528
36604
|
const listWebhooks$1 = async (request) => listWebhooks$2(request.server.publishing, getRequestSite(request));
|
|
35529
36605
|
const listEventTypes = async () => ({ items: [...listEventTypes$1()] });
|
|
@@ -37825,7 +38901,7 @@ const openStoredBundle = (storage, stored) => storage.get(stored.driver).getStre
|
|
|
37825
38901
|
const removeStoredBundle = (storage, stored) => storage.get(stored.driver).delete(stored.key);
|
|
37826
38902
|
//#endregion
|
|
37827
38903
|
//#region src/constants/version.ts
|
|
37828
|
-
const SHAPIO_VERSION = "0.
|
|
38904
|
+
const SHAPIO_VERSION = "0.4.0";
|
|
37829
38905
|
//#endregion
|
|
37830
38906
|
//#region src/repositories/transferExport.ts
|
|
37831
38907
|
/**
|
|
@@ -38448,7 +39524,7 @@ const importUserBatch = async (db, siteId, users) => {
|
|
|
38448
39524
|
...account
|
|
38449
39525
|
}, trx);
|
|
38450
39526
|
}
|
|
38451
|
-
await bumpPermissionsVersion(trx);
|
|
39527
|
+
await bumpPermissionsVersion$2(trx);
|
|
38452
39528
|
});
|
|
38453
39529
|
result.added = added.length;
|
|
38454
39530
|
return result;
|
|
@@ -38885,7 +39961,7 @@ const writeMediaFile = async (context, assetId, input, upload) => {
|
|
|
38885
39961
|
const transferContextFor = (request) => ({
|
|
38886
39962
|
...toSiteActorContext(request),
|
|
38887
39963
|
db: request.server.db,
|
|
38888
|
-
permissions: request
|
|
39964
|
+
permissions: getRequestPermissions(request),
|
|
38889
39965
|
storage: request.server.mediaStorage,
|
|
38890
39966
|
schemaContext: async () => ({
|
|
38891
39967
|
db: request.server.db,
|
|
@@ -39991,15 +41067,20 @@ const sendCacheable = (request, reply, payload) => {
|
|
|
39991
41067
|
if (typeof match === "string" && match.split(",").some((candidate) => candidate.trim() === etag)) return reply.code(304).send();
|
|
39992
41068
|
return reply.type("application/json; charset=utf-8").send(body);
|
|
39993
41069
|
};
|
|
41070
|
+
/** Field usage is counted after the response is sent, off its path (plan delivery-perf). */
|
|
41071
|
+
const usageOptions = (request) => ({
|
|
41072
|
+
usage: request.server.usage,
|
|
41073
|
+
defer: (task) => request.server.recordUsageAfterResponse(request.log, task)
|
|
41074
|
+
});
|
|
39994
41075
|
const listDelivery = async (request, reply) => {
|
|
39995
41076
|
const context = await contentContextFor(request);
|
|
39996
41077
|
const apiKey = apiKeyOfRoute(context.snapshot, request.params.modelKey);
|
|
39997
|
-
return sendCacheable(request, reply, await listDelivery$1(context, apiKey, rawQueryOf(request),
|
|
41078
|
+
return sendCacheable(request, reply, await listDelivery$1(context, apiKey, rawQueryOf(request), usageOptions(request)));
|
|
39998
41079
|
};
|
|
39999
41080
|
const getDelivery = async (request, reply) => {
|
|
40000
41081
|
const context = await contentContextFor(request);
|
|
40001
41082
|
const apiKey = apiKeyOfRoute(context.snapshot, request.params.modelKey);
|
|
40002
|
-
return sendCacheable(request, reply, await getDelivery$1(context, apiKey, request.params.id, rawQueryOf(request),
|
|
41083
|
+
return sendCacheable(request, reply, await getDelivery$1(context, apiKey, request.params.id, rawQueryOf(request), usageOptions(request)));
|
|
40003
41084
|
};
|
|
40004
41085
|
//#endregion
|
|
40005
41086
|
//#region src/controllers/deliveryWrites.ts
|
|
@@ -40180,6 +41261,50 @@ ${graphql ? renderGraphqlSection(graphql) : ""}
|
|
|
40180
41261
|
</main></body></html>`;
|
|
40181
41262
|
};
|
|
40182
41263
|
//#endregion
|
|
41264
|
+
//#region src/routes/site/schemas.ts
|
|
41265
|
+
/** The default social image as delivery shows any asset (the media field shape). */
|
|
41266
|
+
const DeliveredImageSchema = Type.Record(Type.String(), Type.Unknown());
|
|
41267
|
+
const getDeliverySiteSchema = { response: {
|
|
41268
|
+
200: Type.Object({
|
|
41269
|
+
key: Type.String(),
|
|
41270
|
+
name: Type.String(),
|
|
41271
|
+
seo: Type.Object({
|
|
41272
|
+
locales: Type.Record(Type.String(), SeoLocaleDefaultsSchema),
|
|
41273
|
+
twitterHandle: Type.Union([Type.String({ pattern: SEO_TWITTER_HANDLE_PATTERN.source }), Type.Null()]),
|
|
41274
|
+
image: Type.Union([DeliveredImageSchema, Type.Null()])
|
|
41275
|
+
})
|
|
41276
|
+
}),
|
|
41277
|
+
304: { type: "null" },
|
|
41278
|
+
401: ErrorResponseSchema,
|
|
41279
|
+
403: ErrorResponseSchema
|
|
41280
|
+
} };
|
|
41281
|
+
//#endregion
|
|
41282
|
+
//#region src/schema/codegen/openapiSite.ts
|
|
41283
|
+
/**
|
|
41284
|
+
* `GET /api/site` in the OpenAPI document (plan seo-fields): the site's key, name and SEO defaults. Its schema
|
|
41285
|
+
* is the route's own TypeBox schema, never a second copy.
|
|
41286
|
+
*/
|
|
41287
|
+
const SITE_TAG = {
|
|
41288
|
+
name: "Site",
|
|
41289
|
+
description: "The request's site and its SEO defaults"
|
|
41290
|
+
};
|
|
41291
|
+
const errorRef$1 = { $ref: "#/components/schemas/Error" };
|
|
41292
|
+
const sitePaths = () => ({ "/api/site": { get: {
|
|
41293
|
+
tags: [SITE_TAG.name],
|
|
41294
|
+
summary: "The site's key, name and SEO defaults (texts per locale, default image, Twitter handle)",
|
|
41295
|
+
operationId: "getSite",
|
|
41296
|
+
responses: {
|
|
41297
|
+
"200": {
|
|
41298
|
+
description: "OK",
|
|
41299
|
+
content: { "application/json": { schema: getDeliverySiteSchema.response[200] } }
|
|
41300
|
+
},
|
|
41301
|
+
"401": {
|
|
41302
|
+
description: "Anonymous, and the public role may read nothing on this site",
|
|
41303
|
+
content: { "application/json": { schema: errorRef$1 } }
|
|
41304
|
+
}
|
|
41305
|
+
}
|
|
41306
|
+
} } });
|
|
41307
|
+
//#endregion
|
|
40183
41308
|
//#region src/routes/snapshots/schemas.ts
|
|
40184
41309
|
const SeqSchema = Type.Integer({ minimum: 0 });
|
|
40185
41310
|
const NullableInteger = Type.Union([Type.Integer(), Type.Null()]);
|
|
@@ -40448,13 +41573,20 @@ const COMMON_SCHEMAS = {
|
|
|
40448
41573
|
"doc"
|
|
40449
41574
|
]
|
|
40450
41575
|
},
|
|
40451
|
-
RichTextOutput: {
|
|
41576
|
+
RichTextOutput: {
|
|
40452
41577
|
type: "object",
|
|
40453
|
-
|
|
40454
|
-
|
|
40455
|
-
|
|
40456
|
-
|
|
40457
|
-
|
|
41578
|
+
description: "Rich text as delivered, per the `richText` query parameter: `doc` with `json` (the default), `html` with `html`, both with `both`.",
|
|
41579
|
+
properties: {
|
|
41580
|
+
format: { const: "shapio-richtext" },
|
|
41581
|
+
version: { const: 1 },
|
|
41582
|
+
doc: { type: "object" },
|
|
41583
|
+
html: {
|
|
41584
|
+
type: "string",
|
|
41585
|
+
description: "Sanitized HTML rendered from the JSON"
|
|
41586
|
+
}
|
|
41587
|
+
},
|
|
41588
|
+
required: ["format", "version"]
|
|
41589
|
+
},
|
|
40458
41590
|
MediaAsset: {
|
|
40459
41591
|
type: "object",
|
|
40460
41592
|
properties: {
|
|
@@ -40564,9 +41696,20 @@ const listParameters = (model, delivery) => [
|
|
|
40564
41696
|
schema: { type: "string" },
|
|
40565
41697
|
description: "Search the title field"
|
|
40566
41698
|
},
|
|
40567
|
-
...itemParameters(delivery)
|
|
41699
|
+
...itemParameters(model, delivery)
|
|
40568
41700
|
];
|
|
40569
|
-
|
|
41701
|
+
/** `?seo=` is offered where it works: delivery reads of a model with an SEO field. */
|
|
41702
|
+
const seoParameters = (model) => model.fields.some((field) => !field.deprecated && isSeoField(field)) ? [{
|
|
41703
|
+
name: "seo",
|
|
41704
|
+
in: "query",
|
|
41705
|
+
schema: {
|
|
41706
|
+
type: "string",
|
|
41707
|
+
enum: [...SEO_MODES],
|
|
41708
|
+
default: "raw"
|
|
41709
|
+
},
|
|
41710
|
+
description: "SEO fields as stored (`raw`) or merged with the site's SEO defaults (`resolved`): the title through the site's title template (the entry's own title when empty), description and image from the defaults, `noindex` a boolean. A pinned `snapshot` uses today's defaults."
|
|
41711
|
+
}] : [];
|
|
41712
|
+
const itemParameters = (model, delivery) => [
|
|
40570
41713
|
{
|
|
40571
41714
|
name: "fields",
|
|
40572
41715
|
in: "query",
|
|
@@ -40585,15 +41728,28 @@ const itemParameters = (delivery) => [
|
|
|
40585
41728
|
schema: { type: "string" },
|
|
40586
41729
|
description: "Locale to serve; falls back along the locale chain"
|
|
40587
41730
|
},
|
|
40588
|
-
...delivery ? [
|
|
40589
|
-
|
|
40590
|
-
|
|
40591
|
-
|
|
40592
|
-
|
|
40593
|
-
|
|
41731
|
+
...delivery ? [
|
|
41732
|
+
{
|
|
41733
|
+
name: "snapshot",
|
|
41734
|
+
in: "query",
|
|
41735
|
+
schema: {
|
|
41736
|
+
type: "integer",
|
|
41737
|
+
minimum: 1
|
|
41738
|
+
},
|
|
41739
|
+
description: "Read content as of this publication sequence number (`meta.snapshot`)"
|
|
41740
|
+
},
|
|
41741
|
+
{
|
|
41742
|
+
name: "richText",
|
|
41743
|
+
in: "query",
|
|
41744
|
+
schema: {
|
|
41745
|
+
type: "string",
|
|
41746
|
+
enum: [...RICH_TEXT_MODES],
|
|
41747
|
+
default: DEFAULT_RICH_TEXT_MODE
|
|
41748
|
+
},
|
|
41749
|
+
description: "Rich-text shape: the JSON document, sanitized HTML rendered from it, or both"
|
|
40594
41750
|
},
|
|
40595
|
-
|
|
40596
|
-
|
|
41751
|
+
...seoParameters(model)
|
|
41752
|
+
] : []
|
|
40597
41753
|
];
|
|
40598
41754
|
const idParameter = {
|
|
40599
41755
|
name: "id",
|
|
@@ -40647,7 +41803,7 @@ const modelPaths = (model) => {
|
|
|
40647
41803
|
tags: [tag],
|
|
40648
41804
|
summary: single ? `Read the published ${model.label}` : `List published ${model.label} entries`,
|
|
40649
41805
|
operationId: `list${name}`,
|
|
40650
|
-
parameters: single ? itemParameters(true) : listParameters(model, true),
|
|
41806
|
+
parameters: single ? itemParameters(model, true) : listParameters(model, true),
|
|
40651
41807
|
responses: {
|
|
40652
41808
|
"200": json({
|
|
40653
41809
|
type: "object",
|
|
@@ -40670,7 +41826,7 @@ const modelPaths = (model) => {
|
|
|
40670
41826
|
tags: [tag],
|
|
40671
41827
|
summary: `Read one published ${model.label} entry`,
|
|
40672
41828
|
operationId: `get${name}`,
|
|
40673
|
-
parameters: [idParameter, ...itemParameters(true)],
|
|
41829
|
+
parameters: [idParameter, ...itemParameters(model, true)],
|
|
40674
41830
|
responses: {
|
|
40675
41831
|
"200": json({
|
|
40676
41832
|
type: "object",
|
|
@@ -40834,11 +41990,15 @@ const generateOpenApi = (snapshot, options) => {
|
|
|
40834
41990
|
},
|
|
40835
41991
|
servers: [{ url: options.serverUrl }],
|
|
40836
41992
|
security: [{ bearerAuth: [] }, { cookieAuth: [] }],
|
|
40837
|
-
tags: [
|
|
40838
|
-
|
|
40839
|
-
|
|
40840
|
-
|
|
40841
|
-
|
|
41993
|
+
tags: [
|
|
41994
|
+
...models.map((model) => ({
|
|
41995
|
+
name: model.label,
|
|
41996
|
+
description: model.description ?? `${model.kind} \`${model.apiKey}\``
|
|
41997
|
+
})),
|
|
41998
|
+
SNAPSHOTS_TAG,
|
|
41999
|
+
SITE_TAG
|
|
42000
|
+
],
|
|
42001
|
+
paths: Object.assign({}, ...models.map(modelPaths), snapshotPaths(), sitePaths()),
|
|
40842
42002
|
components: {
|
|
40843
42003
|
schemas,
|
|
40844
42004
|
securitySchemes: {
|
|
@@ -40872,8 +42032,11 @@ export type RichTextNode = {
|
|
|
40872
42032
|
|
|
40873
42033
|
export type RichText = { format: 'shapio-richtext'; version: 1; doc: RichTextNode };
|
|
40874
42034
|
|
|
40875
|
-
/**
|
|
40876
|
-
|
|
42035
|
+
/**
|
|
42036
|
+
* Rich text as delivery returns it, per the request's \`richText\`: \`doc\` with \`json\` (the default), sanitized
|
|
42037
|
+
* \`html\` rendered from the document with \`html\`, both with \`both\`.
|
|
42038
|
+
*/
|
|
42039
|
+
export type RichTextOutput = { format: 'shapio-richtext'; version: 1; doc?: RichTextNode; html?: string };
|
|
40877
42040
|
|
|
40878
42041
|
export type MediaAsset = {
|
|
40879
42042
|
id: string;
|
|
@@ -41032,14 +42195,14 @@ const docsRoutes = async (app) => {
|
|
|
41032
42195
|
//#endregion
|
|
41033
42196
|
//#region src/db/migrations/20261001120000_create_platform_tables.ts
|
|
41034
42197
|
var _20261001120000_create_platform_tables_exports = /* @__PURE__ */ __exportAll({
|
|
41035
|
-
down: () => down$
|
|
41036
|
-
up: () => up$
|
|
42198
|
+
down: () => down$30,
|
|
42199
|
+
up: () => up$30
|
|
41037
42200
|
});
|
|
41038
42201
|
/**
|
|
41039
42202
|
* Foundation tables shared by every package: durable versions, the job queue, the transactional outbox
|
|
41040
42203
|
* and the audit log. Statuses are text with CHECK constraints so new states need only a constraint change.
|
|
41041
42204
|
*/
|
|
41042
|
-
const up$
|
|
42205
|
+
const up$30 = async (db) => {
|
|
41043
42206
|
await db.schema.createTable("system_versions").addColumn("id", "boolean", (col) => col.primaryKey().defaultTo(true).check(sql`id`)).addColumn("schema_version", "integer", (col) => col.notNull().defaultTo(0)).addColumn("permissions_version", "integer", (col) => col.notNull().defaultTo(0)).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).execute();
|
|
41044
42207
|
await sql`insert into system_versions default values`.execute(db);
|
|
41045
42208
|
await db.schema.createTable("jobs").addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`)).addColumn("type", "text", (col) => col.notNull()).addColumn("payload", "jsonb", (col) => col.notNull().defaultTo(sql`'{}'::jsonb`)).addColumn("status", "text", (col) => col.notNull().defaultTo("pending").check(sql`status in ('pending', 'running', 'succeeded', 'dead')`)).addColumn("priority", "integer", (col) => col.notNull().defaultTo(0)).addColumn("run_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("attempts", "integer", (col) => col.notNull().defaultTo(0)).addColumn("max_attempts", "integer", (col) => col.notNull().defaultTo(10).check(sql`max_attempts > 0`)).addColumn("idempotency_key", "text").addColumn("locked_by", "text").addColumn("locked_until", "timestamptz").addColumn("checkpoint", "jsonb").addColumn("result", "jsonb").addColumn("last_error", "text").addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("finished_at", "timestamptz").execute();
|
|
@@ -41061,7 +42224,7 @@ const up$27 = async (db) => {
|
|
|
41061
42224
|
"occurred_at desc"
|
|
41062
42225
|
]).execute();
|
|
41063
42226
|
};
|
|
41064
|
-
const down$
|
|
42227
|
+
const down$30 = async (db) => {
|
|
41065
42228
|
await db.schema.dropTable("audit_events").execute();
|
|
41066
42229
|
await db.schema.dropTable("outbox_events").execute();
|
|
41067
42230
|
await db.schema.dropTable("jobs").execute();
|
|
@@ -41070,8 +42233,8 @@ const down$27 = async (db) => {
|
|
|
41070
42233
|
//#endregion
|
|
41071
42234
|
//#region src/db/migrations/20261001130000_create_admin_identity.ts
|
|
41072
42235
|
var _20261001130000_create_admin_identity_exports = /* @__PURE__ */ __exportAll({
|
|
41073
|
-
down: () => down$
|
|
41074
|
-
up: () => up$
|
|
42236
|
+
down: () => down$29,
|
|
42237
|
+
up: () => up$29
|
|
41075
42238
|
});
|
|
41076
42239
|
/**
|
|
41077
42240
|
* Admin identity (package B, ADR 0005): admin users, roles and their permission grants, server-side
|
|
@@ -41080,7 +42243,7 @@ var _20261001130000_create_admin_identity_exports = /* @__PURE__ */ __exportAll(
|
|
|
41080
42243
|
*/
|
|
41081
42244
|
const withTimestamps$3 = (table) => table.addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`));
|
|
41082
42245
|
const uuidPrimaryKey$3 = (table) => table.addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`));
|
|
41083
|
-
const up$
|
|
42246
|
+
const up$29 = async (db) => {
|
|
41084
42247
|
await withTimestamps$3(uuidPrimaryKey$3(db.schema.createTable("admin_users")).addColumn("email", "text", (col) => col.notNull().unique()).addColumn("name", "text", (col) => col.notNull().defaultTo("")).addColumn("password_hash", "text", (col) => col.notNull()).addColumn("status", "text", (col) => col.notNull().defaultTo("active").check(sql`status in ('active', 'disabled')`)).addColumn("last_login_at", "timestamptz").addColumn("password_changed_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`))).execute();
|
|
41085
42248
|
await withTimestamps$3(uuidPrimaryKey$3(db.schema.createTable("admin_roles")).addColumn("key", "text", (col) => col.notNull().unique().check(sql`key ~ '^[a-z][a-z0-9-]{0,62}$'`)).addColumn("name", "text", (col) => col.notNull()).addColumn("description", "text", (col) => col.notNull().defaultTo("")).addColumn("kind", "text", (col) => col.notNull().defaultTo("admin").check(sql`kind in ('admin', 'delivery')`)).addColumn("is_system", "boolean", (col) => col.notNull().defaultTo(false)).addColumn("version", "integer", (col) => col.notNull().defaultTo(1))).execute();
|
|
41086
42249
|
await withTimestamps$3(uuidPrimaryKey$3(db.schema.createTable("admin_role_permissions")).addColumn("role_id", "uuid", (col) => col.notNull().references("admin_roles.id").onDelete("cascade")).addColumn("action", "text", (col) => col.notNull()).addColumn("model_id", "text").addColumn("condition", "text", (col) => col.check(sql`condition in ('ownedByPrincipal')`)).addColumn("field_ids", sql`text[]`).addUniqueConstraint("admin_role_permissions_grant_uq", [
|
|
@@ -41100,7 +42263,7 @@ const up$26 = async (db) => {
|
|
|
41100
42263
|
await withTimestamps$3(uuidPrimaryKey$3(db.schema.createTable("api_tokens")).addColumn("name", "text", (col) => col.notNull()).addColumn("token_hash", "text", (col) => col.notNull().unique()).addColumn("token_prefix", "text", (col) => col.notNull()).addColumn("role_id", "uuid", (col) => col.notNull().references("admin_roles.id").onDelete("restrict")).addColumn("created_by", "uuid", (col) => col.references("admin_users.id").onDelete("set null")).addColumn("expires_at", "timestamptz").addColumn("last_used_at", "timestamptz").addColumn("revoked_at", "timestamptz")).execute();
|
|
41101
42264
|
await db.schema.createIndex("api_tokens_role_idx").on("api_tokens").column("role_id").execute();
|
|
41102
42265
|
};
|
|
41103
|
-
const down$
|
|
42266
|
+
const down$29 = async (db) => {
|
|
41104
42267
|
await db.schema.dropTable("api_tokens").execute();
|
|
41105
42268
|
await db.schema.dropTable("setup_tokens").execute();
|
|
41106
42269
|
await db.schema.dropTable("password_resets").execute();
|
|
@@ -41114,31 +42277,31 @@ const down$26 = async (db) => {
|
|
|
41114
42277
|
//#endregion
|
|
41115
42278
|
//#region src/db/migrations/20261001130500_create_system_settings.ts
|
|
41116
42279
|
var _20261001130500_create_system_settings_exports = /* @__PURE__ */ __exportAll({
|
|
41117
|
-
down: () => down$
|
|
41118
|
-
up: () => up$
|
|
42280
|
+
down: () => down$28,
|
|
42281
|
+
up: () => up$28
|
|
41119
42282
|
});
|
|
41120
42283
|
/**
|
|
41121
42284
|
* Instance-wide key/value settings that must survive restarts and be shared by every instance, e.g. the
|
|
41122
42285
|
* generated signing secret. General purpose: later packages add keys, not tables.
|
|
41123
42286
|
*/
|
|
41124
|
-
const up$
|
|
42287
|
+
const up$28 = async (db) => {
|
|
41125
42288
|
await db.schema.createTable("system_settings").addColumn("key", "text", (col) => col.primaryKey()).addColumn("value", "text", (col) => col.notNull()).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).execute();
|
|
41126
42289
|
};
|
|
41127
|
-
const down$
|
|
42290
|
+
const down$28 = async (db) => {
|
|
41128
42291
|
await db.schema.dropTable("system_settings").execute();
|
|
41129
42292
|
};
|
|
41130
42293
|
//#endregion
|
|
41131
42294
|
//#region src/db/migrations/20261001140000_create_schema_tables.ts
|
|
41132
42295
|
var _20261001140000_create_schema_tables_exports = /* @__PURE__ */ __exportAll({
|
|
41133
|
-
down: () => down$
|
|
41134
|
-
up: () => up$
|
|
42296
|
+
down: () => down$27,
|
|
42297
|
+
up: () => up$27
|
|
41135
42298
|
});
|
|
41136
42299
|
/**
|
|
41137
42300
|
* Schema registry (package D, ADR 0002): models and components (one table, `kind` discriminator), immutable
|
|
41138
42301
|
* schema revisions, the per-model active pointer, schema change jobs, locales and schema settings.
|
|
41139
42302
|
* Model definitions are data: creating or changing a model never runs DDL (CONTRIBUTING.md rule 1).
|
|
41140
42303
|
*/
|
|
41141
|
-
const up$
|
|
42304
|
+
const up$27 = async (db) => {
|
|
41142
42305
|
await db.schema.createTable("models").addColumn("id", "uuid", (col) => col.primaryKey()).addColumn("kind", "text", (col) => col.notNull().check(sql`kind in ('collection', 'singleton', 'component')`)).addColumn("api_key", "text", (col) => col.notNull()).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("deleted_at", "timestamptz").execute();
|
|
41143
42306
|
await sql`create unique index models_api_key_uq on models (lower(api_key)) where deleted_at is null`.execute(db);
|
|
41144
42307
|
await db.schema.createTable("schema_revisions").addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`)).addColumn("model_id", "uuid", (col) => col.notNull().references("models.id").onDelete("restrict")).addColumn("version", "integer", (col) => col.notNull().check(sql`version > 0`)).addColumn("definition", "jsonb", (col) => col.notNull()).addColumn("hash", "text", (col) => col.notNull()).addColumn("parent_revision_id", "uuid", (col) => col.references("schema_revisions.id")).addColumn("created_by_type", "text", (col) => col.notNull()).addColumn("created_by_id", "text").addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addUniqueConstraint("schema_revisions_model_version_uq", ["model_id", "version"]).execute();
|
|
@@ -41166,7 +42329,7 @@ const up$24 = async (db) => {
|
|
|
41166
42329
|
await db.schema.createTable("schema_settings").addColumn("id", "boolean", (col) => col.primaryKey().defaultTo(true).check(sql`id`)).addColumn("read_only", "boolean", (col) => col.notNull().defaultTo(false)).addColumn("read_only_reason", "text").addColumn("updated_by_type", "text").addColumn("updated_by_id", "text").addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).execute();
|
|
41167
42330
|
await sql`insert into schema_settings default values`.execute(db);
|
|
41168
42331
|
};
|
|
41169
|
-
const down$
|
|
42332
|
+
const down$27 = async (db) => {
|
|
41170
42333
|
await db.schema.dropTable("schema_settings").execute();
|
|
41171
42334
|
await db.schema.dropTable("locales").execute();
|
|
41172
42335
|
await db.schema.dropTable("schema_change_jobs").execute();
|
|
@@ -41178,8 +42341,8 @@ const down$24 = async (db) => {
|
|
|
41178
42341
|
//#endregion
|
|
41179
42342
|
//#region src/db/migrations/20261001150000_create_content_tables.ts
|
|
41180
42343
|
var _20261001150000_create_content_tables_exports = /* @__PURE__ */ __exportAll({
|
|
41181
|
-
down: () => down$
|
|
41182
|
-
up: () => up$
|
|
42344
|
+
down: () => down$26,
|
|
42345
|
+
up: () => up$26
|
|
41183
42346
|
});
|
|
41184
42347
|
/**
|
|
41185
42348
|
* Content storage (package E, ADR 0001): entries, immutable content revisions, mutable heads (one per
|
|
@@ -41187,7 +42350,7 @@ var _20261001150000_create_content_tables_exports = /* @__PURE__ */ __exportAll(
|
|
|
41187
42350
|
* and the publication log behind `?snapshot=N`. User content models never get tables of their own
|
|
41188
42351
|
* (CONTRIBUTING.md rule 1): their values are JSONB keyed by stable field IDs.
|
|
41189
42352
|
*/
|
|
41190
|
-
const up$
|
|
42353
|
+
const up$26 = async (db) => {
|
|
41191
42354
|
await db.schema.createTable("entries").addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`)).addColumn("model_id", "uuid", (col) => col.notNull().references("models.id").onDelete("restrict")).addColumn("owner_app_user_id", "uuid").addColumn("created_by_admin_id", "uuid", (col) => col.references("admin_users.id").onDelete("set null")).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("deleted_at", "timestamptz").execute();
|
|
41192
42355
|
await sql`create index entries_model_idx on entries (model_id, created_at) where deleted_at is null`.execute(db);
|
|
41193
42356
|
await sql`create index entries_owner_idx on entries (owner_app_user_id) where owner_app_user_id is not null`.execute(db);
|
|
@@ -41251,7 +42414,7 @@ const up$23 = async (db) => {
|
|
|
41251
42414
|
await sql`create unique index publication_log_open_uq on publication_log (entry_id, locale) where to_seq is null`.execute(db);
|
|
41252
42415
|
await db.schema.createIndex("publication_log_model_seq_idx").on("publication_log").columns(["model_id", "from_seq"]).execute();
|
|
41253
42416
|
};
|
|
41254
|
-
const down$
|
|
42417
|
+
const down$26 = async (db) => {
|
|
41255
42418
|
await db.schema.dropTable("publication_log").execute();
|
|
41256
42419
|
await db.schema.dropTable("publication_state").execute();
|
|
41257
42420
|
await db.schema.dropTable("unique_values").execute();
|
|
@@ -41264,8 +42427,8 @@ const down$23 = async (db) => {
|
|
|
41264
42427
|
//#endregion
|
|
41265
42428
|
//#region src/db/migrations/20261001160000_create_media_tables.ts
|
|
41266
42429
|
var _20261001160000_create_media_tables_exports = /* @__PURE__ */ __exportAll({
|
|
41267
|
-
down: () => down$
|
|
41268
|
-
up: () => up$
|
|
42430
|
+
down: () => down$25,
|
|
42431
|
+
up: () => up$25
|
|
41269
42432
|
});
|
|
41270
42433
|
/**
|
|
41271
42434
|
* Media library (package G): folders, assets, image variants, references from content and upload grants.
|
|
@@ -41276,7 +42439,7 @@ const withTimestamps$2 = (table) => table.addColumn("created_at", "timestamptz",
|
|
|
41276
42439
|
const uuidPrimaryKey$2 = (table) => table.addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`));
|
|
41277
42440
|
const STORAGE_DRIVER_CHECK = sql`storage_driver in ('local', 's3')`;
|
|
41278
42441
|
const VISIBILITY_CHECK = sql`visibility in ('public', 'private')`;
|
|
41279
|
-
const up$
|
|
42442
|
+
const up$25 = async (db) => {
|
|
41280
42443
|
await withTimestamps$2(uuidPrimaryKey$2(db.schema.createTable("media_folders")).addColumn("parent_id", "uuid", (col) => col.references("media_folders.id").onDelete("restrict")).addColumn("name", "text", (col) => col.notNull().check(sql`length(name) between 1 and 255`)).addColumn("created_by", "uuid", (col) => col.references("admin_users.id").onDelete("set null")).addColumn("version", "integer", (col) => col.notNull().defaultTo(1))).execute();
|
|
41281
42444
|
await sql`create unique index media_folders_sibling_name_uq on media_folders
|
|
41282
42445
|
(coalesce(parent_id, '00000000-0000-0000-0000-000000000000'::uuid), lower(name))`.execute(db);
|
|
@@ -41305,7 +42468,7 @@ const up$22 = async (db) => {
|
|
|
41305
42468
|
]).execute();
|
|
41306
42469
|
await db.schema.createTable("media_upload_grants").addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`)).addColumn("asset_id", "uuid", (col) => col.notNull()).addColumn("kind", "text", (col) => col.notNull().check(sql`kind in ('create', 'replace')`)).addColumn("storage_driver", "text", (col) => col.notNull().check(STORAGE_DRIVER_CHECK)).addColumn("storage_key", "text", (col) => col.notNull().unique()).addColumn("original_filename", "text", (col) => col.notNull()).addColumn("declared_mime_type", "text", (col) => col.notNull()).addColumn("max_size_bytes", "bigint", (col) => col.notNull()).addColumn("folder_id", "uuid").addColumn("visibility", "text", (col) => col.notNull().check(VISIBILITY_CHECK)).addColumn("created_by", "uuid", (col) => col.references("admin_users.id").onDelete("set null")).addColumn("created_by_principal", "text", (col) => col.notNull()).addColumn("status", "text", (col) => col.notNull().defaultTo("pending").check(sql`status in ('pending', 'consumed', 'rejected', 'expired')`)).addColumn("expires_at", "timestamptz", (col) => col.notNull()).addColumn("consumed_at", "timestamptz").addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).execute();
|
|
41307
42470
|
};
|
|
41308
|
-
const down$
|
|
42471
|
+
const down$25 = async (db) => {
|
|
41309
42472
|
await db.schema.dropTable("media_upload_grants").execute();
|
|
41310
42473
|
await db.schema.dropTable("media_references").execute();
|
|
41311
42474
|
await db.schema.dropTable("media_variants").execute();
|
|
@@ -41315,8 +42478,8 @@ const down$22 = async (db) => {
|
|
|
41315
42478
|
//#endregion
|
|
41316
42479
|
//#region src/db/migrations/20261001170000_create_publishing_tables.ts
|
|
41317
42480
|
var _20261001170000_create_publishing_tables_exports = /* @__PURE__ */ __exportAll({
|
|
41318
|
-
down: () => down$
|
|
41319
|
-
up: () => up$
|
|
42481
|
+
down: () => down$24,
|
|
42482
|
+
up: () => up$24
|
|
41320
42483
|
});
|
|
41321
42484
|
/**
|
|
41322
42485
|
* Publishing (package H): scheduled publications, releases, webhooks and their delivery log, deployment
|
|
@@ -41355,13 +42518,13 @@ const createPreviewTables = async (db) => {
|
|
|
41355
42518
|
await uuidPrimaryKey$1(db.schema.createTable("preview_tokens")).addColumn("token_hash", "text", (col) => col.notNull().unique()).addColumn("token_prefix", "text", (col) => col.notNull()).addColumn("model_id", "uuid", (col) => col.notNull()).addColumn("entry_id", "uuid", (col) => col.references("entries.id").onDelete("cascade")).addColumn("locale", "text").addColumn("connection_id", "uuid", (col) => col.references("deployment_connections.id").onDelete("set null")).addColumn("delivery_role_id", "uuid", (col) => col.references("admin_roles.id").onDelete("set null")).addColumn("created_by", "uuid", (col) => col.notNull().references("admin_users.id").onDelete("cascade")).addColumn("expires_at", "timestamptz", (col) => col.notNull()).addColumn("revoked_at", "timestamptz").addColumn("last_used_at", "timestamptz").addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).execute();
|
|
41356
42519
|
await db.schema.createIndex("preview_tokens_entry_idx").on("preview_tokens").columns(["entry_id", "created_at desc"]).execute();
|
|
41357
42520
|
};
|
|
41358
|
-
const up$
|
|
42521
|
+
const up$24 = async (db) => {
|
|
41359
42522
|
await createSchedulingTables(db);
|
|
41360
42523
|
await createWebhookTables(db);
|
|
41361
42524
|
await createDeploymentTables(db);
|
|
41362
42525
|
await createPreviewTables(db);
|
|
41363
42526
|
};
|
|
41364
|
-
const down$
|
|
42527
|
+
const down$24 = async (db) => {
|
|
41365
42528
|
await db.schema.dropTable("preview_tokens").execute();
|
|
41366
42529
|
await db.schema.dropTable("deployment_runs").execute();
|
|
41367
42530
|
await db.schema.dropTable("deployment_connections").execute();
|
|
@@ -41374,8 +42537,8 @@ const down$21 = async (db) => {
|
|
|
41374
42537
|
//#endregion
|
|
41375
42538
|
//#region src/db/migrations/20261001180000_create_app_users.ts
|
|
41376
42539
|
var _20261001180000_create_app_users_exports = /* @__PURE__ */ __exportAll({
|
|
41377
|
-
down: () => down$
|
|
41378
|
-
up: () => up$
|
|
42540
|
+
down: () => down$23,
|
|
42541
|
+
up: () => up$23
|
|
41379
42542
|
});
|
|
41380
42543
|
/**
|
|
41381
42544
|
* App users (package I, ADR 0005): the end users of sites and apps built on Shapio, with their own roles
|
|
@@ -41426,7 +42589,7 @@ const createCredentials = async (db) => {
|
|
|
41426
42589
|
}
|
|
41427
42590
|
await db.schema.createTable("app_login_codes").addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`)).addColumn("app_user_id", "uuid", (col) => col.notNull().references("app_users.id").onDelete("cascade")).addColumn("code_hash", "text", (col) => col.notNull().unique()).addColumn("expires_at", "timestamptz", (col) => col.notNull()).addColumn("used_at", "timestamptz").addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).execute();
|
|
41428
42591
|
};
|
|
41429
|
-
const up$
|
|
42592
|
+
const up$23 = async (db) => {
|
|
41430
42593
|
await createUsers(db);
|
|
41431
42594
|
await createRoles(db);
|
|
41432
42595
|
await createCredentials(db);
|
|
@@ -41436,7 +42599,7 @@ const up$20 = async (db) => {
|
|
|
41436
42599
|
foreign key (owner_app_user_id) references app_users (id) on delete set null
|
|
41437
42600
|
`.execute(db);
|
|
41438
42601
|
};
|
|
41439
|
-
const down$
|
|
42602
|
+
const down$23 = async (db) => {
|
|
41440
42603
|
await sql`alter table entries drop constraint if exists entries_owner_app_user_fk`.execute(db);
|
|
41441
42604
|
await db.schema.dropTable("app_login_codes").execute();
|
|
41442
42605
|
await db.schema.dropTable("app_password_resets").execute();
|
|
@@ -41451,8 +42614,8 @@ const down$20 = async (db) => {
|
|
|
41451
42614
|
//#endregion
|
|
41452
42615
|
//#region src/db/migrations/20261002120000_create_extension_hook_runs.ts
|
|
41453
42616
|
var _20261002120000_create_extension_hook_runs_exports = /* @__PURE__ */ __exportAll({
|
|
41454
|
-
down: () => down$
|
|
41455
|
-
up: () => up$
|
|
42617
|
+
down: () => down$22,
|
|
42618
|
+
up: () => up$22
|
|
41456
42619
|
});
|
|
41457
42620
|
/**
|
|
41458
42621
|
* Extension points (package K, ADR 0009): the ledger of post-commit lifecycle hooks (`afterCreate`,
|
|
@@ -41460,17 +42623,17 @@ var _20261002120000_create_extension_hook_runs_exports = /* @__PURE__ */ __expor
|
|
|
41460
42623
|
* as the hook's own database work, so a hook re-run after a worker crash finds its row and is skipped:
|
|
41461
42624
|
* exactly once for everything the hook does through its transaction.
|
|
41462
42625
|
*/
|
|
41463
|
-
const up$
|
|
42626
|
+
const up$22 = async (db) => {
|
|
41464
42627
|
await db.schema.createTable("extension_hook_runs").addColumn("event_id", "uuid", (col) => col.notNull()).addColumn("hook", "text", (col) => col.notNull()).addColumn("job_id", "uuid").addColumn("completed_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addPrimaryKeyConstraint("extension_hook_runs_pkey", ["event_id", "hook"]).execute();
|
|
41465
42628
|
};
|
|
41466
|
-
const down$
|
|
42629
|
+
const down$22 = async (db) => {
|
|
41467
42630
|
await db.schema.dropTable("extension_hook_runs").execute();
|
|
41468
42631
|
};
|
|
41469
42632
|
//#endregion
|
|
41470
42633
|
//#region src/db/migrations/20261002130000_app_auth_token_version_and_login_code_pkce.ts
|
|
41471
42634
|
var _20261002130000_app_auth_token_version_and_login_code_pkce_exports = /* @__PURE__ */ __exportAll({
|
|
41472
|
-
down: () => down$
|
|
41473
|
-
up: () => up$
|
|
42635
|
+
down: () => down$21,
|
|
42636
|
+
up: () => up$21
|
|
41474
42637
|
});
|
|
41475
42638
|
/**
|
|
41476
42639
|
* App users (ADR 0005):
|
|
@@ -41481,20 +42644,20 @@ var _20261002130000_app_auth_token_version_and_login_code_pkce_exports = /* @__P
|
|
|
41481
42644
|
* sign-in; the one-time login code is only exchanged with the matching verifier. Codes live a minute, so
|
|
41482
42645
|
* any pending ones (issued without a challenge) are dropped rather than given a placeholder.
|
|
41483
42646
|
*/
|
|
41484
|
-
const up$
|
|
42647
|
+
const up$21 = async (db) => {
|
|
41485
42648
|
await db.schema.alterTable("app_users").addColumn("token_version", "integer", (col) => col.notNull().defaultTo(0)).execute();
|
|
41486
42649
|
await sql`delete from app_login_codes`.execute(db);
|
|
41487
42650
|
await db.schema.alterTable("app_login_codes").addColumn("code_challenge", "text", (col) => col.notNull()).execute();
|
|
41488
42651
|
};
|
|
41489
|
-
const down$
|
|
42652
|
+
const down$21 = async (db) => {
|
|
41490
42653
|
await db.schema.alterTable("app_login_codes").dropColumn("code_challenge").execute();
|
|
41491
42654
|
await db.schema.alterTable("app_users").dropColumn("token_version").execute();
|
|
41492
42655
|
};
|
|
41493
42656
|
//#endregion
|
|
41494
42657
|
//#region src/db/migrations/20261002180000_add_publication_log_diff_indexes.ts
|
|
41495
42658
|
var _20261002180000_add_publication_log_diff_indexes_exports = /* @__PURE__ */ __exportAll({
|
|
41496
|
-
down: () => down$
|
|
41497
|
-
up: () => up$
|
|
42659
|
+
down: () => down$20,
|
|
42660
|
+
up: () => up$20
|
|
41498
42661
|
});
|
|
41499
42662
|
/**
|
|
41500
42663
|
* The snapshot diff (repositories/snapshotDiff.ts) finds the (entry, locale) pairs whose live period opened
|
|
@@ -41503,7 +42666,7 @@ var _20261002180000_add_publication_log_diff_indexes_exports = /* @__PURE__ */ _
|
|
|
41503
42666
|
* - `(entry_id, locale, from_seq)`: "the row live at seq N" is the latest row starting at or before N.
|
|
41504
42667
|
* Periods that opened in the range use the existing `(model_id, from_seq)` index.
|
|
41505
42668
|
*/
|
|
41506
|
-
const up$
|
|
42669
|
+
const up$20 = async (db) => {
|
|
41507
42670
|
await sql`create index publication_log_to_seq_idx on publication_log (to_seq) where to_seq is not null`.execute(db);
|
|
41508
42671
|
await db.schema.createIndex("publication_log_entry_locale_from_idx").on("publication_log").columns([
|
|
41509
42672
|
"entry_id",
|
|
@@ -41511,15 +42674,15 @@ const up$17 = async (db) => {
|
|
|
41511
42674
|
"from_seq"
|
|
41512
42675
|
]).execute();
|
|
41513
42676
|
};
|
|
41514
|
-
const down$
|
|
42677
|
+
const down$20 = async (db) => {
|
|
41515
42678
|
await db.schema.dropIndex("publication_log_entry_locale_from_idx").execute();
|
|
41516
42679
|
await db.schema.dropIndex("publication_log_to_seq_idx").execute();
|
|
41517
42680
|
};
|
|
41518
42681
|
//#endregion
|
|
41519
42682
|
//#region src/db/migrations/20261002180100_create_usage_tables.ts
|
|
41520
42683
|
var _20261002180100_create_usage_tables_exports = /* @__PURE__ */ __exportAll({
|
|
41521
|
-
down: () => down$
|
|
41522
|
-
up: () => up$
|
|
42684
|
+
down: () => down$19,
|
|
42685
|
+
up: () => up$19
|
|
41523
42686
|
});
|
|
41524
42687
|
/**
|
|
41525
42688
|
* Field usage from delivery traffic (plan developer-face §5): daily counters written by the in-memory
|
|
@@ -41527,7 +42690,7 @@ var _20261002180100_create_usage_tables_exports = /* @__PURE__ */ __exportAll({
|
|
|
41527
42690
|
* payloads. `principal_key` is `token:<id>`, `app_users` or `anonymous`. Pruned by the retention job
|
|
41528
42691
|
* after USAGE_RETENTION_DAYS.
|
|
41529
42692
|
*/
|
|
41530
|
-
const up$
|
|
42693
|
+
const up$19 = async (db) => {
|
|
41531
42694
|
await db.schema.createTable("field_reads").addColumn("day", "date", (col) => col.notNull()).addColumn("model_id", "uuid", (col) => col.notNull()).addColumn("field_path", "text", (col) => col.notNull()).addColumn("principal_key", "text", (col) => col.notNull()).addColumn("selection", "text", (col) => col.notNull().check(sql`selection in ('explicit', 'implicit')`)).addColumn("reads", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("last_read_at", "timestamptz", (col) => col.notNull()).addPrimaryKeyConstraint("field_reads_pkey", [
|
|
41532
42695
|
"day",
|
|
41533
42696
|
"model_id",
|
|
@@ -41538,15 +42701,15 @@ const up$16 = async (db) => {
|
|
|
41538
42701
|
await db.schema.createIndex("field_reads_model_day_idx").on("field_reads").columns(["model_id", "day"]).execute();
|
|
41539
42702
|
await db.schema.createTable("token_reads").addColumn("day", "date", (col) => col.notNull()).addColumn("principal_key", "text", (col) => col.notNull()).addColumn("requests", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("last_snapshot", "bigint").addColumn("last_read_at", "timestamptz", (col) => col.notNull()).addPrimaryKeyConstraint("token_reads_pkey", ["day", "principal_key"]).execute();
|
|
41540
42703
|
};
|
|
41541
|
-
const down$
|
|
42704
|
+
const down$19 = async (db) => {
|
|
41542
42705
|
await db.schema.dropTable("token_reads").execute();
|
|
41543
42706
|
await db.schema.dropTable("field_reads").execute();
|
|
41544
42707
|
};
|
|
41545
42708
|
//#endregion
|
|
41546
42709
|
//#region src/db/migrations/20261002180200_create_change_sets.ts
|
|
41547
42710
|
var _20261002180200_create_change_sets_exports = /* @__PURE__ */ __exportAll({
|
|
41548
|
-
down: () => down$
|
|
41549
|
-
up: () => up$
|
|
42711
|
+
down: () => down$18,
|
|
42712
|
+
up: () => up$18
|
|
41550
42713
|
});
|
|
41551
42714
|
/**
|
|
41552
42715
|
* Change sets (developer-face plan §5, ADR 0010): schema drafts and entry publications that ship as ONE
|
|
@@ -41725,7 +42888,7 @@ const migrateReleases = async (db) => {
|
|
|
41725
42888
|
await sql`drop table release_items`.execute(db);
|
|
41726
42889
|
await sql`drop table releases`.execute(db);
|
|
41727
42890
|
};
|
|
41728
|
-
const up$
|
|
42891
|
+
const up$18 = async (db) => {
|
|
41729
42892
|
await createTables(db);
|
|
41730
42893
|
await linkExecutionRecords(db);
|
|
41731
42894
|
await allowConversionRevisions(db);
|
|
@@ -41797,7 +42960,7 @@ const restoreReleases = async (db) => {
|
|
|
41797
42960
|
await sql`update webhooks set events = array_remove(array_remove(events, 'change_set.shipping'),
|
|
41798
42961
|
'change_set.discarded')`.execute(db);
|
|
41799
42962
|
};
|
|
41800
|
-
const down$
|
|
42963
|
+
const down$18 = async (db) => {
|
|
41801
42964
|
await restoreReleases(db);
|
|
41802
42965
|
await sql`alter table content_revisions drop constraint content_revisions_reason_check`.execute(db);
|
|
41803
42966
|
await sql`alter table content_revisions add constraint content_revisions_reason_check
|
|
@@ -41818,8 +42981,8 @@ const down$15 = async (db) => {
|
|
|
41818
42981
|
//#endregion
|
|
41819
42982
|
//#region src/db/migrations/20261002190000_create_editor_presence_and_content_health.ts
|
|
41820
42983
|
var _20261002190000_create_editor_presence_and_content_health_exports = /* @__PURE__ */ __exportAll({
|
|
41821
|
-
down: () => down$
|
|
41822
|
-
up: () => up$
|
|
42984
|
+
down: () => down$17,
|
|
42985
|
+
up: () => up$17
|
|
41823
42986
|
});
|
|
41824
42987
|
/**
|
|
41825
42988
|
* Editor experience (plan editor-experience §9):
|
|
@@ -41830,7 +42993,7 @@ var _20261002190000_create_editor_presence_and_content_health_exports = /* @__PU
|
|
|
41830
42993
|
* - `content_health_findings`: open and recently resolved findings of the health rules per entry and
|
|
41831
42994
|
* locale (`*` for the entry as a whole). An evaluation upserts what it found and resolves the rest.
|
|
41832
42995
|
*/
|
|
41833
|
-
const up$
|
|
42996
|
+
const up$17 = async (db) => {
|
|
41834
42997
|
await sql`
|
|
41835
42998
|
create unlogged table editor_presence (
|
|
41836
42999
|
entry_id uuid not null,
|
|
@@ -41867,15 +43030,15 @@ const up$14 = async (db) => {
|
|
|
41867
43030
|
await sql`create index content_health_findings_resolved_idx on content_health_findings (resolved_at)
|
|
41868
43031
|
where resolved_at is not null`.execute(db);
|
|
41869
43032
|
};
|
|
41870
|
-
const down$
|
|
43033
|
+
const down$17 = async (db) => {
|
|
41871
43034
|
await sql`drop table content_health_findings`.execute(db);
|
|
41872
43035
|
await sql`drop table editor_presence`.execute(db);
|
|
41873
43036
|
};
|
|
41874
43037
|
//#endregion
|
|
41875
43038
|
//#region src/db/migrations/20261003120000_add_changes_ship_permission.ts
|
|
41876
43039
|
var _20261003120000_add_changes_ship_permission_exports = /* @__PURE__ */ __exportAll({
|
|
41877
|
-
down: () => down$
|
|
41878
|
-
up: () => up$
|
|
43040
|
+
down: () => down$16,
|
|
43041
|
+
up: () => up$16
|
|
41879
43042
|
});
|
|
41880
43043
|
/**
|
|
41881
43044
|
* `changes.ship` (agentic plan §I): shipping a change set (and scheduling a ship) becomes its own global
|
|
@@ -41883,7 +43046,7 @@ var _20261003120000_add_changes_ship_permission_exports = /* @__PURE__ */ __expo
|
|
|
41883
43046
|
* live. Every role that holds `changes.manage` today keeps what it could do: it gets `changes.ship` too.
|
|
41884
43047
|
* The permissions version moves only when grants changed, so running instances reload exactly then.
|
|
41885
43048
|
*/
|
|
41886
|
-
const up$
|
|
43049
|
+
const up$16 = async (db) => {
|
|
41887
43050
|
await sql`
|
|
41888
43051
|
with granted as (
|
|
41889
43052
|
insert into admin_role_permissions (role_id, action, model_id, condition, field_ids)
|
|
@@ -41897,7 +43060,7 @@ const up$13 = async (db) => {
|
|
|
41897
43060
|
where exists (select 1 from granted)
|
|
41898
43061
|
`.execute(db);
|
|
41899
43062
|
};
|
|
41900
|
-
const down$
|
|
43063
|
+
const down$16 = async (db) => {
|
|
41901
43064
|
await sql`
|
|
41902
43065
|
with revoked as (
|
|
41903
43066
|
delete from admin_role_permissions where action = 'changes.ship' returning 1
|
|
@@ -41909,8 +43072,8 @@ const down$13 = async (db) => {
|
|
|
41909
43072
|
//#endregion
|
|
41910
43073
|
//#region src/db/migrations/20261003130000_create_sites.ts
|
|
41911
43074
|
var _20261003130000_create_sites_exports = /* @__PURE__ */ __exportAll({
|
|
41912
|
-
down: () => down$
|
|
41913
|
-
up: () => up$
|
|
43075
|
+
down: () => down$15,
|
|
43076
|
+
up: () => up$15
|
|
41914
43077
|
});
|
|
41915
43078
|
/**
|
|
41916
43079
|
* Sites (plan agentic-ecosystem §H, ADR 0011): one instance hosts many sites with one schema and one login.
|
|
@@ -42098,7 +43261,7 @@ const siteRoleAssignments = async (db) => {
|
|
|
42098
43261
|
`.execute(db);
|
|
42099
43262
|
await sql`alter table admin_invitations drop column role_ids`.execute(db);
|
|
42100
43263
|
};
|
|
42101
|
-
const up$
|
|
43264
|
+
const up$15 = async (db) => {
|
|
42102
43265
|
await createSites(db);
|
|
42103
43266
|
await addSiteColumns(db);
|
|
42104
43267
|
await tieCopiesToEntries(db);
|
|
@@ -42111,7 +43274,7 @@ const assertSingleSite = async (db) => {
|
|
|
42111
43274
|
const { rows } = await sql`select count(*)::text as count from sites`.execute(db);
|
|
42112
43275
|
if (Number(rows[0]?.count ?? 0) > 1) throw new Error("Cannot roll back sites while more than one site exists: merging them would merge their snapshot ledgers and uniqueness and widen role assignments. Delete the other sites first.");
|
|
42113
43276
|
};
|
|
42114
|
-
const down$
|
|
43277
|
+
const down$15 = async (db) => {
|
|
42115
43278
|
await assertSingleSite(db);
|
|
42116
43279
|
await sql`alter table admin_invitations add column role_ids uuid[] not null default '{}'::uuid[]`.execute(db);
|
|
42117
43280
|
await sql`
|
|
@@ -42182,8 +43345,8 @@ const down$12 = async (db) => {
|
|
|
42182
43345
|
//#endregion
|
|
42183
43346
|
//#region src/db/migrations/20261003140200_enqueue_field_index_layout.ts
|
|
42184
43347
|
var _20261003140200_enqueue_field_index_layout_exports = /* @__PURE__ */ __exportAll({
|
|
42185
|
-
down: () => down$
|
|
42186
|
-
up: () => up$
|
|
43348
|
+
down: () => down$14,
|
|
43349
|
+
up: () => up$14
|
|
42187
43350
|
});
|
|
42188
43351
|
/**
|
|
42189
43352
|
* Field index layout v2 (sites plan §H): per-field expression indexes on `entry_heads` lead with `site_id`,
|
|
@@ -42195,7 +43358,7 @@ var _20261003140200_enqueue_field_index_layout_exports = /* @__PURE__ */ __expor
|
|
|
42195
43358
|
*/
|
|
42196
43359
|
const JOB_TYPE = "schema.fieldIndexLayout";
|
|
42197
43360
|
const IDEMPOTENCY_KEY = "schema.fieldIndexLayout:v2";
|
|
42198
|
-
const up$
|
|
43361
|
+
const up$14 = async (db) => {
|
|
42199
43362
|
await sql`
|
|
42200
43363
|
insert into jobs (type, payload, max_attempts, idempotency_key)
|
|
42201
43364
|
select ${JOB_TYPE}, '{}'::jsonb, 5, ${IDEMPOTENCY_KEY}
|
|
@@ -42208,14 +43371,14 @@ const up$11 = async (db) => {
|
|
|
42208
43371
|
`.execute(db);
|
|
42209
43372
|
};
|
|
42210
43373
|
/** The layout job is idempotent; rolling back only forgets it if it has not run yet. */
|
|
42211
|
-
const down$
|
|
43374
|
+
const down$14 = async (db) => {
|
|
42212
43375
|
await sql`delete from jobs where idempotency_key = ${IDEMPOTENCY_KEY} and status = 'pending'`.execute(db);
|
|
42213
43376
|
};
|
|
42214
43377
|
//#endregion
|
|
42215
43378
|
//#region src/db/migrations/20261003140500_change_set_items_site_and_preview_entry.ts
|
|
42216
43379
|
var _20261003140500_change_set_items_site_and_preview_entry_exports = /* @__PURE__ */ __exportAll({
|
|
42217
|
-
down: () => down$
|
|
42218
|
-
up: () => up$
|
|
43380
|
+
down: () => down$13,
|
|
43381
|
+
up: () => up$13
|
|
42219
43382
|
});
|
|
42220
43383
|
/**
|
|
42221
43384
|
* Sites, publishing (plan agentic-ecosystem §H, package G5).
|
|
@@ -42226,7 +43389,7 @@ var _20261003140500_change_set_items_site_and_preview_entry_exports = /* @__PURE
|
|
|
42226
43389
|
* - `preview_tokens.entry_id` becomes required: a preview token previews one entry of one site. Model-wide
|
|
42227
43390
|
* tokens (short-lived by design) are deleted; whoever needs one opens the entry's preview again.
|
|
42228
43391
|
*/
|
|
42229
|
-
const up$
|
|
43392
|
+
const up$13 = async (db) => {
|
|
42230
43393
|
await sql`alter table change_sets add constraint change_sets_id_site_uq unique (id, site_id)`.execute(db);
|
|
42231
43394
|
await sql`alter table change_set_items add column site_id uuid`.execute(db);
|
|
42232
43395
|
await sql`
|
|
@@ -42242,7 +43405,7 @@ const up$10 = async (db) => {
|
|
|
42242
43405
|
await sql`delete from preview_tokens where entry_id is null`.execute(db);
|
|
42243
43406
|
await sql`alter table preview_tokens alter column entry_id set not null`.execute(db);
|
|
42244
43407
|
};
|
|
42245
|
-
const down$
|
|
43408
|
+
const down$13 = async (db) => {
|
|
42246
43409
|
await sql`alter table preview_tokens alter column entry_id drop not null`.execute(db);
|
|
42247
43410
|
await sql`alter table change_set_items drop constraint change_set_items_entry_site_fk`.execute(db);
|
|
42248
43411
|
await sql`alter table change_set_items drop constraint change_set_items_set_site_fk`.execute(db);
|
|
@@ -42252,8 +43415,8 @@ const down$10 = async (db) => {
|
|
|
42252
43415
|
//#endregion
|
|
42253
43416
|
//#region src/db/migrations/20261003150000_widen_deployment_providers.ts
|
|
42254
43417
|
var _20261003150000_widen_deployment_providers_exports = /* @__PURE__ */ __exportAll({
|
|
42255
|
-
down: () => down$
|
|
42256
|
-
up: () => up$
|
|
43418
|
+
down: () => down$12,
|
|
43419
|
+
up: () => up$12
|
|
42257
43420
|
});
|
|
42258
43421
|
/**
|
|
42259
43422
|
* Deploy adapters (plan agentic-ecosystem §B2): deployment connections may use the Vercel and Netlify
|
|
@@ -42268,11 +43431,11 @@ const replaceCheck = async (db, check) => {
|
|
|
42268
43431
|
check (${check}) not valid`.execute(db);
|
|
42269
43432
|
await sql`alter table deployment_connections validate constraint deployment_connections_provider_check`.execute(db);
|
|
42270
43433
|
};
|
|
42271
|
-
const up$
|
|
43434
|
+
const up$12 = async (db) => {
|
|
42272
43435
|
await replaceCheck(db, WIDENED);
|
|
42273
43436
|
};
|
|
42274
43437
|
/** Refuses while Vercel or Netlify connections exist: a down migration never deletes user data. */
|
|
42275
|
-
const down$
|
|
43438
|
+
const down$12 = async (db) => {
|
|
42276
43439
|
const { rows } = await sql`
|
|
42277
43440
|
select count(*)::text as count from deployment_connections where provider in ('vercel', 'netlify')
|
|
42278
43441
|
`.execute(db);
|
|
@@ -42283,8 +43446,8 @@ const down$9 = async (db) => {
|
|
|
42283
43446
|
//#endregion
|
|
42284
43447
|
//#region src/db/migrations/20261003160000_create_assist_runs.ts
|
|
42285
43448
|
var _20261003160000_create_assist_runs_exports = /* @__PURE__ */ __exportAll({
|
|
42286
|
-
down: () => down$
|
|
42287
|
-
up: () => up$
|
|
43449
|
+
down: () => down$11,
|
|
43450
|
+
up: () => up$11
|
|
42288
43451
|
});
|
|
42289
43452
|
/**
|
|
42290
43453
|
* Editor assists (plan agentic-ecosystem §A0, §I): one `assist_runs` row per assist request (who, which
|
|
@@ -42300,14 +43463,14 @@ const replaceSourceCheck = async (db, check) => {
|
|
|
42300
43463
|
await sql`alter table change_sets add constraint change_sets_source_check check (${check}) not valid`.execute(db);
|
|
42301
43464
|
await sql`alter table change_sets validate constraint change_sets_source_check`.execute(db);
|
|
42302
43465
|
};
|
|
42303
|
-
const up$
|
|
43466
|
+
const up$11 = async (db) => {
|
|
42304
43467
|
await db.schema.createTable("assist_runs").addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`)).addColumn("site_id", "uuid", (col) => col.notNull().references("sites.id").onDelete("cascade")).addColumn("actor_type", "text", (col) => col.notNull()).addColumn("actor_id", "text", (col) => col.notNull()).addColumn("action", "text", (col) => col.notNull().check(ACTIONS)).addColumn("rule", "text").addColumn("provider", "text", (col) => col.notNull()).addColumn("model", "text", (col) => col.notNull()).addColumn("input_tokens", "integer", (col) => col.notNull().defaultTo(0)).addColumn("output_tokens", "integer", (col) => col.notNull().defaultTo(0)).addColumn("duration_ms", "integer", (col) => col.notNull().defaultTo(0)).addColumn("status", "text", (col) => col.notNull().check(sql`status in ('queued', 'running', 'succeeded', 'failed')`)).addColumn("error_code", "text").addColumn("job_id", "uuid", (col) => col.references("jobs.id").onDelete("set null")).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("finished_at", "timestamptz").execute();
|
|
42305
43468
|
await db.schema.createIndex("assist_runs_site_created_idx").on("assist_runs").columns(["site_id", "created_at"]).execute();
|
|
42306
43469
|
await db.schema.createIndex("assist_runs_created_idx").on("assist_runs").column("created_at").execute();
|
|
42307
43470
|
await replaceSourceCheck(db, SOURCES_WIDENED);
|
|
42308
43471
|
};
|
|
42309
43472
|
/** Refuses while change sets opened by assist exist: a down migration never deletes user data. */
|
|
42310
|
-
const down$
|
|
43473
|
+
const down$11 = async (db) => {
|
|
42311
43474
|
const { rows } = await sql`
|
|
42312
43475
|
select count(*)::text as count from change_sets where source = 'assist'
|
|
42313
43476
|
`.execute(db);
|
|
@@ -42319,8 +43482,8 @@ const down$8 = async (db) => {
|
|
|
42319
43482
|
//#endregion
|
|
42320
43483
|
//#region src/db/migrations/20261003170000_audit_events_seq.ts
|
|
42321
43484
|
var _20261003170000_audit_events_seq_exports$2 = /* @__PURE__ */ __exportAll({
|
|
42322
|
-
down: () => down$
|
|
42323
|
-
up: () => up$
|
|
43485
|
+
down: () => down$10,
|
|
43486
|
+
up: () => up$10
|
|
42324
43487
|
});
|
|
42325
43488
|
/**
|
|
42326
43489
|
* `audit_events.seq`: a monotonic tiebreaker after `occurred_at`, so events written in the same instant (one
|
|
@@ -42329,17 +43492,17 @@ var _20261003170000_audit_events_seq_exports$2 = /* @__PURE__ */ __exportAll({
|
|
|
42329
43492
|
* are numbered in the order the rewrite reads them. No index: the audit queries filter and sort by
|
|
42330
43493
|
* `occurred_at` first, and `seq` only breaks ties.
|
|
42331
43494
|
*/
|
|
42332
|
-
const up$
|
|
43495
|
+
const up$10 = async (db) => {
|
|
42333
43496
|
await db.schema.alterTable("audit_events").addColumn("seq", "bigint", (col) => col.notNull().generatedAlwaysAsIdentity()).execute();
|
|
42334
43497
|
};
|
|
42335
|
-
const down$
|
|
43498
|
+
const down$10 = async (db) => {
|
|
42336
43499
|
await db.schema.alterTable("audit_events").dropColumn("seq").execute();
|
|
42337
43500
|
};
|
|
42338
43501
|
//#endregion
|
|
42339
43502
|
//#region src/db/migrations/20261004120000_models_site_scope.ts
|
|
42340
43503
|
var _20261004120000_models_site_scope_exports$2 = /* @__PURE__ */ __exportAll({
|
|
42341
|
-
down: () => down$
|
|
42342
|
-
up: () => up$
|
|
43504
|
+
down: () => down$9,
|
|
43505
|
+
up: () => up$9
|
|
42343
43506
|
});
|
|
42344
43507
|
/**
|
|
42345
43508
|
* Per-site schema (plan site-schema): a definition belongs to one site (`models.site_id`) or is shared by every
|
|
@@ -42352,7 +43515,7 @@ var _20261004120000_models_site_scope_exports$2 = /* @__PURE__ */ __exportAll({
|
|
|
42352
43515
|
* over `lower(api_key)` cannot hold. Share or delete them first; then `down` is lossless.
|
|
42353
43516
|
*/
|
|
42354
43517
|
const NIL_UUID$1 = "00000000-0000-0000-0000-000000000000";
|
|
42355
|
-
const up$
|
|
43518
|
+
const up$9 = async (db) => {
|
|
42356
43519
|
await sql`alter table models add column site_id uuid references sites (id)`.execute(db);
|
|
42357
43520
|
await sql`create index models_site_idx on models (site_id)`.execute(db);
|
|
42358
43521
|
await sql`drop index models_api_key_uq`.execute(db);
|
|
@@ -42362,7 +43525,7 @@ const up$6 = async (db) => {
|
|
|
42362
43525
|
`.execute(db);
|
|
42363
43526
|
await sql`alter table schema_drafts add column shared boolean not null default false`.execute(db);
|
|
42364
43527
|
};
|
|
42365
|
-
const down$
|
|
43528
|
+
const down$9 = async (db) => {
|
|
42366
43529
|
const { rows } = await sql`select count(*)::text as count from models where site_id is not null`.execute(db);
|
|
42367
43530
|
if (Number(rows[0]?.count ?? 0) > 0) throw new Error("Cannot roll back per-site schema while definitions belong to a site: two sites may share an API ID. Share or delete the site definitions first.");
|
|
42368
43531
|
await sql`alter table schema_drafts drop column shared`.execute(db);
|
|
@@ -42372,10 +43535,50 @@ const down$6 = async (db) => {
|
|
|
42372
43535
|
await sql`alter table models drop column site_id`.execute(db);
|
|
42373
43536
|
};
|
|
42374
43537
|
//#endregion
|
|
43538
|
+
//#region src/db/migrations/20261004130000_site_seo_settings.ts
|
|
43539
|
+
var _20261004130000_site_seo_settings_exports$2 = /* @__PURE__ */ __exportAll({
|
|
43540
|
+
down: () => down$8,
|
|
43541
|
+
up: () => up$8
|
|
43542
|
+
});
|
|
43543
|
+
/**
|
|
43544
|
+
* SEO defaults per site (plan seo-fields): `sites.seo_defaults` holds the site's default texts per locale, its
|
|
43545
|
+
* default social image and Twitter handle (`SeoDefaults` in @shapio/schema). Nullable, no default: a
|
|
43546
|
+
* catalog-only change, no table rewrite.
|
|
43547
|
+
*
|
|
43548
|
+
* `site.settings` is the site action that edits them. Every role that holds `publishing.manage` (everything
|
|
43549
|
+
* about what a site publishes) gets it; the built-in owner and admin roles get it from `ensureSystemRoles`.
|
|
43550
|
+
* The permissions version moves only when grants changed, so running instances reload exactly then.
|
|
43551
|
+
*/
|
|
43552
|
+
const up$8 = async (db) => {
|
|
43553
|
+
await sql`alter table sites add column seo_defaults jsonb`.execute(db);
|
|
43554
|
+
await sql`
|
|
43555
|
+
with granted as (
|
|
43556
|
+
insert into admin_role_permissions (role_id, action, model_id, condition, field_ids)
|
|
43557
|
+
select role_id, 'site.settings', null, null, null
|
|
43558
|
+
from admin_role_permissions
|
|
43559
|
+
where action = 'publishing.manage' and model_id is null
|
|
43560
|
+
on conflict on constraint admin_role_permissions_grant_uq do nothing
|
|
43561
|
+
returning 1
|
|
43562
|
+
)
|
|
43563
|
+
update system_versions set permissions_version = permissions_version + 1, updated_at = now()
|
|
43564
|
+
where exists (select 1 from granted)
|
|
43565
|
+
`.execute(db);
|
|
43566
|
+
};
|
|
43567
|
+
const down$8 = async (db) => {
|
|
43568
|
+
await sql`
|
|
43569
|
+
with revoked as (
|
|
43570
|
+
delete from admin_role_permissions where action = 'site.settings' returning 1
|
|
43571
|
+
)
|
|
43572
|
+
update system_versions set permissions_version = permissions_version + 1, updated_at = now()
|
|
43573
|
+
where exists (select 1 from revoked)
|
|
43574
|
+
`.execute(db);
|
|
43575
|
+
await sql`alter table sites drop column seo_defaults`.execute(db);
|
|
43576
|
+
};
|
|
43577
|
+
//#endregion
|
|
42375
43578
|
//#region src/db/migrations/mysql/0001_baseline.ts
|
|
42376
43579
|
var _0001_baseline_exports$1 = /* @__PURE__ */ __exportAll({
|
|
42377
|
-
down: () => down$
|
|
42378
|
-
up: () => up$
|
|
43580
|
+
down: () => down$7,
|
|
43581
|
+
up: () => up$7
|
|
42379
43582
|
});
|
|
42380
43583
|
/**
|
|
42381
43584
|
* MySQL baseline: Shapio's whole system schema as of PostgreSQL migration
|
|
@@ -43558,13 +44761,13 @@ const withoutForeignKeyChecks = async (db, statements) => db.connection().execut
|
|
|
43558
44761
|
await sql`set foreign_key_checks = 1`.execute(connection);
|
|
43559
44762
|
}
|
|
43560
44763
|
});
|
|
43561
|
-
const up$
|
|
43562
|
-
const down$
|
|
44764
|
+
const up$7 = async (db) => withoutForeignKeyChecks(db, STATEMENTS$1);
|
|
44765
|
+
const down$7 = async (db) => withoutForeignKeyChecks(db, [...TABLES$1].reverse().map((table) => `drop table if exists \`${table}\``));
|
|
43563
44766
|
//#endregion
|
|
43564
44767
|
//#region src/db/migrations/mysql/20261003170000_audit_events_seq.ts
|
|
43565
44768
|
var _20261003170000_audit_events_seq_exports$1 = /* @__PURE__ */ __exportAll({
|
|
43566
|
-
down: () => down$
|
|
43567
|
-
up: () => up$
|
|
44769
|
+
down: () => down$6,
|
|
44770
|
+
up: () => up$6
|
|
43568
44771
|
});
|
|
43569
44772
|
/**
|
|
43570
44773
|
* MySQL twin of `../20261003170000_audit_events_seq.ts`: `seq` is `AUTO_INCREMENT`, which InnoDB only allows
|
|
@@ -43572,26 +44775,26 @@ var _20261003170000_audit_events_seq_exports$1 = /* @__PURE__ */ __exportAll({
|
|
|
43572
44775
|
* tiebreaker). Existing rows are numbered in primary-key order. `db/mysql/tables.ts` lists `seq` as the
|
|
43573
44776
|
* table's identity, so an insert's emulated RETURNING finds its row by `LAST_INSERT_ID()`.
|
|
43574
44777
|
*/
|
|
43575
|
-
const up$
|
|
44778
|
+
const up$6 = async (db) => {
|
|
43576
44779
|
await sql`alter table \`audit_events\`
|
|
43577
44780
|
add column \`seq\` bigint not null auto_increment,
|
|
43578
44781
|
add unique key \`audit_events_seq_key\` (\`seq\`)`.execute(db);
|
|
43579
44782
|
};
|
|
43580
|
-
const down$
|
|
44783
|
+
const down$6 = async (db) => {
|
|
43581
44784
|
await sql`alter table \`audit_events\` drop column \`seq\``.execute(db);
|
|
43582
44785
|
};
|
|
43583
44786
|
//#endregion
|
|
43584
44787
|
//#region src/db/migrations/mysql/20261004120000_models_site_scope.ts
|
|
43585
44788
|
var _20261004120000_models_site_scope_exports$1 = /* @__PURE__ */ __exportAll({
|
|
43586
|
-
down: () => down$
|
|
43587
|
-
up: () => up$
|
|
44789
|
+
down: () => down$5,
|
|
44790
|
+
up: () => up$5
|
|
43588
44791
|
});
|
|
43589
44792
|
/**
|
|
43590
44793
|
* MySQL twin of `../20261004120000_models_site_scope.ts`. The per-scope API ID key uses functional key parts,
|
|
43591
44794
|
* like the baseline: `coalesce(site_id, '')` for the scope (MySQL has no `NULLS NOT DISTINCT`) and the
|
|
43592
44795
|
* case-folded API ID of live rows only (a deleted row's part is NULL, and NULLs never collide).
|
|
43593
44796
|
*/
|
|
43594
|
-
const up$
|
|
44797
|
+
const up$5 = async (db) => {
|
|
43595
44798
|
await sql`alter table \`models\` add column \`site_id\` char(36) character set ascii collate ascii_general_ci`.execute(db);
|
|
43596
44799
|
await sql`create index \`models_site_idx\` on \`models\` (\`site_id\`)`.execute(db);
|
|
43597
44800
|
await sql`alter table \`models\` add constraint \`models_site_id_fkey\` foreign key (\`site_id\`) references \`sites\` (\`id\`)`.execute(db);
|
|
@@ -43600,7 +44803,7 @@ const up$3 = async (db) => {
|
|
|
43600
44803
|
((coalesce(\`site_id\`, '')), (case when (\`deleted_at\` IS NULL) then lower(\`api_key\`) end))`.execute(db);
|
|
43601
44804
|
await sql`alter table \`schema_drafts\` add column \`shared\` tinyint(1) not null default 0`.execute(db);
|
|
43602
44805
|
};
|
|
43603
|
-
const down$
|
|
44806
|
+
const down$5 = async (db) => {
|
|
43604
44807
|
const { rows } = await sql`select count(*) as count from \`models\` where \`site_id\` is not null`.execute(db);
|
|
43605
44808
|
if (Number(rows[0]?.count ?? 0) > 0) throw new Error("Cannot roll back per-site schema while definitions belong to a site: two sites may share an API ID. Share or delete the site definitions first.");
|
|
43606
44809
|
await sql`alter table \`schema_drafts\` drop column \`shared\``.execute(db);
|
|
@@ -43612,17 +44815,45 @@ const down$3 = async (db) => {
|
|
|
43612
44815
|
await sql`alter table \`models\` drop column \`site_id\``.execute(db);
|
|
43613
44816
|
};
|
|
43614
44817
|
//#endregion
|
|
44818
|
+
//#region src/db/migrations/mysql/20261004130000_site_seo_settings.ts
|
|
44819
|
+
var _20261004130000_site_seo_settings_exports$1 = /* @__PURE__ */ __exportAll({
|
|
44820
|
+
down: () => down$4,
|
|
44821
|
+
up: () => up$4
|
|
44822
|
+
});
|
|
44823
|
+
/**
|
|
44824
|
+
* MySQL twin of `../20261004130000_site_seo_settings.ts`. `insert ignore` stands in for `on conflict do
|
|
44825
|
+
* nothing` (the grant's unique key); the permissions version moves only when a grant was added or removed.
|
|
44826
|
+
*/
|
|
44827
|
+
const bumpPermissionsVersion$1 = (db) => sql`update \`system_versions\` set \`permissions_version\` = \`permissions_version\` + 1,
|
|
44828
|
+
\`updated_at\` = current_timestamp(6)`.execute(db);
|
|
44829
|
+
const up$4 = async (db) => {
|
|
44830
|
+
await sql`alter table \`sites\` add column \`seo_defaults\` json`.execute(db);
|
|
44831
|
+
const granted = await sql`
|
|
44832
|
+
insert ignore into \`admin_role_permissions\` (\`id\`, \`role_id\`, \`action\`, \`model_id\`, \`condition\`, \`field_ids\`)
|
|
44833
|
+
select uuid(), \`role_id\`, 'site.settings', null, null, null
|
|
44834
|
+
from \`admin_role_permissions\`
|
|
44835
|
+
where \`action\` = 'publishing.manage' and \`model_id\` is null
|
|
44836
|
+
`.execute(db);
|
|
44837
|
+
if (Number(granted.numAffectedRows ?? 0) > 0) await bumpPermissionsVersion$1(db);
|
|
44838
|
+
};
|
|
44839
|
+
const down$4 = async (db) => {
|
|
44840
|
+
const revoked = await sql`delete from \`admin_role_permissions\` where \`action\` = 'site.settings'`.execute(db);
|
|
44841
|
+
if (Number(revoked.numAffectedRows ?? 0) > 0) await bumpPermissionsVersion$1(db);
|
|
44842
|
+
await sql`alter table \`sites\` drop column \`seo_defaults\``.execute(db);
|
|
44843
|
+
};
|
|
44844
|
+
//#endregion
|
|
43615
44845
|
//#region src/db/migrations/mysql/index.ts
|
|
43616
44846
|
const MYSQL_MIGRATIONS = {
|
|
43617
44847
|
"0001_baseline": _0001_baseline_exports$1,
|
|
43618
44848
|
"20261003170000_audit_events_seq": _20261003170000_audit_events_seq_exports$1,
|
|
43619
|
-
"20261004120000_models_site_scope": _20261004120000_models_site_scope_exports$1
|
|
44849
|
+
"20261004120000_models_site_scope": _20261004120000_models_site_scope_exports$1,
|
|
44850
|
+
"20261004130000_site_seo_settings": _20261004130000_site_seo_settings_exports$1
|
|
43620
44851
|
};
|
|
43621
44852
|
//#endregion
|
|
43622
44853
|
//#region src/db/migrations/sqlite/0001_baseline.ts
|
|
43623
44854
|
var _0001_baseline_exports = /* @__PURE__ */ __exportAll({
|
|
43624
|
-
down: () => down$
|
|
43625
|
-
up: () => up$
|
|
44855
|
+
down: () => down$3,
|
|
44856
|
+
up: () => up$3
|
|
43626
44857
|
});
|
|
43627
44858
|
/**
|
|
43628
44859
|
* SQLite baseline: Shapio's whole system schema as of PostgreSQL migration
|
|
@@ -44767,18 +45998,18 @@ const TABLES = [
|
|
|
44767
45998
|
"assist_runs",
|
|
44768
45999
|
"sequences"
|
|
44769
46000
|
];
|
|
44770
|
-
const up$
|
|
46001
|
+
const up$3 = async (db) => {
|
|
44771
46002
|
for (const statement of STATEMENTS) await sql.raw(statement).execute(db);
|
|
44772
46003
|
};
|
|
44773
|
-
const down$
|
|
46004
|
+
const down$3 = async (db) => {
|
|
44774
46005
|
await sql`pragma defer_foreign_keys = on`.execute(db);
|
|
44775
46006
|
for (const table of [...TABLES].reverse()) await sql.raw(`drop table if exists "${table}"`).execute(db);
|
|
44776
46007
|
};
|
|
44777
46008
|
//#endregion
|
|
44778
46009
|
//#region src/db/migrations/sqlite/20261003170000_audit_events_seq.ts
|
|
44779
46010
|
var _20261003170000_audit_events_seq_exports = /* @__PURE__ */ __exportAll({
|
|
44780
|
-
down: () => down$
|
|
44781
|
-
up: () => up$
|
|
46011
|
+
down: () => down$2,
|
|
46012
|
+
up: () => up$2
|
|
44782
46013
|
});
|
|
44783
46014
|
/**
|
|
44784
46015
|
* SQLite twin of `../20261003170000_audit_events_seq.ts`. SQLite cannot add a column whose default calls a
|
|
@@ -44805,7 +46036,7 @@ const INDEXES = [
|
|
|
44805
46036
|
`create index "audit_events_occurred_at_idx" on "audit_events" (occurred_at DESC)`,
|
|
44806
46037
|
`create index "audit_events_target_idx" on "audit_events" (target_type, target_id, occurred_at DESC)`
|
|
44807
46038
|
];
|
|
44808
|
-
const up$
|
|
46039
|
+
const up$2 = async (db) => {
|
|
44809
46040
|
await sql`insert into "sequences" (name, value) values ('audit_events_seq', 0)`.execute(db);
|
|
44810
46041
|
await sql`alter table "audit_events" rename to "audit_events_before_seq"`.execute(db);
|
|
44811
46042
|
await sql`create table "audit_events" (
|
|
@@ -44833,15 +46064,15 @@ const up$1 = async (db) => {
|
|
|
44833
46064
|
await sql`drop table "audit_events_before_seq"`.execute(db);
|
|
44834
46065
|
for (const statement of INDEXES) await sql.raw(statement).execute(db);
|
|
44835
46066
|
};
|
|
44836
|
-
const down$
|
|
46067
|
+
const down$2 = async (db) => {
|
|
44837
46068
|
await sql`alter table "audit_events" drop column "seq"`.execute(db);
|
|
44838
46069
|
await sql`delete from "sequences" where name = 'audit_events_seq'`.execute(db);
|
|
44839
46070
|
};
|
|
44840
46071
|
//#endregion
|
|
44841
46072
|
//#region src/db/migrations/sqlite/20261004120000_models_site_scope.ts
|
|
44842
46073
|
var _20261004120000_models_site_scope_exports = /* @__PURE__ */ __exportAll({
|
|
44843
|
-
down: () => down,
|
|
44844
|
-
up: () => up
|
|
46074
|
+
down: () => down$1,
|
|
46075
|
+
up: () => up$1
|
|
44845
46076
|
});
|
|
44846
46077
|
/**
|
|
44847
46078
|
* SQLite twin of `../20261004120000_models_site_scope.ts`. `down` rebuilds `models` without `site_id`
|
|
@@ -44851,7 +46082,7 @@ var _20261004120000_models_site_scope_exports = /* @__PURE__ */ __exportAll({
|
|
|
44851
46082
|
* deletes nothing they reference.
|
|
44852
46083
|
*/
|
|
44853
46084
|
const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
44854
|
-
const up = async (db) => {
|
|
46085
|
+
const up$1 = async (db) => {
|
|
44855
46086
|
await sql`alter table "models" add column "site_id" text_uuid references sites (id)`.execute(db);
|
|
44856
46087
|
await sql`create index "models_site_idx" on "models" (site_id)`.execute(db);
|
|
44857
46088
|
await sql`drop index "models_api_key_uq"`.execute(db);
|
|
@@ -44861,7 +46092,7 @@ const up = async (db) => {
|
|
|
44861
46092
|
`.execute(db);
|
|
44862
46093
|
await sql`alter table "schema_drafts" add column "shared" integer_boolean not null default 0`.execute(db);
|
|
44863
46094
|
};
|
|
44864
|
-
const down = async (db) => {
|
|
46095
|
+
const down$1 = async (db) => {
|
|
44865
46096
|
const { rows } = await sql`select count(*) as count from "models" where site_id is not null`.execute(db);
|
|
44866
46097
|
if (Number(rows[0]?.count ?? 0) > 0) throw new Error("Cannot roll back per-site schema while definitions belong to a site: two sites may share an API ID. Share or delete the site definitions first.");
|
|
44867
46098
|
await sql`alter table "schema_drafts" drop column "shared"`.execute(db);
|
|
@@ -44884,11 +46115,38 @@ const down = async (db) => {
|
|
|
44884
46115
|
await sql`create unique index "models_api_key_uq" on "models" (lower(api_key)) where (deleted_at IS NULL)`.execute(db);
|
|
44885
46116
|
};
|
|
44886
46117
|
//#endregion
|
|
46118
|
+
//#region src/db/migrations/sqlite/20261004130000_site_seo_settings.ts
|
|
46119
|
+
var _20261004130000_site_seo_settings_exports = /* @__PURE__ */ __exportAll({
|
|
46120
|
+
down: () => down,
|
|
46121
|
+
up: () => up
|
|
46122
|
+
});
|
|
46123
|
+
/**
|
|
46124
|
+
* SQLite twin of `../20261004130000_site_seo_settings.ts`. `insert or ignore` stands in for `on conflict do
|
|
46125
|
+
* nothing` (the grant's unique index); the permissions version moves only when a grant was added or removed.
|
|
46126
|
+
*/
|
|
46127
|
+
const bumpPermissionsVersion = (db) => sql`update "system_versions" set permissions_version = permissions_version + 1, updated_at = shapio_now()`.execute(db);
|
|
46128
|
+
const up = async (db) => {
|
|
46129
|
+
await sql`alter table "sites" add column "seo_defaults" text_jsonb`.execute(db);
|
|
46130
|
+
const granted = await sql`
|
|
46131
|
+
insert or ignore into "admin_role_permissions" (role_id, action, model_id, condition, field_ids)
|
|
46132
|
+
select role_id, 'site.settings', null, null, null
|
|
46133
|
+
from "admin_role_permissions"
|
|
46134
|
+
where action = 'publishing.manage' and model_id is null
|
|
46135
|
+
`.execute(db);
|
|
46136
|
+
if (Number(granted.numAffectedRows ?? 0) > 0) await bumpPermissionsVersion(db);
|
|
46137
|
+
};
|
|
46138
|
+
const down = async (db) => {
|
|
46139
|
+
const revoked = await sql`delete from "admin_role_permissions" where action = 'site.settings'`.execute(db);
|
|
46140
|
+
if (Number(revoked.numAffectedRows ?? 0) > 0) await bumpPermissionsVersion(db);
|
|
46141
|
+
await sql`alter table "sites" drop column "seo_defaults"`.execute(db);
|
|
46142
|
+
};
|
|
46143
|
+
//#endregion
|
|
44887
46144
|
//#region src/db/migrations/sqlite/index.ts
|
|
44888
46145
|
const SQLITE_MIGRATIONS = {
|
|
44889
46146
|
"0001_baseline": _0001_baseline_exports,
|
|
44890
46147
|
"20261003170000_audit_events_seq": _20261003170000_audit_events_seq_exports,
|
|
44891
|
-
"20261004120000_models_site_scope": _20261004120000_models_site_scope_exports
|
|
46148
|
+
"20261004120000_models_site_scope": _20261004120000_models_site_scope_exports,
|
|
46149
|
+
"20261004130000_site_seo_settings": _20261004130000_site_seo_settings_exports
|
|
44892
46150
|
};
|
|
44893
46151
|
//#endregion
|
|
44894
46152
|
//#region src/db/migrations/index.ts
|
|
@@ -44919,7 +46177,8 @@ const MIGRATIONS = {
|
|
|
44919
46177
|
"20261003150000_widen_deployment_providers": _20261003150000_widen_deployment_providers_exports,
|
|
44920
46178
|
"20261003160000_create_assist_runs": _20261003160000_create_assist_runs_exports,
|
|
44921
46179
|
"20261003170000_audit_events_seq": _20261003170000_audit_events_seq_exports$2,
|
|
44922
|
-
"20261004120000_models_site_scope": _20261004120000_models_site_scope_exports$2
|
|
46180
|
+
"20261004120000_models_site_scope": _20261004120000_models_site_scope_exports$2,
|
|
46181
|
+
"20261004130000_site_seo_settings": _20261004130000_site_seo_settings_exports$2
|
|
44923
46182
|
};
|
|
44924
46183
|
const PROVIDERS = {
|
|
44925
46184
|
postgres: { getMigrations: () => Promise.resolve({ ...MIGRATIONS }) },
|
|
@@ -45067,13 +46326,17 @@ const getVersionSchema = { response: { 200: Type.Object({
|
|
|
45067
46326
|
const healthRoutes = async (app) => {
|
|
45068
46327
|
declareSiteScope(app, "network");
|
|
45069
46328
|
const config = { rateLimit: false };
|
|
46329
|
+
const probeConfig = {
|
|
46330
|
+
...config,
|
|
46331
|
+
requestLog: "debug"
|
|
46332
|
+
};
|
|
45070
46333
|
app.get("/health", {
|
|
45071
46334
|
schema: getHealthSchema,
|
|
45072
|
-
config
|
|
46335
|
+
config: probeConfig
|
|
45073
46336
|
}, getHealth);
|
|
45074
46337
|
app.get("/ready", {
|
|
45075
46338
|
schema: getReadySchema,
|
|
45076
|
-
config
|
|
46339
|
+
config: probeConfig
|
|
45077
46340
|
}, getReady);
|
|
45078
46341
|
app.get("/version", {
|
|
45079
46342
|
schema: getVersionSchema,
|
|
@@ -45388,7 +46651,9 @@ const DRAFT = {
|
|
|
45388
46651
|
const ITEM_PARAMETERS = /* @__PURE__ */ new Set([
|
|
45389
46652
|
"fields",
|
|
45390
46653
|
"populate",
|
|
45391
|
-
"locale"
|
|
46654
|
+
"locale",
|
|
46655
|
+
"richText",
|
|
46656
|
+
"seo"
|
|
45392
46657
|
]);
|
|
45393
46658
|
const unauthenticated = () => new AppError(401, "INVALID_PREVIEW_TOKEN", "The preview token is invalid, expired or revoked");
|
|
45394
46659
|
const allows = (mask, fieldId) => mask.mode === "all" || mask.fieldIds.includes(fieldId);
|
|
@@ -45461,7 +46726,11 @@ const authorize = async (runtime, base, fields, request) => {
|
|
|
45461
46726
|
};
|
|
45462
46727
|
/** The token's locale wins; a different `locale` in the query is refused rather than silently ignored. */
|
|
45463
46728
|
const scopedQuery = (scope, rawQuery) => {
|
|
45464
|
-
const query = parseQueryFor(scope.context, scope.model, scope.policy, rawQuery, {
|
|
46729
|
+
const query = parseQueryFor(scope.context, scope.model, scope.policy, rawQuery, {
|
|
46730
|
+
allowSnapshot: false,
|
|
46731
|
+
allowRichText: true,
|
|
46732
|
+
allowSeo: true
|
|
46733
|
+
});
|
|
45465
46734
|
if (scope.token.locale && query.locale && query.locale !== scope.token.locale) throw new AppError(403, "PREVIEW_SCOPE", `This preview token is for locale "${scope.token.locale}"`);
|
|
45466
46735
|
return scope.token.locale ? {
|
|
45467
46736
|
...query,
|
|
@@ -45470,9 +46739,9 @@ const scopedQuery = (scope, rawQuery) => {
|
|
|
45470
46739
|
};
|
|
45471
46740
|
/** A preview token reads its one entry only. */
|
|
45472
46741
|
const entryCondition = (token) => [entryIdIs(token.entry_id)];
|
|
45473
|
-
const readOf = (scope,
|
|
46742
|
+
const readOf = (scope, query) => ({
|
|
45474
46743
|
context: scope.context,
|
|
45475
|
-
|
|
46744
|
+
source: DRAFT,
|
|
45476
46745
|
model: scope.model,
|
|
45477
46746
|
policy: scope.policy,
|
|
45478
46747
|
query,
|
|
@@ -45487,30 +46756,26 @@ const metaOf = (scope, query) => ({
|
|
|
45487
46756
|
const listPreview$1 = async (runtime, base, fields, request) => {
|
|
45488
46757
|
const scope = await authorize(runtime, base, fields, request);
|
|
45489
46758
|
const query = scopedQuery(scope, request.rawQuery);
|
|
45490
|
-
|
|
45491
|
-
|
|
45492
|
-
|
|
45493
|
-
|
|
45494
|
-
|
|
45495
|
-
|
|
45496
|
-
|
|
45497
|
-
|
|
45498
|
-
};
|
|
45499
|
-
}, scope.context.db);
|
|
46759
|
+
const { entries, limit, total } = await readEntryPage(readOf(scope, query));
|
|
46760
|
+
return {
|
|
46761
|
+
data: entries,
|
|
46762
|
+
meta: {
|
|
46763
|
+
...metaOf(scope, query),
|
|
46764
|
+
pagination: paginationMeta(query.page, limit, total)
|
|
46765
|
+
}
|
|
46766
|
+
};
|
|
45500
46767
|
};
|
|
45501
46768
|
const getPreview$1 = async (runtime, base, fields, request) => {
|
|
45502
46769
|
assertItemParameters(request.rawQuery, ITEM_PARAMETERS);
|
|
45503
46770
|
const scope = await authorize(runtime, base, fields, request);
|
|
45504
46771
|
if (scope.token.entry_id !== request.id) throw new AppError(403, "PREVIEW_SCOPE", "This preview token is for another entry");
|
|
45505
46772
|
const query = scopedQuery(scope, request.rawQuery);
|
|
45506
|
-
|
|
45507
|
-
|
|
45508
|
-
|
|
45509
|
-
|
|
45510
|
-
|
|
45511
|
-
|
|
45512
|
-
};
|
|
45513
|
-
}, scope.context.db);
|
|
46773
|
+
const { entry } = await readOneEntry(readOf(scope, query), request.id);
|
|
46774
|
+
if (!entry) throw entryNotFound(request.id);
|
|
46775
|
+
return {
|
|
46776
|
+
data: entry,
|
|
46777
|
+
meta: metaOf(scope, query)
|
|
46778
|
+
};
|
|
45514
46779
|
};
|
|
45515
46780
|
//#endregion
|
|
45516
46781
|
//#region src/controllers/previewContent.ts
|
|
@@ -45571,6 +46836,20 @@ const previewRoutes = async (app) => {
|
|
|
45571
46836
|
}, getPreview);
|
|
45572
46837
|
};
|
|
45573
46838
|
//#endregion
|
|
46839
|
+
//#region src/controllers/site.ts
|
|
46840
|
+
const getDeliverySite = async (request, reply) => sendCacheable(request, reply, await getDeliverySite$1(await contentContextFor(request)));
|
|
46841
|
+
//#endregion
|
|
46842
|
+
//#region src/routes/site/index.ts
|
|
46843
|
+
/**
|
|
46844
|
+
* /api/site (plan seo-fields): the delivery view of the request's site (token, `Shapio-Site` or `?site=`):
|
|
46845
|
+
* its key, name and SEO defaults, for pages without an entry and for GraphQL users merging SEO themselves.
|
|
46846
|
+
* Readable by any caller that reads content on the site; cacheable like delivery reads.
|
|
46847
|
+
*/
|
|
46848
|
+
const siteRoutes = async (app) => {
|
|
46849
|
+
declareSiteScope(app, "site");
|
|
46850
|
+
app.get("/", { schema: getDeliverySiteSchema }, getDeliverySite);
|
|
46851
|
+
};
|
|
46852
|
+
//#endregion
|
|
45574
46853
|
//#region src/controllers/snapshots.ts
|
|
45575
46854
|
const listSnapshotChanges = async (request) => listSnapshotChanges$1(await contentContextFor(request), request.query);
|
|
45576
46855
|
const getCurrentSnapshot = async (request) => currentSnapshot(await contentContextFor(request));
|
|
@@ -45672,6 +46951,7 @@ const buildApp = async (config, deps) => {
|
|
|
45672
46951
|
} } : {},
|
|
45673
46952
|
trustProxy: toFastifyTrustProxy(config.server.trustProxy),
|
|
45674
46953
|
genReqId: () => randomUUID(),
|
|
46954
|
+
logController: new LogController({ disableRequestLogging: true }),
|
|
45675
46955
|
ajv: { customOptions: { removeAdditional: false } }
|
|
45676
46956
|
};
|
|
45677
46957
|
const app = Fastify(options).withTypeProvider();
|
|
@@ -45681,6 +46961,7 @@ const buildApp = async (config, deps) => {
|
|
|
45681
46961
|
});
|
|
45682
46962
|
const schemaLookup = deps.fieldVisibility ?? createSchemaFieldVisibility(schemaRegistry);
|
|
45683
46963
|
await app.register(auditDeclarationPlugin);
|
|
46964
|
+
await app.register(requestLogPlugin);
|
|
45684
46965
|
await app.register(gracefulClosePlugin);
|
|
45685
46966
|
await app.register(errorHandlerPlugin);
|
|
45686
46967
|
await app.register(servicesPlugin, {
|
|
@@ -45756,6 +47037,7 @@ const buildApp = async (config, deps) => {
|
|
|
45756
47037
|
await app.register(themeCataloguePlugin, { catalogue: buildThemeCatalogue(extensions.loaded.config.themes ?? []) });
|
|
45757
47038
|
await app.register(adminExtensionsRoutes, { prefix: urls.withBasePath("/api/admin/extensions") });
|
|
45758
47039
|
await app.register(deliveryRoutes, { prefix: urls.withBasePath("/api/content") });
|
|
47040
|
+
await app.register(siteRoutes, { prefix: urls.withBasePath("/api/site") });
|
|
45759
47041
|
await app.register(snapshotsRoutes, { prefix: urls.withBasePath("/api/snapshots") });
|
|
45760
47042
|
await app.register(docsRoutes, { prefix: urls.withBasePath("/api/docs") });
|
|
45761
47043
|
if (config.graphql.enabled) {
|
|
@@ -47995,4 +49277,4 @@ const startDedicatedWorker = async (config) => {
|
|
|
47995
49277
|
//#endregion
|
|
47996
49278
|
export { toTypeName as $, update$8 as A, checkThemeContrast as B, BEFORE_EVENTS as C, listForAssets as D, buildEditorManifest as E, RESERVED_FIELD_API_KEYS as F, mergePulledLock as G, hashDefinition as H, RESERVED_TYPE_NAMES as I, schemaFilePath as J, parseDefinition as K, SCHEMA_KIND_DIRECTORIES as L, describeError as M, LOCK_FILE_PATH as N, listLiveByDriver as O, RESERVED_DEFINITION_API_KEYS as P, suggestPlural as Q, SITES_DIRECTORY as R, AFTER_EVENTS as S, EXTENSION_ROUTE_ROOT as T, lockCoversSite as U, foldApiKey as V, lockEntriesForSite as W, serializeDefinition as X, scopeDirectory as Y, serializeLockFile as Z, createLogger as _, loadConfig as a, createStableId as at, SITE_KEY_PATTERN as b, getPendingMigrations as c, sqliteDriverOf as ct, createMediaStorage as d, upgradeLockFile as et, createSite$1 as f, generateToken as g, SYSTEM_CLI_ACTOR as h, assertDatabaseReachable as i, SAFE_LINK_PATTERN as it, STORAGE_DRIVERS$1 as j, lockById$5 as k, migrateToLatest as l, createAdminDirect as m, startServer as n, withSiteCovered as nt, loadEnvFileIfPresent as o, validateRichText as ot, listAllSites as p, parseLockFile as q, copyObjectVerified as r, RICHTEXT_FORMAT as rt, buildApp as s, createDb as st, startDedicatedWorker as t, validateSchema as tt, SHAPIO_VERSION as u, createUrlBuilder as v, EXTENSION_JOB_PREFIX as w, loadProjectConfig as x, loopbackUrl as y, THEME_VARIANTS as z };
|
|
47997
49279
|
|
|
47998
|
-
//# sourceMappingURL=worker-
|
|
49280
|
+
//# sourceMappingURL=worker-i1rY2TIZ.js.map
|