@velora-cms/server 0.11.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/LICENSE +202 -0
- package/README.md +65 -0
- package/dist/admin-bundle/assets/__virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react__loadShare__.js_commonjs-proxy-Ng0fOpdU.js +1 -0
- package/dist/admin-bundle/assets/__virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react_mf_2_dom__loadShare__.js_commonjs-proxy-oc1i_iBU.js +9 -0
- package/dist/admin-bundle/assets/_virtual_mf-localSharedImportMap___mfe_internal__velora_admin__mf_owner__1-DOKdWYyj.js +1 -0
- package/dist/admin-bundle/assets/_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare___mf_0_velora_mf_2_cms_mf_1_plugin_mf_2_sdk__loadShare__.js-C1-ZAxwO.js +2 -0
- package/dist/admin-bundle/assets/_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react__loadShare__.js-BuUBskgj.js +9 -0
- package/dist/admin-bundle/assets/_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-DMaku-HC.js +9 -0
- package/dist/admin-bundle/assets/_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react_mf_2_dom__loadShare__.js-DYfqn12F.js +1 -0
- package/dist/admin-bundle/assets/_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-BPoj3DC9.js +25 -0
- package/dist/admin-bundle/assets/content-trash.schema-DfXHkC-w.js +72 -0
- package/dist/admin-bundle/assets/hostInit--C4xK6nj.js +1 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-400-normal-CyU-ddYS.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-400-normal-EA1o-u_Q.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-500-normal-C4MQITzh.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-500-normal-DPXcppbm.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-600-normal-0pRdybE_.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-arabic-600-normal-MJZ0liF5.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-400-normal-Bo5KPYvw.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-400-normal-CFZh1unN.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-500-normal-BletK1Hp.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-500-normal-Cd6jVIg7.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-600-normal-Dk5eISD_.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-600-normal-KrqB56Mw.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-400-normal-BeVLg08x.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-400-normal-BpLfq-63.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-500-normal-Ck_W4G78.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-500-normal-eJxPnL1P.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-600-normal-DT29Q8HW.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-arabic-latin-ext-600-normal-DjDAeNQD.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
- package/dist/admin-bundle/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
- package/dist/admin-bundle/assets/index-BIAF0ShP.js +542 -0
- package/dist/admin-bundle/assets/index-DhMY-0sC.js +148 -0
- package/dist/admin-bundle/assets/index-DnZV_gsg.css +1 -0
- package/dist/admin-bundle/assets/index-Do71v6NZ.js +7 -0
- package/dist/admin-bundle/assets/literata-cyrillic-ext-opsz-italic-5yUt4G_9.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-cyrillic-ext-opsz-normal-C5tfwALZ.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-cyrillic-opsz-italic-Bj0ndbyp.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-cyrillic-opsz-normal-DgmypKXX.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-greek-ext-opsz-italic-DFpO5j-Z.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-greek-ext-opsz-normal-DGm64vGR.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-greek-opsz-italic-CHERNRFq.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-greek-opsz-normal-DFglUmAd.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-latin-ext-opsz-italic-4y8iAqqs.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-latin-ext-opsz-normal-STiTPs0F.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-latin-opsz-italic-CJ93D38p.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-latin-opsz-normal-BHZsWIlc.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-vietnamese-opsz-italic-BdcrZMcO.woff2 +0 -0
- package/dist/admin-bundle/assets/literata-vietnamese-opsz-normal-Gqw5JQlq.woff2 +0 -0
- package/dist/admin-bundle/assets/mf-entry-bootstrap-0-aef015cd.js +41 -0
- package/dist/admin-bundle/assets/remoteEntry-CNyN68bc.js +14 -0
- package/dist/admin-bundle/assets/virtualExposes-BDCSo5OS.js +1 -0
- package/dist/admin-bundle/assets/vite-preload-helper-Cgledu0r.js +1 -0
- package/dist/admin-bundle/index.html +16 -0
- package/dist/app.d.ts +8 -0
- package/dist/app.js +252 -0
- package/dist/auth/api-content-author.d.ts +2 -0
- package/dist/auth/api-content-author.js +27 -0
- package/dist/auth/api-keys.d.ts +7 -0
- package/dist/auth/api-keys.js +85 -0
- package/dist/auth/jwt-payload.d.ts +16 -0
- package/dist/auth/jwt-payload.js +1 -0
- package/dist/auth/preview-tokens.d.ts +7 -0
- package/dist/auth/preview-tokens.js +76 -0
- package/dist/auth/rate-limit.d.ts +16 -0
- package/dist/auth/rate-limit.js +283 -0
- package/dist/auth/require-auth.d.ts +4 -0
- package/dist/auth/require-auth.js +28 -0
- package/dist/auth/route-rate-limits.d.ts +31 -0
- package/dist/auth/route-rate-limits.js +155 -0
- package/dist/auth/tokens.d.ts +15 -0
- package/dist/auth/tokens.js +38 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +6 -0
- package/dist/boot/core-migration-gate.d.ts +12 -0
- package/dist/boot/core-migration-gate.js +52 -0
- package/dist/boot/resolve-boot-mode.d.ts +6 -0
- package/dist/boot/resolve-boot-mode.js +206 -0
- package/dist/bundled-templates/com.velora.blog/manifest.json +39 -0
- package/dist/bundled-templates/com.velora.blog/package.tar.gz +0 -0
- package/dist/bundled-templates/com.velora.blog/sample-content.json +14 -0
- package/dist/config/admin-ui.d.ts +1 -0
- package/dist/config/admin-ui.js +10 -0
- package/dist/config/cors.d.ts +7 -0
- package/dist/config/cors.js +53 -0
- package/dist/config/env-file-path.d.ts +2 -0
- package/dist/config/env-file-path.js +22 -0
- package/dist/config/graphql-limits.d.ts +2 -0
- package/dist/config/graphql-limits.js +26 -0
- package/dist/config/headless.d.ts +1 -0
- package/dist/config/headless.js +8 -0
- package/dist/config/license-revalidation.d.ts +2 -0
- package/dist/config/license-revalidation.js +16 -0
- package/dist/config/load-env.d.ts +1 -0
- package/dist/config/load-env.js +53 -0
- package/dist/config/marketplace.d.ts +11 -0
- package/dist/config/marketplace.js +19 -0
- package/dist/config/preview.d.ts +3 -0
- package/dist/config/preview.js +39 -0
- package/dist/config/public-docs.d.ts +1 -0
- package/dist/config/public-docs.js +9 -0
- package/dist/config/site.d.ts +7 -0
- package/dist/config/site.js +12 -0
- package/dist/config/trust-proxy.d.ts +2 -0
- package/dist/config/trust-proxy.js +42 -0
- package/dist/data-types/validate-data-type-settings.d.ts +1 -0
- package/dist/data-types/validate-data-type-settings.js +33 -0
- package/dist/db/content-db.d.ts +16 -0
- package/dist/db/content-db.js +109 -0
- package/dist/db/core-migrations/index.d.ts +14 -0
- package/dist/db/core-migrations/index.js +184 -0
- package/dist/db/core-migrations/steps.d.ts +4 -0
- package/dist/db/core-migrations/steps.js +66 -0
- package/dist/db/load-prisma-client.d.ts +2 -0
- package/dist/db/load-prisma-client.js +44 -0
- package/dist/db/prisma.d.ts +3 -0
- package/dist/db/prisma.js +67 -0
- package/dist/db/resolve-client-dir.d.ts +2 -0
- package/dist/db/resolve-client-dir.js +83 -0
- package/dist/docs/filter-public-api-spec.d.ts +2 -0
- package/dist/docs/filter-public-api-spec.js +52 -0
- package/dist/docs-bundle/404.html +41 -0
- package/dist/docs-bundle/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.BcSo_yiZ.js +1 -0
- package/dist/docs-bundle/_astro/Search.astro_astro_type_script_index_0_lang.CavldbAs.js +1 -0
- package/dist/docs-bundle/_astro/TableOfContents.astro_astro_type_script_index_0_lang.Csloo1VZ.js +1 -0
- package/dist/docs-bundle/_astro/common.DTXA7sXM.css +3 -0
- package/dist/docs-bundle/_astro/ec.0vx5m.js +3 -0
- package/dist/docs-bundle/_astro/ec.w36nc.css +1 -0
- package/dist/docs-bundle/_astro/page.Dwipeu-R.js +1 -0
- package/dist/docs-bundle/_astro/print.ehPL0gv-.css +1 -0
- package/dist/docs-bundle/_astro/starlight-toc.CTCQXEge.js +1 -0
- package/dist/docs-bundle/_astro/ui-core.D-7Scnhs.js +2 -0
- package/dist/docs-bundle/favicon.svg +1 -0
- package/dist/docs-bundle/index.html +1 -0
- package/dist/docs-bundle/pagefind/fragment/en_18a36d7.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_1d9eb24.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_1ef320c.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_3099406.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_4a5b6fe.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_9aa652e.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_baccdf6.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_d81f3fe.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_d855550.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_e6d88d8.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/fragment/en_e9d3189.pf_fragment +0 -0
- package/dist/docs-bundle/pagefind/index/en_5feeed7.pf_index +0 -0
- package/dist/docs-bundle/pagefind/index/en_c6981f9.pf_index +0 -0
- package/dist/docs-bundle/pagefind/index/en_ce4f6bf.pf_index +0 -0
- package/dist/docs-bundle/pagefind/pagefind-component-ui.css +1509 -0
- package/dist/docs-bundle/pagefind/pagefind-component-ui.js +56 -0
- package/dist/docs-bundle/pagefind/pagefind-entry.json +1 -0
- package/dist/docs-bundle/pagefind/pagefind-highlight.js +1070 -0
- package/dist/docs-bundle/pagefind/pagefind-modular-ui.css +214 -0
- package/dist/docs-bundle/pagefind/pagefind-modular-ui.js +8 -0
- package/dist/docs-bundle/pagefind/pagefind-ui.css +1 -0
- package/dist/docs-bundle/pagefind/pagefind-ui.js +2 -0
- package/dist/docs-bundle/pagefind/pagefind-worker.js +6 -0
- package/dist/docs-bundle/pagefind/pagefind.en_1e94abd852.pf_meta +0 -0
- package/dist/docs-bundle/pagefind/pagefind.js +6 -0
- package/dist/docs-bundle/pagefind/wasm.en.pagefind +0 -0
- package/dist/docs-bundle/pagefind/wasm.unknown.pagefind +0 -0
- package/dist/docs-bundle/v1/index.html +86 -0
- package/dist/docs-bundle/v1/using-velora/build-a-headless-frontend/index.html +413 -0
- package/dist/docs-bundle/v1/using-velora/content-model/index.html +858 -0
- package/dist/docs-bundle/v1/using-velora/editing/index.html +748 -0
- package/dist/docs-bundle/v1/using-velora/getting-started/index.html +588 -0
- package/dist/docs-bundle/v1/using-velora/headless-api/index.html +1620 -0
- package/dist/docs-bundle/v1/using-velora/index.html +215 -0
- package/dist/docs-bundle/v1/using-velora/installation/index.html +886 -0
- package/dist/docs-bundle/v1/using-velora/installing-plugins/index.html +836 -0
- package/dist/docs-bundle/v1/using-velora/media/index.html +468 -0
- package/dist/docs-bundle/v1/using-velora/users-and-roles/index.html +561 -0
- package/dist/graphql/field-count-rule.d.ts +5 -0
- package/dist/graphql/field-count-rule.js +59 -0
- package/dist/graphql/schema.d.ts +12 -0
- package/dist/graphql/schema.js +330 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +180 -0
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +39 -0
- package/dist/marketplace/install-bundle-members.d.ts +33 -0
- package/dist/marketplace/install-bundle-members.js +137 -0
- package/dist/marketplace/install-from-marketplace.d.ts +60 -0
- package/dist/marketplace/install-from-marketplace.js +581 -0
- package/dist/marketplace/license-revalidation-job.d.ts +38 -0
- package/dist/marketplace/license-revalidation-job.js +139 -0
- package/dist/marketplace/marketplace-client.d.ts +43 -0
- package/dist/marketplace/marketplace-client.js +228 -0
- package/dist/marketplace/revalidate-license.d.ts +39 -0
- package/dist/marketplace/revalidate-license.js +103 -0
- package/dist/media/media-service.d.ts +36 -0
- package/dist/media/media-service.js +162 -0
- package/dist/plugins/boot-plugin-loader.d.ts +32 -0
- package/dist/plugins/boot-plugin-loader.js +57 -0
- package/dist/plugins/hook-dispatcher.d.ts +13 -0
- package/dist/plugins/hook-dispatcher.js +68 -0
- package/dist/plugins/install-built-package.d.ts +36 -0
- package/dist/plugins/install-built-package.js +199 -0
- package/dist/plugins/install-package-tarball.d.ts +25 -0
- package/dist/plugins/install-package-tarball.js +73 -0
- package/dist/plugins/plugin-context.d.ts +5 -0
- package/dist/plugins/plugin-context.js +26 -0
- package/dist/plugins/plugin-package-store.d.ts +31 -0
- package/dist/plugins/plugin-package-store.js +64 -0
- package/dist/plugins/plugin-registry-service.d.ts +227 -0
- package/dist/plugins/plugin-registry-service.js +636 -0
- package/dist/plugins/seed-template-content.d.ts +23 -0
- package/dist/plugins/seed-template-content.js +198 -0
- package/dist/plugins/server-serializers.d.ts +3 -0
- package/dist/plugins/server-serializers.js +16 -0
- package/dist/plugins/simple-storage-api.d.ts +9 -0
- package/dist/plugins/simple-storage-api.js +26 -0
- package/dist/plugins/snapshot-groups.d.ts +16 -0
- package/dist/plugins/snapshot-groups.js +81 -0
- package/dist/plugins/structured-query-builder.d.ts +21 -0
- package/dist/plugins/structured-query-builder.js +60 -0
- package/dist/plugins/structured-snapshot-service.d.ts +16 -0
- package/dist/plugins/structured-snapshot-service.js +286 -0
- package/dist/plugins/structured-storage-api.d.ts +22 -0
- package/dist/plugins/structured-storage-api.js +91 -0
- package/dist/plugins/structured-storage-transaction.d.ts +22 -0
- package/dist/plugins/structured-storage-transaction.js +63 -0
- package/dist/plugins/structured-table-naming.d.ts +1 -0
- package/dist/plugins/structured-table-naming.js +18 -0
- package/dist/plugins/trusted-keys.d.ts +11 -0
- package/dist/plugins/trusted-keys.js +67 -0
- package/dist/plugins/trusted-signing-keys.d.ts +27 -0
- package/dist/plugins/trusted-signing-keys.js +40 -0
- package/dist/plugins/uninstall-bundle.d.ts +29 -0
- package/dist/plugins/uninstall-bundle.js +150 -0
- package/dist/plugins/verify-package.d.ts +36 -0
- package/dist/plugins/verify-package.js +121 -0
- package/dist/routes/admin-static.d.ts +8 -0
- package/dist/routes/admin-static.js +196 -0
- package/dist/routes/api-keys.d.ts +2 -0
- package/dist/routes/api-keys.js +95 -0
- package/dist/routes/api-playground.d.ts +2 -0
- package/dist/routes/api-playground.js +21 -0
- package/dist/routes/auth.d.ts +2 -0
- package/dist/routes/auth.js +102 -0
- package/dist/routes/content.d.ts +3 -0
- package/dist/routes/content.js +471 -0
- package/dist/routes/data-types.d.ts +3 -0
- package/dist/routes/data-types.js +100 -0
- package/dist/routes/docs-static.d.ts +8 -0
- package/dist/routes/docs-static.js +137 -0
- package/dist/routes/document-types.d.ts +3 -0
- package/dist/routes/document-types.js +149 -0
- package/dist/routes/headless-bridge.d.ts +10 -0
- package/dist/routes/headless-bridge.js +207 -0
- package/dist/routes/health.d.ts +2 -0
- package/dist/routes/health.js +15 -0
- package/dist/routes/marketplace-proxy.d.ts +8 -0
- package/dist/routes/marketplace-proxy.js +135 -0
- package/dist/routes/media.d.ts +6 -0
- package/dist/routes/media.js +93 -0
- package/dist/routes/plugin-api-routes.d.ts +12 -0
- package/dist/routes/plugin-api-routes.js +224 -0
- package/dist/routes/plugin-assets.d.ts +8 -0
- package/dist/routes/plugin-assets.js +51 -0
- package/dist/routes/plugin-management.d.ts +7 -0
- package/dist/routes/plugin-management.js +525 -0
- package/dist/routes/plugin-storage.d.ts +8 -0
- package/dist/routes/plugin-storage.js +104 -0
- package/dist/routes/plugins.d.ts +2 -0
- package/dist/routes/plugins.js +36 -0
- package/dist/routes/public-api.d.ts +9 -0
- package/dist/routes/public-api.js +545 -0
- package/dist/routes/published-item.d.ts +57 -0
- package/dist/routes/published-item.js +236 -0
- package/dist/routes/setup.d.ts +9 -0
- package/dist/routes/setup.js +477 -0
- package/dist/routes/snapshots.d.ts +3 -0
- package/dist/routes/snapshots.js +65 -0
- package/dist/routes/templates.d.ts +3 -0
- package/dist/routes/templates.js +37 -0
- package/dist/routes/themes.d.ts +2 -0
- package/dist/routes/themes.js +30 -0
- package/dist/routes/users.d.ts +2 -0
- package/dist/routes/users.js +113 -0
- package/dist/sandbox/register-local-plugin.d.ts +36 -0
- package/dist/sandbox/register-local-plugin.js +108 -0
- package/dist/sandbox/seed-sandbox.d.ts +20 -0
- package/dist/sandbox/seed-sandbox.js +122 -0
- package/dist/scripts/copy-admin-bundle.d.ts +1 -0
- package/dist/scripts/copy-admin-bundle.js +24 -0
- package/dist/scripts/copy-docs-bundle.d.ts +1 -0
- package/dist/scripts/copy-docs-bundle.js +22 -0
- package/dist/scripts/create-api-key.d.ts +5 -0
- package/dist/scripts/create-api-key.js +56 -0
- package/dist/scripts/generate-prisma-clients.d.ts +6 -0
- package/dist/scripts/generate-prisma-clients.js +125 -0
- package/dist/scripts/pack-bundled-templates.d.ts +1 -0
- package/dist/scripts/pack-bundled-templates.js +87 -0
- package/dist/setup/build-connection-string.d.ts +19 -0
- package/dist/setup/build-connection-string.js +14 -0
- package/dist/setup/bundled-templates.d.ts +19 -0
- package/dist/setup/bundled-templates.js +121 -0
- package/dist/setup/ensure-prisma-client.d.ts +3 -0
- package/dist/setup/ensure-prisma-client.js +23 -0
- package/dist/setup/install-starter.d.ts +14 -0
- package/dist/setup/install-starter.js +76 -0
- package/dist/setup/normalize-database-url.d.ts +2 -0
- package/dist/setup/normalize-database-url.js +35 -0
- package/dist/setup/persist-env-config.d.ts +2 -0
- package/dist/setup/persist-env-config.js +63 -0
- package/dist/setup/provision-database.d.ts +2 -0
- package/dist/setup/provision-database.js +89 -0
- package/dist/setup-app.d.ts +4 -0
- package/dist/setup-app.js +43 -0
- package/package.json +96 -0
- package/prisma/schema.prisma +303 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { PluginListQuerySchema, PluginListResponseSchema } from "@velora-cms/api-schemas";
|
|
2
|
+
import { prisma } from "../db/prisma.js";
|
|
3
|
+
// Read-only, no auth (matches GET /api/data-types — this is metadata,
|
|
4
|
+
// not content). Always filters to enabled:true: an installed-but-inactive
|
|
5
|
+
// plugin isn't a candidate for the admin's boot-time remote loader, the
|
|
6
|
+
// only consumer today.
|
|
7
|
+
export const pluginRoutes = async (app) => {
|
|
8
|
+
app.get("/api/plugins", {
|
|
9
|
+
schema: {
|
|
10
|
+
querystring: PluginListQuerySchema,
|
|
11
|
+
response: {
|
|
12
|
+
200: PluginListResponseSchema,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
}, async (request) => {
|
|
16
|
+
const { cursor, limit } = request.query;
|
|
17
|
+
const rows = await prisma.pluginRegistry.findMany({
|
|
18
|
+
where: { enabled: true },
|
|
19
|
+
orderBy: { id: "asc" },
|
|
20
|
+
take: limit,
|
|
21
|
+
...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
|
|
22
|
+
});
|
|
23
|
+
const items = rows.map((row) => ({
|
|
24
|
+
pluginId: row.pluginId,
|
|
25
|
+
name: row.name,
|
|
26
|
+
type: row.type,
|
|
27
|
+
version: row.version,
|
|
28
|
+
trustTier: row.trustTier,
|
|
29
|
+
storageType: row.storageType,
|
|
30
|
+
enabled: row.enabled,
|
|
31
|
+
remoteUrl: row.remoteUrl,
|
|
32
|
+
}));
|
|
33
|
+
const nextCursor = items.length === limit ? (rows[rows.length - 1]?.id ?? null) : null;
|
|
34
|
+
return { items, nextCursor };
|
|
35
|
+
});
|
|
36
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FastifyPluginAsyncZod } from "fastify-type-provider-zod";
|
|
2
|
+
import type { ContentDb } from "../db/content-db.js";
|
|
3
|
+
import type { MediaService } from "../media/media-service.js";
|
|
4
|
+
interface PublicApiRoutesOptions extends ContentDb {
|
|
5
|
+
mediaService: MediaService;
|
|
6
|
+
apiContentAuthorId: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const publicApiRoutes: FastifyPluginAsyncZod<PublicApiRoutesOptions>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { Ajv } from "ajv";
|
|
3
|
+
import mercurius from "mercurius";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { ApiErrorSchema, ContentValidationErrorSchema, PublicContentByPathQuerySchema, PublicContentFormatResponseSchema, PublicContentItemQuerySchema, PublicContentItemSchema, PublicContentListQuerySchema, PublicContentListResponseSchema, PublicContentSearchQuerySchema, PublicContentWriteResponseSchema, PublicCreateContentRequestSchema, PublicUpdateContentRequestSchema, PublicDocumentTypeListQuerySchema, PublicDocumentTypeListResponseSchema, PublicDocumentTypeSchema, PublicMediaFileSchema, PublicMediaListQuerySchema, PublicMediaListResponseSchema, PublicTemplateListQuerySchema, PublicTemplateListResponseSchema, } from "@velora-cms/api-schemas";
|
|
6
|
+
import { ContentValidationError, DocumentTypeNotFoundForContentError, ParentContentNotFoundError, TreeRulesViolationError, } from "@velora-cms/content-service";
|
|
7
|
+
import { requireApiKey, requireApiKeyScope } from "../auth/api-keys.js";
|
|
8
|
+
import { verifyPreviewToken } from "../auth/preview-tokens.js";
|
|
9
|
+
import { rateLimitConfig } from "../auth/rate-limit.js";
|
|
10
|
+
import { isPublicDocsEnabled } from "../config/public-docs.js";
|
|
11
|
+
import { maxQueryComplexity, maxQueryDepth } from "../config/graphql-limits.js";
|
|
12
|
+
import { createFieldCountRule } from "../graphql/field-count-rule.js";
|
|
13
|
+
import { buildGraphqlSchema } from "../graphql/schema.js";
|
|
14
|
+
import { computeExtensions, isNotFoundError, preparePublishedItem, resolveItemNode, resolveSubtreeFilter, toPublicDocumentType, toPublicMediaFile, toSummaries, } from "./published-item.js";
|
|
15
|
+
const PublicContentParamsSchema = z.object({ id: z.string().uuid() });
|
|
16
|
+
// mercurius' own /api/v1/graphql routes carry plain JSON-Schema objects for
|
|
17
|
+
// their request/response envelope (not Zod). This app's validatorCompiler/
|
|
18
|
+
// serializerCompiler are set globally to fastify-type-provider-zod's Zod
|
|
19
|
+
// compilers (app.ts), which unconditionally treat `schema` as a Zod object
|
|
20
|
+
// and mis-validate anything else — every mercurius request failed
|
|
21
|
+
// validation until these were wired in as a per-route override via
|
|
22
|
+
// mercurius' `additionalRouteOptions` (Session 113, Task 2). Plain `ajv`,
|
|
23
|
+
// already a direct dependency (see validate-data-type-settings.ts), stands
|
|
24
|
+
// in for Fastify's own default compiler here.
|
|
25
|
+
const mercuriusAjv = new Ajv({ allErrors: true, strict: false });
|
|
26
|
+
const mercuriusValidatorCompiler = ({ schema }) => {
|
|
27
|
+
const validate = mercuriusAjv.compile(schema);
|
|
28
|
+
return (data) => {
|
|
29
|
+
if (validate(data))
|
|
30
|
+
return { value: data };
|
|
31
|
+
return { error: new Error(mercuriusAjv.errorsText(validate.errors)) };
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
// fast-json-stringify isn't wired up for this one-off route; a plain
|
|
35
|
+
// JSON.stringify is a correct (if slower) response serializer, and
|
|
36
|
+
// response payloads here are small.
|
|
37
|
+
const mercuriusSerializerCompiler = () => {
|
|
38
|
+
return (data) => JSON.stringify(data);
|
|
39
|
+
};
|
|
40
|
+
// One uniform 404 body for every public miss — unknown id, unknown path,
|
|
41
|
+
// draft, unpublished translation. Distinguishing them would let a keyed
|
|
42
|
+
// caller probe which drafts exist.
|
|
43
|
+
const NOT_FOUND_MESSAGE = "No published content found";
|
|
44
|
+
// The public Content API namespace (Month 9). Registered by buildApp()
|
|
45
|
+
// only when VELORA_HEADLESS is on — when off, none of these routes exist.
|
|
46
|
+
// Every route is API-key gated (requireApiKey), never JWT. Strict
|
|
47
|
+
// published-only rule: every node this surface touches — the anchor of a
|
|
48
|
+
// locale resolution AND the resolved translation — must be published, so
|
|
49
|
+
// drafts are indistinguishable from nonexistent everywhere. Exception: the
|
|
50
|
+
// write routes (create/PUT/PATCH/publish, Session 107) operate on a node by
|
|
51
|
+
// id in any publish state — a key holder is a trusted writer editing their
|
|
52
|
+
// own draft, not an anonymous reader.
|
|
53
|
+
export const publicApiRoutes = async (app, opts) => {
|
|
54
|
+
await app.register(import("@fastify/rate-limit"), rateLimitConfig);
|
|
55
|
+
const { adapter, contentService, mediaService, apiContentAuthorId } = opts;
|
|
56
|
+
// Shared by the by-id and by-path routes: apply locale resolution (same
|
|
57
|
+
// contract as the backoffice — fallback is a locale string), enforce the
|
|
58
|
+
// published-only rule on the resolved node, and load its pointer version.
|
|
59
|
+
async function sendPublishedItem(request, reply, anchor, query, previewContentId) {
|
|
60
|
+
const node = await resolveItemNode({ adapter, contentService }, anchor, { locale: query.locale, fallback: query.fallback }, { previewContentId });
|
|
61
|
+
if (!node) {
|
|
62
|
+
return reply.code(404).send({ message: NOT_FOUND_MESSAGE });
|
|
63
|
+
}
|
|
64
|
+
// Passed in AFTER locale resolution, against the FINAL resolved
|
|
65
|
+
// node — `preparePublishedItem` itself only bypasses its gate when
|
|
66
|
+
// `node.id === previewContentId`, so a token for a different node
|
|
67
|
+
// (or no token) has no effect here.
|
|
68
|
+
const prepare = await preparePublishedItem({
|
|
69
|
+
adapter,
|
|
70
|
+
listSerializerContributions: () => app.pluginRegistry.listActiveSerializerContributions(),
|
|
71
|
+
warn: (obj, msg) => request.log.warn(obj, msg),
|
|
72
|
+
}, node, { previewContentId });
|
|
73
|
+
if (prepare.status === "notFound") {
|
|
74
|
+
return reply.code(404).send({ message: NOT_FOUND_MESSAGE });
|
|
75
|
+
}
|
|
76
|
+
if (prepare.status === "brokenVersion") {
|
|
77
|
+
return reply.code(500).send({ message: prepare.message });
|
|
78
|
+
}
|
|
79
|
+
const { core, data, contributions, buildSerializerContext } = prepare;
|
|
80
|
+
// ?format=<pluginId>:<name> — a whole-response projection owned by one
|
|
81
|
+
// plugin. Never falls back to the default shape on failure: the caller
|
|
82
|
+
// asked for a specific projection, so a missing/broken one is a 400.
|
|
83
|
+
if (query.format) {
|
|
84
|
+
const [pluginId, formatName] = query.format.split(":", 2);
|
|
85
|
+
const owner = contributions.find((c) => c.pluginId === pluginId);
|
|
86
|
+
const format = owner?.serializers.customFormats?.find((f) => f.name === formatName);
|
|
87
|
+
if (!format) {
|
|
88
|
+
return reply.code(400).send({ message: `Unknown format ${query.format}` });
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const content = await format.build(buildSerializerContext());
|
|
92
|
+
JSON.stringify(content); // S111-handoff guard: non-JSON output is a plugin bug, not a 500
|
|
93
|
+
return reply.send({ format: query.format, content });
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
request.log.warn({ pluginId, format: formatName, err: error }, "custom format build failed");
|
|
97
|
+
return reply.code(400).send({ message: `Format ${query.format} failed to build` });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const extensions = await computeExtensions(prepare, (obj, msg) => request.log.warn(obj, msg));
|
|
101
|
+
return reply.send({
|
|
102
|
+
...core,
|
|
103
|
+
data,
|
|
104
|
+
...(extensions ? { extensions } : {}),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
app.get("/api/v1/content/roots", {
|
|
108
|
+
onRequest: requireApiKey,
|
|
109
|
+
schema: {
|
|
110
|
+
querystring: PublicContentListQuerySchema,
|
|
111
|
+
response: { 200: PublicContentListResponseSchema, 401: ApiErrorSchema },
|
|
112
|
+
},
|
|
113
|
+
}, async (request) => {
|
|
114
|
+
const { cursor, limit } = request.query;
|
|
115
|
+
const items = await adapter.getRootNodes({ cursor, limit, publishedOnly: true });
|
|
116
|
+
const nextCursor = items.length === limit ? (items[items.length - 1]?.id ?? null) : null;
|
|
117
|
+
return { items: toSummaries(items), nextCursor };
|
|
118
|
+
});
|
|
119
|
+
app.get("/api/v1/content/by-path", {
|
|
120
|
+
onRequest: requireApiKey,
|
|
121
|
+
schema: {
|
|
122
|
+
querystring: PublicContentByPathQuerySchema,
|
|
123
|
+
response: {
|
|
124
|
+
200: z.union([PublicContentItemSchema, PublicContentFormatResponseSchema]),
|
|
125
|
+
400: ApiErrorSchema,
|
|
126
|
+
401: ApiErrorSchema,
|
|
127
|
+
404: ApiErrorSchema,
|
|
128
|
+
500: ApiErrorSchema,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}, async (request, reply) => {
|
|
132
|
+
const { path, ...query } = request.query;
|
|
133
|
+
// Session 116 (Task 2 fix): verified ONCE, up front, so it can also
|
|
134
|
+
// decide whether the anchor fetch itself may return a draft.
|
|
135
|
+
// `verifyPreviewToken` returns null for every failure mode (absent,
|
|
136
|
+
// malformed, tampered, expired), so an absent/invalid token
|
|
137
|
+
// collapses to `previewContentId: null` and the anchor fetch keeps
|
|
138
|
+
// its original `publishedOnly: true` — bit-for-bit today's
|
|
139
|
+
// behavior. A verified token lifts the adapter-level filter so a
|
|
140
|
+
// draft-only anchor can even be fetched; `resolveItemNode` and
|
|
141
|
+
// `preparePublishedItem` still do ALL the actual gating below (a
|
|
142
|
+
// token only ever unlocks the exact node it names).
|
|
143
|
+
const previewContentId = query.previewToken
|
|
144
|
+
? (verifyPreviewToken(query.previewToken)?.contentId ?? null)
|
|
145
|
+
: null;
|
|
146
|
+
const anchor = previewContentId
|
|
147
|
+
? await adapter.getContentNodeByPath(path)
|
|
148
|
+
: await adapter.getContentNodeByPath(path, { publishedOnly: true });
|
|
149
|
+
return sendPublishedItem(request, reply, anchor, query, previewContentId);
|
|
150
|
+
});
|
|
151
|
+
app.get("/api/v1/content", {
|
|
152
|
+
onRequest: requireApiKey,
|
|
153
|
+
schema: {
|
|
154
|
+
querystring: PublicContentSearchQuerySchema,
|
|
155
|
+
response: {
|
|
156
|
+
200: PublicContentListResponseSchema,
|
|
157
|
+
401: ApiErrorSchema,
|
|
158
|
+
404: ApiErrorSchema,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}, async (request, reply) => {
|
|
162
|
+
const { documentTypeId, under, q, cursor, limit } = request.query;
|
|
163
|
+
let pathPrefix;
|
|
164
|
+
if (under) {
|
|
165
|
+
const resolved = await resolveSubtreeFilter(adapter, under);
|
|
166
|
+
if (!resolved) {
|
|
167
|
+
return reply.code(404).send({ message: NOT_FOUND_MESSAGE });
|
|
168
|
+
}
|
|
169
|
+
pathPrefix = resolved.pathPrefix;
|
|
170
|
+
}
|
|
171
|
+
const items = await adapter.searchPublishedContent({
|
|
172
|
+
cursor,
|
|
173
|
+
limit,
|
|
174
|
+
documentTypeId,
|
|
175
|
+
pathPrefix,
|
|
176
|
+
text: q,
|
|
177
|
+
});
|
|
178
|
+
const nextCursor = items.length === limit ? (items[items.length - 1]?.id ?? null) : null;
|
|
179
|
+
return { items: toSummaries(items), nextCursor };
|
|
180
|
+
});
|
|
181
|
+
function toWriteResponse(node, version) {
|
|
182
|
+
return {
|
|
183
|
+
id: node.id,
|
|
184
|
+
documentTypeId: node.documentTypeId,
|
|
185
|
+
parentId: node.parentId,
|
|
186
|
+
path: node.path,
|
|
187
|
+
locale: node.locale,
|
|
188
|
+
version: version.version,
|
|
189
|
+
publishedAt: node.publishedAt,
|
|
190
|
+
data: version.data,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
app.post("/api/v1/content", {
|
|
194
|
+
onRequest: [requireApiKey, requireApiKeyScope("write")],
|
|
195
|
+
schema: {
|
|
196
|
+
description: "Create a content node. Derives path and depth server-side from parentId + slug; sortOrder is fixed at 0 for API-created nodes. Non-empty `data` is fully field-validated against the document type (400 with fieldErrors); empty `data` ({}) creates a stub to fill in later via PATCH.",
|
|
197
|
+
body: PublicCreateContentRequestSchema,
|
|
198
|
+
response: {
|
|
199
|
+
201: PublicContentWriteResponseSchema,
|
|
200
|
+
// Union so both bodies serialize on the one 400 slot: the
|
|
201
|
+
// structured field-validation body, and the plain-message body
|
|
202
|
+
// Fastify's own Zod body validation produces.
|
|
203
|
+
400: z.union([ContentValidationErrorSchema, ApiErrorSchema]),
|
|
204
|
+
401: ApiErrorSchema,
|
|
205
|
+
403: ApiErrorSchema,
|
|
206
|
+
404: ApiErrorSchema,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
}, async (request, reply) => {
|
|
210
|
+
const { documentTypeId, parentId, slug, locale, isDefaultLocale, translationGroupId, data } = request.body;
|
|
211
|
+
// Derive path/depth from the parent (create derives tree internals so
|
|
212
|
+
// an external consumer never computes materialized-path values).
|
|
213
|
+
let path;
|
|
214
|
+
let depth;
|
|
215
|
+
if (parentId === null) {
|
|
216
|
+
path = `/${slug}`;
|
|
217
|
+
depth = 0;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const parent = await adapter.getContentNode(parentId);
|
|
221
|
+
if (!parent) {
|
|
222
|
+
return reply.code(404).send({ message: `No content found for parent id ${parentId}` });
|
|
223
|
+
}
|
|
224
|
+
path = `${parent.path}/${slug}`;
|
|
225
|
+
depth = parent.depth + 1;
|
|
226
|
+
}
|
|
227
|
+
// Field validation of non-empty `data` lives in
|
|
228
|
+
// ContentService.createContent (S182), so the backoffice and public
|
|
229
|
+
// create paths share one rule; the ContentValidationError it throws
|
|
230
|
+
// is mapped to a 400 by the catch below.
|
|
231
|
+
try {
|
|
232
|
+
const { node, version } = await contentService.createContent({
|
|
233
|
+
documentTypeId,
|
|
234
|
+
parentId,
|
|
235
|
+
path,
|
|
236
|
+
depth,
|
|
237
|
+
sortOrder: 0,
|
|
238
|
+
locale,
|
|
239
|
+
isDefaultLocale,
|
|
240
|
+
...(translationGroupId ? { translationGroupId } : {}),
|
|
241
|
+
data,
|
|
242
|
+
createdBy: apiContentAuthorId,
|
|
243
|
+
});
|
|
244
|
+
return reply.code(201).send(toWriteResponse(node, version));
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (error instanceof ParentContentNotFoundError) {
|
|
248
|
+
return reply.code(404).send({ message: error.message });
|
|
249
|
+
}
|
|
250
|
+
if (error instanceof ContentValidationError) {
|
|
251
|
+
return reply.code(400).send({ message: error.message, fieldErrors: error.failures });
|
|
252
|
+
}
|
|
253
|
+
if (error instanceof DocumentTypeNotFoundForContentError || error instanceof TreeRulesViolationError) {
|
|
254
|
+
// Same 400 slot as the field-validation branch above, so it must
|
|
255
|
+
// satisfy ContentValidationErrorSchema's required fieldErrors —
|
|
256
|
+
// there are no per-field failures for these two error kinds, so
|
|
257
|
+
// an empty array is the honest shape, not a placeholder.
|
|
258
|
+
return reply.code(400).send({ message: error.message, fieldErrors: [] });
|
|
259
|
+
}
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
// Shared by PUT/PATCH: resolve the node (any publish state — writers edit
|
|
264
|
+
// drafts), compute the data to save per the verb, then go through
|
|
265
|
+
// saveContent (Session 106 field-validates the result). Returns the reply.
|
|
266
|
+
async function saveContentWrite(reply, id, computeData) {
|
|
267
|
+
const node = await adapter.getContentNode(id);
|
|
268
|
+
if (!node) {
|
|
269
|
+
return reply.code(404).send({ message: `No content found for id ${id}` });
|
|
270
|
+
}
|
|
271
|
+
const latest = await adapter.getLatestContentVersion(id);
|
|
272
|
+
if (!latest) {
|
|
273
|
+
return reply.code(404).send({ message: `No content found for id ${id}` });
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const version = await contentService.saveContent(id, computeData(latest), apiContentAuthorId);
|
|
277
|
+
// saveContent doesn't return the node; the node's path/parentId/locale
|
|
278
|
+
// are unchanged by a save, so reuse the one already fetched.
|
|
279
|
+
return reply.code(200).send(toWriteResponse(node, version));
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
if (error instanceof ContentValidationError) {
|
|
283
|
+
return reply.code(400).send({ message: error.message, fieldErrors: error.failures });
|
|
284
|
+
}
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
app.put("/api/v1/content/:id", {
|
|
289
|
+
onRequest: [requireApiKey, requireApiKeyScope("write")],
|
|
290
|
+
schema: {
|
|
291
|
+
description: "Full-replace update: the node's data becomes exactly the request body's `data`, as a new immutable version. The whole record is field-validated, so omitting a required field is a 400. Use PATCH to change a subset of fields.",
|
|
292
|
+
params: PublicContentParamsSchema,
|
|
293
|
+
body: PublicUpdateContentRequestSchema,
|
|
294
|
+
response: {
|
|
295
|
+
200: PublicContentWriteResponseSchema,
|
|
296
|
+
400: z.union([ContentValidationErrorSchema, ApiErrorSchema]),
|
|
297
|
+
401: ApiErrorSchema,
|
|
298
|
+
403: ApiErrorSchema,
|
|
299
|
+
404: ApiErrorSchema,
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
}, async (request, reply) => saveContentWrite(reply, request.params.id, () => request.body.data));
|
|
303
|
+
app.patch("/api/v1/content/:id", {
|
|
304
|
+
onRequest: [requireApiKey, requireApiKeyScope("write")],
|
|
305
|
+
schema: {
|
|
306
|
+
description: "Partial update: the request body's `data` keys are shallow-merged over the node's latest version (top-level per-key replace — a JSON/object field value is replaced whole, never deep-merged), saved as a new immutable version. The merged result is field-validated. Omitting a required field the node already has succeeds.",
|
|
307
|
+
params: PublicContentParamsSchema,
|
|
308
|
+
body: PublicUpdateContentRequestSchema,
|
|
309
|
+
response: {
|
|
310
|
+
200: PublicContentWriteResponseSchema,
|
|
311
|
+
400: z.union([ContentValidationErrorSchema, ApiErrorSchema]),
|
|
312
|
+
401: ApiErrorSchema,
|
|
313
|
+
403: ApiErrorSchema,
|
|
314
|
+
404: ApiErrorSchema,
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
}, async (request, reply) => saveContentWrite(reply, request.params.id, (latest) => ({ ...latest.data, ...request.body.data })));
|
|
318
|
+
app.post("/api/v1/content/:id/publish", {
|
|
319
|
+
onRequest: [requireApiKey, requireApiKeyScope("write")],
|
|
320
|
+
schema: {
|
|
321
|
+
description: "Publish the node's latest saved version, repointing current_version (no new version row). Makes the node readable on the published-only read surface.",
|
|
322
|
+
params: PublicContentParamsSchema,
|
|
323
|
+
response: {
|
|
324
|
+
200: PublicContentWriteResponseSchema,
|
|
325
|
+
401: ApiErrorSchema,
|
|
326
|
+
403: ApiErrorSchema,
|
|
327
|
+
404: ApiErrorSchema,
|
|
328
|
+
500: ApiErrorSchema,
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
}, async (request, reply) => {
|
|
332
|
+
try {
|
|
333
|
+
const node = await contentService.publishContent(request.params.id);
|
|
334
|
+
const version = await adapter.getContentVersion(node.id, node.currentVersion);
|
|
335
|
+
if (!version) {
|
|
336
|
+
return reply.code(500).send({
|
|
337
|
+
message: `current_version (${node.currentVersion}) for ${node.id} has no matching content version`,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return reply.code(200).send(toWriteResponse(node, version));
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
if (isNotFoundError(error)) {
|
|
344
|
+
return reply.code(404).send({ message: NOT_FOUND_MESSAGE });
|
|
345
|
+
}
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
app.get("/api/v1/content/:id/children", {
|
|
350
|
+
onRequest: requireApiKey,
|
|
351
|
+
schema: {
|
|
352
|
+
params: PublicContentParamsSchema,
|
|
353
|
+
querystring: PublicContentListQuerySchema,
|
|
354
|
+
response: {
|
|
355
|
+
200: PublicContentListResponseSchema,
|
|
356
|
+
401: ApiErrorSchema,
|
|
357
|
+
404: ApiErrorSchema,
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
}, async (request, reply) => {
|
|
361
|
+
const parent = await adapter.getContentNode(request.params.id);
|
|
362
|
+
if (!parent || parent.publishedAt === null) {
|
|
363
|
+
return reply.code(404).send({ message: NOT_FOUND_MESSAGE });
|
|
364
|
+
}
|
|
365
|
+
const { cursor, limit } = request.query;
|
|
366
|
+
const items = await adapter.getChildren(parent.id, { cursor, limit, publishedOnly: true });
|
|
367
|
+
const nextCursor = items.length === limit ? (items[items.length - 1]?.id ?? null) : null;
|
|
368
|
+
return { items: toSummaries(items), nextCursor };
|
|
369
|
+
});
|
|
370
|
+
app.get("/api/v1/content/:id", {
|
|
371
|
+
onRequest: requireApiKey,
|
|
372
|
+
schema: {
|
|
373
|
+
params: PublicContentParamsSchema,
|
|
374
|
+
querystring: PublicContentItemQuerySchema,
|
|
375
|
+
response: {
|
|
376
|
+
200: z.union([PublicContentItemSchema, PublicContentFormatResponseSchema]),
|
|
377
|
+
400: ApiErrorSchema,
|
|
378
|
+
401: ApiErrorSchema,
|
|
379
|
+
404: ApiErrorSchema,
|
|
380
|
+
500: ApiErrorSchema,
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
}, async (request, reply) => {
|
|
384
|
+
// Verified ONCE, up front — see the by-path route's identical
|
|
385
|
+
// comment. `getContentNode` never filters by publish state, so
|
|
386
|
+
// unlike by-path this anchor fetch was never the bug; the
|
|
387
|
+
// verify-once shape is kept identical across both routes anyway.
|
|
388
|
+
const previewContentId = request.query.previewToken
|
|
389
|
+
? (verifyPreviewToken(request.query.previewToken)?.contentId ?? null)
|
|
390
|
+
: null;
|
|
391
|
+
const anchor = await adapter.getContentNode(request.params.id);
|
|
392
|
+
return sendPublishedItem(request, reply, anchor, request.query, previewContentId);
|
|
393
|
+
});
|
|
394
|
+
app.get("/api/v1/media", {
|
|
395
|
+
onRequest: requireApiKey,
|
|
396
|
+
schema: {
|
|
397
|
+
querystring: PublicMediaListQuerySchema,
|
|
398
|
+
response: { 200: PublicMediaListResponseSchema, 401: ApiErrorSchema },
|
|
399
|
+
},
|
|
400
|
+
}, async (request) => {
|
|
401
|
+
const { cursor, limit, search } = request.query;
|
|
402
|
+
const files = await mediaService.list({ cursor, limit, search });
|
|
403
|
+
const items = await Promise.all(files.map((file) => toPublicMediaFile(mediaService, file)));
|
|
404
|
+
const nextCursor = files.length === limit ? (files[files.length - 1]?.id ?? null) : null;
|
|
405
|
+
return { items, nextCursor };
|
|
406
|
+
});
|
|
407
|
+
app.get("/api/v1/media/:id", {
|
|
408
|
+
onRequest: requireApiKey,
|
|
409
|
+
schema: {
|
|
410
|
+
params: PublicContentParamsSchema,
|
|
411
|
+
response: { 200: PublicMediaFileSchema, 401: ApiErrorSchema, 404: ApiErrorSchema },
|
|
412
|
+
},
|
|
413
|
+
}, async (request, reply) => {
|
|
414
|
+
const media = await mediaService.getById(request.params.id);
|
|
415
|
+
if (!media) {
|
|
416
|
+
return reply.code(404).send({ message: `No media file found for id ${request.params.id}` });
|
|
417
|
+
}
|
|
418
|
+
return toPublicMediaFile(mediaService, media);
|
|
419
|
+
});
|
|
420
|
+
// Public document type introspection + template listing (Session 105).
|
|
421
|
+
// Both are read-only projections over existing backoffice adapter calls
|
|
422
|
+
// (document-types.ts / templates.ts) — no new adapter methods.
|
|
423
|
+
app.get("/api/v1/document-types", {
|
|
424
|
+
onRequest: requireApiKey,
|
|
425
|
+
schema: {
|
|
426
|
+
querystring: PublicDocumentTypeListQuerySchema,
|
|
427
|
+
response: { 200: PublicDocumentTypeListResponseSchema, 401: ApiErrorSchema },
|
|
428
|
+
},
|
|
429
|
+
}, async (request) => {
|
|
430
|
+
const { cursor, limit } = request.query;
|
|
431
|
+
const items = await adapter.listDocumentTypes({ cursor, limit });
|
|
432
|
+
const nextCursor = items.length === limit ? (items[items.length - 1]?.id ?? null) : null;
|
|
433
|
+
return { items: items.map(toPublicDocumentType), nextCursor };
|
|
434
|
+
});
|
|
435
|
+
app.get("/api/v1/document-types/:id", {
|
|
436
|
+
onRequest: requireApiKey,
|
|
437
|
+
schema: {
|
|
438
|
+
params: PublicContentParamsSchema,
|
|
439
|
+
response: { 200: PublicDocumentTypeSchema, 401: ApiErrorSchema, 404: ApiErrorSchema },
|
|
440
|
+
},
|
|
441
|
+
}, async (request, reply) => {
|
|
442
|
+
const doc = await adapter.getDocumentType(request.params.id);
|
|
443
|
+
if (!doc) {
|
|
444
|
+
return reply.code(404).send({ message: `No document type found for id ${request.params.id}` });
|
|
445
|
+
}
|
|
446
|
+
return toPublicDocumentType(doc);
|
|
447
|
+
});
|
|
448
|
+
app.get("/api/v1/templates", {
|
|
449
|
+
onRequest: requireApiKey,
|
|
450
|
+
schema: {
|
|
451
|
+
querystring: PublicTemplateListQuerySchema,
|
|
452
|
+
response: { 200: PublicTemplateListResponseSchema, 401: ApiErrorSchema },
|
|
453
|
+
},
|
|
454
|
+
}, async (request) => {
|
|
455
|
+
const { cursor, limit, documentTypeId } = request.query;
|
|
456
|
+
const items = await adapter.listTemplates({ cursor, limit, documentTypeId });
|
|
457
|
+
const nextCursor = items.length === limit ? (items[items.length - 1]?.id ?? null) : null;
|
|
458
|
+
return { items, nextCursor };
|
|
459
|
+
});
|
|
460
|
+
// GraphQL surface (Session 113, Task 2) — same public-content namespace,
|
|
461
|
+
// a second protocol over the identical published-item pipeline as the
|
|
462
|
+
// REST routes above. Nested scope so the key-gate hook below only
|
|
463
|
+
// touches this plugin's own routes, never the REST ones (which already
|
|
464
|
+
// gate per-route via onRequest: requireApiKey).
|
|
465
|
+
await app.register(async (gql) => {
|
|
466
|
+
// Key-gate ONLY the data endpoint: GraphiQL's HTML/asset routes ride
|
|
467
|
+
// the docs flag alone (its queries still need a key like any caller,
|
|
468
|
+
// same as swagger-ui showing schema shape without exposing data).
|
|
469
|
+
// mercurius' graphiql page lives at /api/v1/graphiql (and
|
|
470
|
+
// /api/v1/graphiql/*), which never matches this startsWith check.
|
|
471
|
+
gql.addHook("onRequest", async (request, reply) => {
|
|
472
|
+
if (request.url.startsWith("/api/v1/graphql")) {
|
|
473
|
+
await requireApiKey(request, reply);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
// Dual-package hazard: this file is loaded as ESM, so `import {
|
|
477
|
+
// NoSchemaIntrospectionCustomRule } from "graphql"` resolves the ESM
|
|
478
|
+
// build — a DIFFERENT module instance than the CJS `graphql` mercurius
|
|
479
|
+
// (itself CJS) requires internally to validate queries. A rule object
|
|
480
|
+
// built from the ESM instance fails `instanceof`/identity checks
|
|
481
|
+
// inside mercurius' CJS graphql-js, so instead of a clean
|
|
482
|
+
// introspection-disabled rejection every query 500s with "Cannot use
|
|
483
|
+
// GraphQLNonNull ... from another module or realm ... Duplicate
|
|
484
|
+
// "graphql" modules". createRequire forces Node's CJS resolver, which
|
|
485
|
+
// hits the very same `graphql/index.js` mercurius itself requires —
|
|
486
|
+
// one instance, no realm mismatch.
|
|
487
|
+
const cjsGraphql = createRequire(import.meta.url)("graphql");
|
|
488
|
+
const { GraphQLError, NoSchemaIntrospectionCustomRule } = cjsGraphql;
|
|
489
|
+
// field-count-rule.ts's rule reports via `context.reportError(new
|
|
490
|
+
// GraphQLErrorCtor(...))` — the GraphQLError class it's given here is
|
|
491
|
+
// the SAME createRequire'd one NoSchemaIntrospectionCustomRule above
|
|
492
|
+
// is already built from, so every error either rule reports lives in
|
|
493
|
+
// the one true realm mercurius' CJS graphql-js instance validates
|
|
494
|
+
// against (see field-count-rule.ts's realm note for the full
|
|
495
|
+
// reasoning; S113 is the original incident this guards against).
|
|
496
|
+
const fieldCountRule = createFieldCountRule(maxQueryComplexity(), GraphQLError);
|
|
497
|
+
// mercurius registers its GraphiQL page (/api/v1/graphiql,
|
|
498
|
+
// .../main.js, .../sw.js, .../config.js) as plain, schema-less Fastify
|
|
499
|
+
// routes. Without an explicit `hide`, fastify-swagger picks each one
|
|
500
|
+
// up as a bare, undocumented path entry — and because this plugin
|
|
501
|
+
// scope is nested under BOTH the internal (/api/docs) and public
|
|
502
|
+
// (/api/v1/docs) Swagger surfaces, they leak into both specs. Hide
|
|
503
|
+
// them before mercurius registers those routes.
|
|
504
|
+
gql.addHook("onRoute", (routeOptions) => {
|
|
505
|
+
if (routeOptions.url.startsWith("/api/v1/graphiql")) {
|
|
506
|
+
routeOptions.schema = { ...routeOptions.schema, hide: true };
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
await gql.register(mercurius, {
|
|
510
|
+
schema: buildGraphqlSchema(),
|
|
511
|
+
prefix: "/api/v1",
|
|
512
|
+
path: "/graphql",
|
|
513
|
+
// mercurius 16's graphiql option has no per-path override — its
|
|
514
|
+
// GraphiQL page is always served at the plugin's own `/graphiql`,
|
|
515
|
+
// so `prefix` above is what nests it under /api/v1 alongside the
|
|
516
|
+
// data endpoint. Read at registration (boot) time, same convention
|
|
517
|
+
// as the /api/v1/docs Swagger UI gate in app.ts.
|
|
518
|
+
graphiql: isPublicDocsEnabled() ? { enabled: true } : false,
|
|
519
|
+
queryDepth: maxQueryDepth(),
|
|
520
|
+
// Session 119, Task 2: array-form validationRules (unlike the S115
|
|
521
|
+
// function-form workaround graphql-query-complexity required) keeps
|
|
522
|
+
// mercurius' query-plan cache on — the S118 baseline measured the
|
|
523
|
+
// function-form/`cache: false` alternative at ~10ms higher p50 and
|
|
524
|
+
// 23% lower throughput on GraphQL item reads.
|
|
525
|
+
validationRules: [
|
|
526
|
+
...(isPublicDocsEnabled() ? [] : [NoSchemaIntrospectionCustomRule]),
|
|
527
|
+
fieldCountRule,
|
|
528
|
+
],
|
|
529
|
+
additionalRouteOptions: {
|
|
530
|
+
validatorCompiler: mercuriusValidatorCompiler,
|
|
531
|
+
serializerCompiler: mercuriusSerializerCompiler,
|
|
532
|
+
},
|
|
533
|
+
context: async (request) => ({
|
|
534
|
+
deps: {
|
|
535
|
+
adapter,
|
|
536
|
+
listSerializerContributions: () => app.pluginRegistry.listActiveSerializerContributions(),
|
|
537
|
+
warn: (obj, msg) => request.log.warn(obj, msg),
|
|
538
|
+
},
|
|
539
|
+
contentService,
|
|
540
|
+
mediaService,
|
|
541
|
+
getContentNode: (id) => adapter.getContentNode(id),
|
|
542
|
+
}),
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { DatabaseAdapter } from "@velora-cms/db-adapter";
|
|
2
|
+
import { type ContentService } from "@velora-cms/content-service";
|
|
3
|
+
import type { ContentNode, DocumentTypeDefinition, MediaFile, PublicContentNodeSummary, PublicDocumentType, PublicMediaFile } from "@velora-cms/api-schemas";
|
|
4
|
+
import type { SerializerContext, SerializerContribution } from "@velora-cms/plugin-sdk/manifest";
|
|
5
|
+
import type { MediaService } from "../media/media-service.js";
|
|
6
|
+
export declare function isNotFoundError(error: unknown): error is Error;
|
|
7
|
+
export declare function resolveItemNode(deps: {
|
|
8
|
+
adapter: DatabaseAdapter;
|
|
9
|
+
contentService: ContentService;
|
|
10
|
+
}, anchor: ContentNode | null, query: {
|
|
11
|
+
locale?: string;
|
|
12
|
+
fallback?: string;
|
|
13
|
+
}, options?: {
|
|
14
|
+
previewContentId?: string | null;
|
|
15
|
+
}): Promise<ContentNode | null>;
|
|
16
|
+
export declare function resolveSubtreeFilter(adapter: DatabaseAdapter, underId: string): Promise<{
|
|
17
|
+
pathPrefix: string;
|
|
18
|
+
} | null>;
|
|
19
|
+
export declare function toSummaries(nodes: ContentNode[]): PublicContentNodeSummary[];
|
|
20
|
+
export declare function toPublicDocumentType(doc: DocumentTypeDefinition): PublicDocumentType;
|
|
21
|
+
export declare function toPublicMediaFile(mediaService: MediaService, media: MediaFile): Promise<PublicMediaFile>;
|
|
22
|
+
export type PublishedItemDeps = {
|
|
23
|
+
adapter: DatabaseAdapter;
|
|
24
|
+
listSerializerContributions: () => Array<{
|
|
25
|
+
pluginId: string;
|
|
26
|
+
serializers: SerializerContribution;
|
|
27
|
+
}>;
|
|
28
|
+
warn: (obj: Record<string, unknown>, msg: string) => void;
|
|
29
|
+
};
|
|
30
|
+
export type PreparedPublishedItem = {
|
|
31
|
+
status: "notFound";
|
|
32
|
+
} | {
|
|
33
|
+
status: "brokenVersion";
|
|
34
|
+
message: string;
|
|
35
|
+
} | {
|
|
36
|
+
status: "ok";
|
|
37
|
+
core: {
|
|
38
|
+
id: string;
|
|
39
|
+
documentTypeId: string;
|
|
40
|
+
locale: string;
|
|
41
|
+
publishedAt: string | null;
|
|
42
|
+
version: number;
|
|
43
|
+
};
|
|
44
|
+
data: Record<string, unknown>;
|
|
45
|
+
contributions: Array<{
|
|
46
|
+
pluginId: string;
|
|
47
|
+
serializers: SerializerContribution;
|
|
48
|
+
}>;
|
|
49
|
+
buildSerializerContext: () => SerializerContext;
|
|
50
|
+
};
|
|
51
|
+
export type PreparePublishedItemOptions = {
|
|
52
|
+
previewContentId?: string | null;
|
|
53
|
+
};
|
|
54
|
+
export declare function preparePublishedItem(deps: PublishedItemDeps, anchor: ContentNode | null, options?: PreparePublishedItemOptions): Promise<PreparedPublishedItem>;
|
|
55
|
+
export declare function computeExtensions(prepared: Extract<PreparedPublishedItem, {
|
|
56
|
+
status: "ok";
|
|
57
|
+
}>, warn: PublishedItemDeps["warn"]): Promise<Record<string, Record<string, unknown>> | undefined>;
|