qarpeo 0.0.0-alpha.10
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 +21 -0
- package/README.md +102 -0
- package/dist/chunks/auth-schema.mjs +219 -0
- package/dist/chunks/wrangler.mjs +783 -0
- package/dist/cli/index.mjs +1085 -0
- package/dist/dev/env.d.ts +12 -0
- package/dist/dev/wrangler.d.ts +18 -0
- package/dist/module.d.ts +16 -0
- package/dist/module.mjs +4436 -0
- package/dist/runtime/ai.d.ts +25 -0
- package/dist/runtime/ai.mjs +106 -0
- package/dist/runtime/analytics.d.ts +24 -0
- package/dist/runtime/analytics.mjs +38 -0
- package/dist/runtime/app/components/BlobImage.vue +100 -0
- package/dist/runtime/app/components/CMSBlocks.vue +93 -0
- package/dist/runtime/app/components/CMSContent.vue +23 -0
- package/dist/runtime/app/components/CMSImage.vue +49 -0
- package/dist/runtime/app/components/CMSLocaleSwitcher.vue +110 -0
- package/dist/runtime/app/components/CMSPreviewBanner.vue +79 -0
- package/dist/runtime/app/components/CMSVersionBanner.vue +49 -0
- package/dist/runtime/app/components/CMSVersionSwitcher.vue +126 -0
- package/dist/runtime/app/components/ContentRenderer.vue +80 -0
- package/dist/runtime/app/components/FileUpload.vue +196 -0
- package/dist/runtime/app/components/ImpersonationBanner.vue +57 -0
- package/dist/runtime/app/components/SignedIn.vue +13 -0
- package/dist/runtime/app/components/SignedOut.vue +13 -0
- package/dist/runtime/app/components/shiki.css +34 -0
- package/dist/runtime/app/composables/analytics.d.ts +125 -0
- package/dist/runtime/app/composables/analytics.mjs +81 -0
- package/dist/runtime/app/composables/api-tokens.d.ts +49 -0
- package/dist/runtime/app/composables/api-tokens.mjs +61 -0
- package/dist/runtime/app/composables/auth.d.ts +50 -0
- package/dist/runtime/app/composables/auth.mjs +101 -0
- package/dist/runtime/app/composables/chat.d.ts +69 -0
- package/dist/runtime/app/composables/chat.mjs +159 -0
- package/dist/runtime/app/composables/cms-preview.d.ts +33 -0
- package/dist/runtime/app/composables/cms-preview.mjs +83 -0
- package/dist/runtime/app/composables/cms.d.ts +307 -0
- package/dist/runtime/app/composables/cms.mjs +355 -0
- package/dist/runtime/app/composables/docs-version.d.ts +35 -0
- package/dist/runtime/app/composables/docs-version.mjs +70 -0
- package/dist/runtime/app/composables/impersonation.d.ts +14 -0
- package/dist/runtime/app/composables/impersonation.mjs +44 -0
- package/dist/runtime/app/composables/notifications.d.ts +35 -0
- package/dist/runtime/app/composables/notifications.mjs +139 -0
- package/dist/runtime/app/composables/organization.d.ts +127 -0
- package/dist/runtime/app/composables/organization.mjs +138 -0
- package/dist/runtime/app/composables/passkey.d.ts +6 -0
- package/dist/runtime/app/composables/passkey.mjs +105 -0
- package/dist/runtime/app/composables/passwordless.d.ts +7 -0
- package/dist/runtime/app/composables/passwordless.mjs +67 -0
- package/dist/runtime/app/composables/payments.d.ts +43 -0
- package/dist/runtime/app/composables/payments.mjs +74 -0
- package/dist/runtime/app/composables/realtime.d.ts +96 -0
- package/dist/runtime/app/composables/realtime.mjs +432 -0
- package/dist/runtime/app/composables/site.d.ts +35 -0
- package/dist/runtime/app/composables/site.mjs +36 -0
- package/dist/runtime/app/composables/storage.d.ts +60 -0
- package/dist/runtime/app/composables/storage.mjs +294 -0
- package/dist/runtime/app/composables/tenant.d.ts +82 -0
- package/dist/runtime/app/composables/tenant.mjs +120 -0
- package/dist/runtime/app/composables/turnstile.d.ts +3 -0
- package/dist/runtime/app/composables/turnstile.mjs +10 -0
- package/dist/runtime/app/composables/two-factor.d.ts +12 -0
- package/dist/runtime/app/composables/two-factor.mjs +65 -0
- package/dist/runtime/app/middleware/auth.global.d.ts +2 -0
- package/dist/runtime/app/middleware/auth.global.mjs +44 -0
- package/dist/runtime/app/pages/cms.vue +2249 -0
- package/dist/runtime/app/pages/devtools/agents.vue +717 -0
- package/dist/runtime/app/pages/devtools/analytics.vue +618 -0
- package/dist/runtime/app/pages/devtools/auth.vue +351 -0
- package/dist/runtime/app/pages/devtools/blob.vue +745 -0
- package/dist/runtime/app/pages/devtools/db.vue +546 -0
- package/dist/runtime/app/pages/devtools/domains.vue +772 -0
- package/dist/runtime/app/pages/devtools/kv.vue +829 -0
- package/dist/runtime/app/pages/devtools/logs.vue +655 -0
- package/dist/runtime/app/pages/devtools/mail.vue +389 -0
- package/dist/runtime/app/pages/devtools/overview.vue +390 -0
- package/dist/runtime/app/pages/devtools/settings.vue +560 -0
- package/dist/runtime/app/pages/devtools.vue +165 -0
- package/dist/runtime/app/plugins/seo.d.ts +2 -0
- package/dist/runtime/app/plugins/seo.mjs +105 -0
- package/dist/runtime/app/plugins/shortcuts.client.d.ts +6 -0
- package/dist/runtime/app/plugins/shortcuts.client.mjs +19 -0
- package/dist/runtime/app/public/sw-notifications.mjs +52 -0
- package/dist/runtime/auth.d.ts +25 -0
- package/dist/runtime/auth.mjs +11 -0
- package/dist/runtime/bindings.d.ts +27 -0
- package/dist/runtime/bindings.mjs +38 -0
- package/dist/runtime/blob.d.ts +26 -0
- package/dist/runtime/blob.mjs +334 -0
- package/dist/runtime/bots.d.ts +1 -0
- package/dist/runtime/bots.mjs +1 -0
- package/dist/runtime/browser.d.ts +21 -0
- package/dist/runtime/browser.mjs +64 -0
- package/dist/runtime/cache.d.ts +19 -0
- package/dist/runtime/cache.mjs +12 -0
- package/dist/runtime/cms.d.ts +2 -0
- package/dist/runtime/cms.mjs +0 -0
- package/dist/runtime/db.d.ts +21 -0
- package/dist/runtime/db.mjs +36 -0
- package/dist/runtime/drizzle.d.ts +11 -0
- package/dist/runtime/drizzle.mjs +18 -0
- package/dist/runtime/email.d.ts +51 -0
- package/dist/runtime/email.mjs +245 -0
- package/dist/runtime/hyperdrive.d.ts +21 -0
- package/dist/runtime/hyperdrive.mjs +49 -0
- package/dist/runtime/index.d.ts +23 -0
- package/dist/runtime/index.mjs +23 -0
- package/dist/runtime/kv.d.ts +16 -0
- package/dist/runtime/kv.mjs +126 -0
- package/dist/runtime/payments.d.ts +2 -0
- package/dist/runtime/payments.mjs +0 -0
- package/dist/runtime/queue.d.ts +21 -0
- package/dist/runtime/queue.mjs +41 -0
- package/dist/runtime/rag.d.ts +1 -0
- package/dist/runtime/rag.mjs +1 -0
- package/dist/runtime/ratelimit.d.ts +21 -0
- package/dist/runtime/ratelimit.mjs +38 -0
- package/dist/runtime/realtime.d.ts +6 -0
- package/dist/runtime/realtime.mjs +13 -0
- package/dist/runtime/server/db/cms-schema.d.ts +651 -0
- package/dist/runtime/server/db/cms-schema.mjs +74 -0
- package/dist/runtime/server/durable-objects/agent.d.ts +14 -0
- package/dist/runtime/server/durable-objects/agent.mjs +571 -0
- package/dist/runtime/server/durable-objects/room.d.ts +34 -0
- package/dist/runtime/server/durable-objects/room.mjs +514 -0
- package/dist/runtime/server/middleware/auth.d.ts +2 -0
- package/dist/runtime/server/middleware/auth.mjs +108 -0
- package/dist/runtime/server/middleware/devtools.d.ts +2 -0
- package/dist/runtime/server/middleware/devtools.mjs +61 -0
- package/dist/runtime/server/middleware/ratelimit.d.ts +2 -0
- package/dist/runtime/server/middleware/ratelimit.mjs +49 -0
- package/dist/runtime/server/middleware/tenancy.d.ts +6 -0
- package/dist/runtime/server/middleware/tenancy.mjs +15 -0
- package/dist/runtime/server/middleware/tracker.d.ts +7 -0
- package/dist/runtime/server/middleware/tracker.mjs +9 -0
- package/dist/runtime/server/plugins/agents.d.ts +7 -0
- package/dist/runtime/server/plugins/agents.mjs +3 -0
- package/dist/runtime/server/plugins/cms.d.ts +7 -0
- package/dist/runtime/server/plugins/cms.mjs +16 -0
- package/dist/runtime/server/plugins/dev-logs.d.ts +1 -0
- package/dist/runtime/server/plugins/dev-logs.mjs +58 -0
- package/dist/runtime/server/plugins/dev-proxy.d.ts +3 -0
- package/dist/runtime/server/plugins/dev-proxy.mjs +21 -0
- package/dist/runtime/server/plugins/env.d.ts +6 -0
- package/dist/runtime/server/plugins/env.mjs +17 -0
- package/dist/runtime/server/plugins/queue.d.ts +6 -0
- package/dist/runtime/server/plugins/queue.mjs +8 -0
- package/dist/runtime/server/routes/__qarpeo/agents/chat.post.d.ts +20 -0
- package/dist/runtime/server/routes/__qarpeo/agents/chat.post.mjs +110 -0
- package/dist/runtime/server/routes/__qarpeo/agents/clear.post.d.ts +7 -0
- package/dist/runtime/server/routes/__qarpeo/agents/clear.post.mjs +15 -0
- package/dist/runtime/server/routes/__qarpeo/agents/create.post.d.ts +17 -0
- package/dist/runtime/server/routes/__qarpeo/agents/create.post.mjs +47 -0
- package/dist/runtime/server/routes/__qarpeo/agents/history.get.d.ts +15 -0
- package/dist/runtime/server/routes/__qarpeo/agents/history.get.mjs +24 -0
- package/dist/runtime/server/routes/__qarpeo/agents/list.get.d.ts +5 -0
- package/dist/runtime/server/routes/__qarpeo/agents/list.get.mjs +63 -0
- package/dist/runtime/server/routes/__qarpeo/analytics/events.get.d.ts +64 -0
- package/dist/runtime/server/routes/__qarpeo/analytics/events.get.mjs +55 -0
- package/dist/runtime/server/routes/__qarpeo/analytics/summary.get.d.ts +30 -0
- package/dist/runtime/server/routes/__qarpeo/analytics/summary.get.mjs +73 -0
- package/dist/runtime/server/routes/__qarpeo/auth/revoke-session.post.d.ts +6 -0
- package/dist/runtime/server/routes/__qarpeo/auth/revoke-session.post.mjs +26 -0
- package/dist/runtime/server/routes/__qarpeo/auth/sessions.get.d.ts +6 -0
- package/dist/runtime/server/routes/__qarpeo/auth/sessions.get.mjs +46 -0
- package/dist/runtime/server/routes/__qarpeo/auth/users.get.d.ts +7 -0
- package/dist/runtime/server/routes/__qarpeo/auth/users.get.mjs +65 -0
- package/dist/runtime/server/routes/__qarpeo/blob/files.get.d.ts +22 -0
- package/dist/runtime/server/routes/__qarpeo/blob/files.get.mjs +36 -0
- package/dist/runtime/server/routes/__qarpeo/db/query.post.d.ts +23 -0
- package/dist/runtime/server/routes/__qarpeo/db/query.post.mjs +34 -0
- package/dist/runtime/server/routes/__qarpeo/db/tables.get.d.ts +31 -0
- package/dist/runtime/server/routes/__qarpeo/db/tables.get.mjs +81 -0
- package/dist/runtime/server/routes/__qarpeo/kv/delete.post.d.ts +11 -0
- package/dist/runtime/server/routes/__qarpeo/kv/delete.post.mjs +18 -0
- package/dist/runtime/server/routes/__qarpeo/kv/get.get.d.ts +11 -0
- package/dist/runtime/server/routes/__qarpeo/kv/get.get.mjs +20 -0
- package/dist/runtime/server/routes/__qarpeo/kv/keys.get.d.ts +8 -0
- package/dist/runtime/server/routes/__qarpeo/kv/keys.get.mjs +27 -0
- package/dist/runtime/server/routes/__qarpeo/kv/set.post.d.ts +14 -0
- package/dist/runtime/server/routes/__qarpeo/kv/set.post.mjs +24 -0
- package/dist/runtime/server/routes/__qarpeo/logs.delete.d.ts +5 -0
- package/dist/runtime/server/routes/__qarpeo/logs.delete.mjs +6 -0
- package/dist/runtime/server/routes/__qarpeo/logs.get.d.ts +5 -0
- package/dist/runtime/server/routes/__qarpeo/logs.get.mjs +26 -0
- package/dist/runtime/server/routes/__qarpeo/logs.post.d.ts +6 -0
- package/dist/runtime/server/routes/__qarpeo/logs.post.mjs +17 -0
- package/dist/runtime/server/routes/__qarpeo/mail/bounce.post.d.ts +5 -0
- package/dist/runtime/server/routes/__qarpeo/mail/bounce.post.mjs +37 -0
- package/dist/runtime/server/routes/__qarpeo/mail/clear.post.d.ts +4 -0
- package/dist/runtime/server/routes/__qarpeo/mail/clear.post.mjs +6 -0
- package/dist/runtime/server/routes/__qarpeo/mail/inbox.get.d.ts +5 -0
- package/dist/runtime/server/routes/__qarpeo/mail/inbox.get.mjs +7 -0
- package/dist/runtime/server/routes/__qarpeo/mail/send-test.post.d.ts +4 -0
- package/dist/runtime/server/routes/__qarpeo/mail/send-test.post.mjs +29 -0
- package/dist/runtime/server/routes/__qarpeo/mail/track/click/[id].get.d.ts +2 -0
- package/dist/runtime/server/routes/__qarpeo/mail/track/click/[id].get.mjs +23 -0
- package/dist/runtime/server/routes/__qarpeo/mail/track/open/[id].get.d.ts +2 -0
- package/dist/runtime/server/routes/__qarpeo/mail/track/open/[id].get.mjs +31 -0
- package/dist/runtime/server/routes/__qarpeo/overview/health.get.d.ts +8 -0
- package/dist/runtime/server/routes/__qarpeo/overview/health.get.mjs +46 -0
- package/dist/runtime/server/routes/__qarpeo/overview.get.d.ts +32 -0
- package/dist/runtime/server/routes/__qarpeo/overview.get.mjs +38 -0
- package/dist/runtime/server/routes/__qarpeo/settings/reset-dev-data.post.d.ts +11 -0
- package/dist/runtime/server/routes/__qarpeo/settings/reset-dev-data.post.mjs +84 -0
- package/dist/runtime/server/routes/__qarpeo/settings.get.d.ts +124 -0
- package/dist/runtime/server/routes/__qarpeo/settings.get.mjs +145 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/create.post.d.ts +7 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/create.post.mjs +26 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/domain-add.post.d.ts +6 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/domain-add.post.mjs +29 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/domain-delete.post.d.ts +7 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/domain-delete.post.mjs +21 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/domain-verify.post.d.ts +9 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/domain-verify.post.mjs +21 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/list.get.d.ts +15 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/list.get.mjs +22 -0
- package/dist/runtime/server/routes/agents/[agent]/chat.post.d.ts +2 -0
- package/dist/runtime/server/routes/agents/[agent]/chat.post.mjs +35 -0
- package/dist/runtime/server/routes/agents/[agent]/clear.post.d.ts +4 -0
- package/dist/runtime/server/routes/agents/[agent]/clear.post.mjs +27 -0
- package/dist/runtime/server/routes/agents/[agent]/history.get.d.ts +4 -0
- package/dist/runtime/server/routes/agents/[agent]/history.get.mjs +27 -0
- package/dist/runtime/server/routes/analytics/events.get.d.ts +3 -0
- package/dist/runtime/server/routes/analytics/events.get.mjs +17 -0
- package/dist/runtime/server/routes/analytics/pageviews.get.d.ts +3 -0
- package/dist/runtime/server/routes/analytics/pageviews.get.mjs +15 -0
- package/dist/runtime/server/routes/analytics/summary.get.d.ts +6 -0
- package/dist/runtime/server/routes/analytics/summary.get.mjs +15 -0
- package/dist/runtime/server/routes/atom.xml.get.d.ts +2 -0
- package/dist/runtime/server/routes/atom.xml.get.mjs +76 -0
- package/dist/runtime/server/routes/auth/2fa/disable.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/2fa/disable.post.mjs +44 -0
- package/dist/runtime/server/routes/auth/2fa/setup.post.d.ts +6 -0
- package/dist/runtime/server/routes/auth/2fa/setup.post.mjs +32 -0
- package/dist/runtime/server/routes/auth/2fa/verify.post.d.ts +6 -0
- package/dist/runtime/server/routes/auth/2fa/verify.post.mjs +92 -0
- package/dist/runtime/server/routes/auth/[provider]/callback.d.ts +1 -0
- package/dist/runtime/server/routes/auth/[provider]/callback.mjs +1 -0
- package/dist/runtime/server/routes/auth/[provider].d.ts +3 -0
- package/dist/runtime/server/routes/auth/[provider].mjs +95 -0
- package/dist/runtime/server/routes/auth/account.delete.d.ts +5 -0
- package/dist/runtime/server/routes/auth/account.delete.mjs +52 -0
- package/dist/runtime/server/routes/auth/impersonate/start.post.d.ts +5 -0
- package/dist/runtime/server/routes/auth/impersonate/start.post.mjs +14 -0
- package/dist/runtime/server/routes/auth/impersonate/stop.post.d.ts +5 -0
- package/dist/runtime/server/routes/auth/impersonate/stop.post.mjs +5 -0
- package/dist/runtime/server/routes/auth/logout-all.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/logout-all.post.mjs +16 -0
- package/dist/runtime/server/routes/auth/logout-others.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/logout-others.post.mjs +11 -0
- package/dist/runtime/server/routes/auth/logout.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/logout.post.mjs +17 -0
- package/dist/runtime/server/routes/auth/magic-link/send.post.d.ts +10 -0
- package/dist/runtime/server/routes/auth/magic-link/send.post.mjs +29 -0
- package/dist/runtime/server/routes/auth/magic-link/verify.get.d.ts +2 -0
- package/dist/runtime/server/routes/auth/magic-link/verify.get.mjs +81 -0
- package/dist/runtime/server/routes/auth/organization/create.post.d.ts +5 -0
- package/dist/runtime/server/routes/auth/organization/create.post.mjs +10 -0
- package/dist/runtime/server/routes/auth/organization/invite.post.d.ts +9 -0
- package/dist/runtime/server/routes/auth/organization/invite.post.mjs +27 -0
- package/dist/runtime/server/routes/auth/organization/join.post.d.ts +6 -0
- package/dist/runtime/server/routes/auth/organization/join.post.mjs +15 -0
- package/dist/runtime/server/routes/auth/organization/list.get.d.ts +5 -0
- package/dist/runtime/server/routes/auth/organization/list.get.mjs +7 -0
- package/dist/runtime/server/routes/auth/organization/members/[id].delete.d.ts +4 -0
- package/dist/runtime/server/routes/auth/organization/members/[id].delete.mjs +33 -0
- package/dist/runtime/server/routes/auth/organization/members/index.get.d.ts +6 -0
- package/dist/runtime/server/routes/auth/organization/members/index.get.mjs +18 -0
- package/dist/runtime/server/routes/auth/organization/mfa.post.d.ts +6 -0
- package/dist/runtime/server/routes/auth/organization/mfa.post.mjs +33 -0
- package/dist/runtime/server/routes/auth/organization/sso/[id].delete.d.ts +4 -0
- package/dist/runtime/server/routes/auth/organization/sso/[id].delete.mjs +28 -0
- package/dist/runtime/server/routes/auth/organization/sso/index.get.d.ts +2 -0
- package/dist/runtime/server/routes/auth/organization/sso/index.get.mjs +23 -0
- package/dist/runtime/server/routes/auth/organization/sso/index.post.d.ts +5 -0
- package/dist/runtime/server/routes/auth/organization/sso/index.post.mjs +84 -0
- package/dist/runtime/server/routes/auth/organization/switch.post.d.ts +6 -0
- package/dist/runtime/server/routes/auth/organization/switch.post.mjs +14 -0
- package/dist/runtime/server/routes/auth/otp/send.post.d.ts +10 -0
- package/dist/runtime/server/routes/auth/otp/send.post.mjs +29 -0
- package/dist/runtime/server/routes/auth/otp/verify.post.d.ts +6 -0
- package/dist/runtime/server/routes/auth/otp/verify.post.mjs +95 -0
- package/dist/runtime/server/routes/auth/sessions/[id].delete.d.ts +5 -0
- package/dist/runtime/server/routes/auth/sessions/[id].delete.mjs +24 -0
- package/dist/runtime/server/routes/auth/sessions.get.d.ts +2 -0
- package/dist/runtime/server/routes/auth/sessions.get.mjs +11 -0
- package/dist/runtime/server/routes/auth/sso/login.get.d.ts +2 -0
- package/dist/runtime/server/routes/auth/sso/login.get.mjs +94 -0
- package/dist/runtime/server/routes/auth/sso/oidc/callback.get.d.ts +2 -0
- package/dist/runtime/server/routes/auth/sso/oidc/callback.get.mjs +113 -0
- package/dist/runtime/server/routes/auth/sso/saml/acs.post.d.ts +2 -0
- package/dist/runtime/server/routes/auth/sso/saml/acs.post.mjs +95 -0
- package/dist/runtime/server/routes/auth/tokens/[id].delete.d.ts +4 -0
- package/dist/runtime/server/routes/auth/tokens/[id].delete.mjs +13 -0
- package/dist/runtime/server/routes/auth/tokens/create.post.d.ts +9 -0
- package/dist/runtime/server/routes/auth/tokens/create.post.mjs +17 -0
- package/dist/runtime/server/routes/auth/tokens/index.get.d.ts +9 -0
- package/dist/runtime/server/routes/auth/tokens/index.get.mjs +5 -0
- package/dist/runtime/server/routes/auth/turnstile/verify.post.d.ts +2 -0
- package/dist/runtime/server/routes/auth/turnstile/verify.post.mjs +15 -0
- package/dist/runtime/server/routes/auth/user.get.d.ts +2 -0
- package/dist/runtime/server/routes/auth/user.get.mjs +6 -0
- package/dist/runtime/server/routes/auth/webauthn/login/options.post.d.ts +2 -0
- package/dist/runtime/server/routes/auth/webauthn/login/options.post.mjs +17 -0
- package/dist/runtime/server/routes/auth/webauthn/login/verify.post.d.ts +6 -0
- package/dist/runtime/server/routes/auth/webauthn/login/verify.post.mjs +127 -0
- package/dist/runtime/server/routes/auth/webauthn/register/options.post.d.ts +2 -0
- package/dist/runtime/server/routes/auth/webauthn/register/options.post.mjs +47 -0
- package/dist/runtime/server/routes/auth/webauthn/register/verify.post.d.ts +5 -0
- package/dist/runtime/server/routes/auth/webauthn/register/verify.post.mjs +88 -0
- package/dist/runtime/server/routes/cms/[collection]/[id]/preview-token.post.d.ts +8 -0
- package/dist/runtime/server/routes/cms/[collection]/[id]/preview-token.post.mjs +27 -0
- package/dist/runtime/server/routes/cms/[collection]/[id]/publish.post.d.ts +6 -0
- package/dist/runtime/server/routes/cms/[collection]/[id]/publish.post.mjs +19 -0
- package/dist/runtime/server/routes/cms/[collection]/[id]/review.post.d.ts +6 -0
- package/dist/runtime/server/routes/cms/[collection]/[id]/review.post.mjs +33 -0
- package/dist/runtime/server/routes/cms/[collection]/[id].delete.d.ts +4 -0
- package/dist/runtime/server/routes/cms/[collection]/[id].delete.mjs +17 -0
- package/dist/runtime/server/routes/cms/[collection]/[id].put.d.ts +4 -0
- package/dist/runtime/server/routes/cms/[collection]/[id].put.mjs +18 -0
- package/dist/runtime/server/routes/cms/[collection]/[slug].get.d.ts +4 -0
- package/dist/runtime/server/routes/cms/[collection]/[slug].get.mjs +19 -0
- package/dist/runtime/server/routes/cms/[collection]/atom.xml.get.d.ts +2 -0
- package/dist/runtime/server/routes/cms/[collection]/atom.xml.get.mjs +69 -0
- package/dist/runtime/server/routes/cms/[collection]/by-path.get.d.ts +8 -0
- package/dist/runtime/server/routes/cms/[collection]/by-path.get.mjs +23 -0
- package/dist/runtime/server/routes/cms/[collection]/feed.xml.get.d.ts +2 -0
- package/dist/runtime/server/routes/cms/[collection]/feed.xml.get.mjs +65 -0
- package/dist/runtime/server/routes/cms/[collection]/index.get.d.ts +7 -0
- package/dist/runtime/server/routes/cms/[collection]/index.get.mjs +15 -0
- package/dist/runtime/server/routes/cms/[collection]/index.post.d.ts +4 -0
- package/dist/runtime/server/routes/cms/[collection]/index.post.mjs +23 -0
- package/dist/runtime/server/routes/cms/[collection]/references.get.d.ts +19 -0
- package/dist/runtime/server/routes/cms/[collection]/references.get.mjs +47 -0
- package/dist/runtime/server/routes/cms/[collection]/surroundings.get.d.ts +10 -0
- package/dist/runtime/server/routes/cms/[collection]/surroundings.get.mjs +12 -0
- package/dist/runtime/server/routes/cms/collections/[id].delete.d.ts +4 -0
- package/dist/runtime/server/routes/cms/collections/[id].delete.mjs +10 -0
- package/dist/runtime/server/routes/cms/collections/[id].put.d.ts +4 -0
- package/dist/runtime/server/routes/cms/collections/[id].put.mjs +11 -0
- package/dist/runtime/server/routes/cms/collections/index.get.d.ts +4 -0
- package/dist/runtime/server/routes/cms/collections/index.get.mjs +6 -0
- package/dist/runtime/server/routes/cms/collections/index.post.d.ts +4 -0
- package/dist/runtime/server/routes/cms/collections/index.post.mjs +19 -0
- package/dist/runtime/server/routes/cms/components/index.get.d.ts +17 -0
- package/dist/runtime/server/routes/cms/components/index.get.mjs +16 -0
- package/dist/runtime/server/routes/cms/deploy.post.d.ts +4 -0
- package/dist/runtime/server/routes/cms/deploy.post.mjs +46 -0
- package/dist/runtime/server/routes/cms/export.post.d.ts +10 -0
- package/dist/runtime/server/routes/cms/export.post.mjs +55 -0
- package/dist/runtime/server/routes/cms/navigation.get.d.ts +8 -0
- package/dist/runtime/server/routes/cms/navigation.get.mjs +36 -0
- package/dist/runtime/server/routes/cms/preview/disable.get.d.ts +9 -0
- package/dist/runtime/server/routes/cms/preview/disable.get.mjs +9 -0
- package/dist/runtime/server/routes/cms/preview/enable.get.d.ts +14 -0
- package/dist/runtime/server/routes/cms/preview/enable.get.mjs +42 -0
- package/dist/runtime/server/routes/cms/publish-scheduled.post.d.ts +11 -0
- package/dist/runtime/server/routes/cms/publish-scheduled.post.mjs +35 -0
- package/dist/runtime/server/routes/cms/search.get.d.ts +13 -0
- package/dist/runtime/server/routes/cms/search.get.mjs +137 -0
- package/dist/runtime/server/routes/cms/sync.post.d.ts +7 -0
- package/dist/runtime/server/routes/cms/sync.post.mjs +18 -0
- package/dist/runtime/server/routes/cms/webhooks/[provider].post.d.ts +150 -0
- package/dist/runtime/server/routes/cms/webhooks/[provider].post.mjs +213 -0
- package/dist/runtime/server/routes/docs/search.get.d.ts +15 -0
- package/dist/runtime/server/routes/docs/search.get.mjs +20 -0
- package/dist/runtime/server/routes/feed.xml.get.d.ts +2 -0
- package/dist/runtime/server/routes/feed.xml.get.mjs +73 -0
- package/dist/runtime/server/routes/mail/bounce.post.d.ts +14 -0
- package/dist/runtime/server/routes/mail/bounce.post.mjs +56 -0
- package/dist/runtime/server/routes/mail/inbox/[id].get.d.ts +2 -0
- package/dist/runtime/server/routes/mail/inbox/[id].get.mjs +9 -0
- package/dist/runtime/server/routes/mail/inbox.get.d.ts +4 -0
- package/dist/runtime/server/routes/mail/inbox.get.mjs +7 -0
- package/dist/runtime/server/routes/mail/suppressions/[email].delete.d.ts +11 -0
- package/dist/runtime/server/routes/mail/suppressions/[email].delete.mjs +18 -0
- package/dist/runtime/server/routes/mail/suppressions/index.get.d.ts +12 -0
- package/dist/runtime/server/routes/mail/suppressions/index.get.mjs +23 -0
- package/dist/runtime/server/routes/mail/suppressions/index.post.d.ts +10 -0
- package/dist/runtime/server/routes/mail/suppressions/index.post.mjs +23 -0
- package/dist/runtime/server/routes/mail/unsubscribe.d.ts +13 -0
- package/dist/runtime/server/routes/mail/unsubscribe.mjs +91 -0
- package/dist/runtime/server/routes/mail/webhooks.post.d.ts +12 -0
- package/dist/runtime/server/routes/mail/webhooks.post.mjs +71 -0
- package/dist/runtime/server/routes/notifications/[id]/read.post.d.ts +4 -0
- package/dist/runtime/server/routes/notifications/[id]/read.post.mjs +13 -0
- package/dist/runtime/server/routes/notifications/[id].delete.d.ts +4 -0
- package/dist/runtime/server/routes/notifications/[id].delete.mjs +13 -0
- package/dist/runtime/server/routes/notifications/[id].get.d.ts +2 -0
- package/dist/runtime/server/routes/notifications/[id].get.mjs +15 -0
- package/dist/runtime/server/routes/notifications/index.get.d.ts +6 -0
- package/dist/runtime/server/routes/notifications/index.get.mjs +18 -0
- package/dist/runtime/server/routes/notifications/push/subscribe.post.d.ts +4 -0
- package/dist/runtime/server/routes/notifications/push/subscribe.post.mjs +20 -0
- package/dist/runtime/server/routes/notifications/push/unsubscribe.post.d.ts +4 -0
- package/dist/runtime/server/routes/notifications/push/unsubscribe.post.mjs +15 -0
- package/dist/runtime/server/routes/notifications/read-all.post.d.ts +4 -0
- package/dist/runtime/server/routes/notifications/read-all.post.mjs +11 -0
- package/dist/runtime/server/routes/payments/cancel.post.d.ts +7 -0
- package/dist/runtime/server/routes/payments/cancel.post.mjs +9 -0
- package/dist/runtime/server/routes/payments/change-plan.post.d.ts +7 -0
- package/dist/runtime/server/routes/payments/change-plan.post.mjs +12 -0
- package/dist/runtime/server/routes/payments/checkout.post.d.ts +9 -0
- package/dist/runtime/server/routes/payments/checkout.post.mjs +16 -0
- package/dist/runtime/server/routes/payments/pause.post.d.ts +7 -0
- package/dist/runtime/server/routes/payments/pause.post.mjs +9 -0
- package/dist/runtime/server/routes/payments/portal.post.d.ts +5 -0
- package/dist/runtime/server/routes/payments/portal.post.mjs +9 -0
- package/dist/runtime/server/routes/payments/subscription.get.d.ts +6 -0
- package/dist/runtime/server/routes/payments/subscription.get.mjs +9 -0
- package/dist/runtime/server/routes/payments/usage.get.d.ts +12 -0
- package/dist/runtime/server/routes/payments/usage.get.mjs +18 -0
- package/dist/runtime/server/routes/payments/usage.post.d.ts +6 -0
- package/dist/runtime/server/routes/payments/usage.post.mjs +24 -0
- package/dist/runtime/server/routes/payments/webhook.post.d.ts +14 -0
- package/dist/runtime/server/routes/payments/webhook.post.mjs +21 -0
- package/dist/runtime/server/routes/realtime/broadcast.post.d.ts +4 -0
- package/dist/runtime/server/routes/realtime/broadcast.post.mjs +26 -0
- package/dist/runtime/server/routes/realtime/history.delete.d.ts +13 -0
- package/dist/runtime/server/routes/realtime/history.delete.mjs +12 -0
- package/dist/runtime/server/routes/realtime/history.get.d.ts +16 -0
- package/dist/runtime/server/routes/realtime/history.get.mjs +15 -0
- package/dist/runtime/server/routes/realtime/presence.get.d.ts +6 -0
- package/dist/runtime/server/routes/realtime/presence.get.mjs +27 -0
- package/dist/runtime/server/routes/realtime/state.get.d.ts +5 -0
- package/dist/runtime/server/routes/realtime/state.get.mjs +26 -0
- package/dist/runtime/server/routes/realtime/state.post.d.ts +6 -0
- package/dist/runtime/server/routes/realtime/state.post.mjs +37 -0
- package/dist/runtime/server/routes/realtime/ws.get.d.ts +2 -0
- package/dist/runtime/server/routes/realtime/ws.get.mjs +73 -0
- package/dist/runtime/server/routes/robots.txt.get.d.ts +2 -0
- package/dist/runtime/server/routes/robots.txt.get.mjs +41 -0
- package/dist/runtime/server/routes/rss.xml.get.d.ts +2 -0
- package/dist/runtime/server/routes/rss.xml.get.mjs +2 -0
- package/dist/runtime/server/routes/sitemap.xml.get.d.ts +2 -0
- package/dist/runtime/server/routes/sitemap.xml.get.mjs +84 -0
- package/dist/runtime/server/routes/storage/file/[...key].delete.d.ts +4 -0
- package/dist/runtime/server/routes/storage/file/[...key].delete.mjs +23 -0
- package/dist/runtime/server/routes/storage/file/[...key].get.d.ts +2 -0
- package/dist/runtime/server/routes/storage/file/[...key].get.mjs +23 -0
- package/dist/runtime/server/routes/storage/files.get.d.ts +14 -0
- package/dist/runtime/server/routes/storage/files.get.mjs +11 -0
- package/dist/runtime/server/routes/storage/presigned.post.d.ts +8 -0
- package/dist/runtime/server/routes/storage/presigned.post.mjs +39 -0
- package/dist/runtime/server/routes/storage/sign.post.d.ts +6 -0
- package/dist/runtime/server/routes/storage/sign.post.mjs +24 -0
- package/dist/runtime/server/routes/storage/upload.post.d.ts +22 -0
- package/dist/runtime/server/routes/storage/upload.post.mjs +96 -0
- package/dist/runtime/server/routes/tenants/[tenant]/domains/[domain]/index.delete.d.ts +8 -0
- package/dist/runtime/server/routes/tenants/[tenant]/domains/[domain]/index.delete.mjs +39 -0
- package/dist/runtime/server/routes/tenants/[tenant]/domains/[domain]/verify.post.d.ts +8 -0
- package/dist/runtime/server/routes/tenants/[tenant]/domains/[domain]/verify.post.mjs +39 -0
- package/dist/runtime/server/routes/tenants/[tenant]/domains/index.get.d.ts +8 -0
- package/dist/runtime/server/routes/tenants/[tenant]/domains/index.get.mjs +20 -0
- package/dist/runtime/server/routes/tenants/[tenant]/domains/index.post.d.ts +8 -0
- package/dist/runtime/server/routes/tenants/[tenant]/domains/index.post.mjs +44 -0
- package/dist/runtime/server/routes/tenants/current.get.d.ts +8 -0
- package/dist/runtime/server/routes/tenants/current.get.mjs +6 -0
- package/dist/runtime/server/tasks/cms/publish.d.ts +4 -0
- package/dist/runtime/server/tasks/cms/publish.mjs +18 -0
- package/dist/runtime/server/utils/agent-store.d.ts +24 -0
- package/dist/runtime/server/utils/agent-store.mjs +53 -0
- package/dist/runtime/server/utils/agents-registry-fallback.d.ts +1 -0
- package/dist/runtime/server/utils/agents-registry-fallback.mjs +1 -0
- package/dist/runtime/server/utils/ai.d.ts +119 -0
- package/dist/runtime/server/utils/ai.mjs +607 -0
- package/dist/runtime/server/utils/analytics-query.d.ts +109 -0
- package/dist/runtime/server/utils/analytics-query.mjs +234 -0
- package/dist/runtime/server/utils/auth.d.ts +60 -0
- package/dist/runtime/server/utils/auth.mjs +361 -0
- package/dist/runtime/server/utils/blob-store.d.ts +13 -0
- package/dist/runtime/server/utils/blob-store.mjs +60 -0
- package/dist/runtime/server/utils/bots.d.ts +21 -0
- package/dist/runtime/server/utils/bots.mjs +491 -0
- package/dist/runtime/server/utils/cache.d.ts +33 -0
- package/dist/runtime/server/utils/cache.mjs +128 -0
- package/dist/runtime/server/utils/cms-events.d.ts +17 -0
- package/dist/runtime/server/utils/cms-events.mjs +80 -0
- package/dist/runtime/server/utils/cms-filter.d.ts +3 -0
- package/dist/runtime/server/utils/cms-filter.mjs +34 -0
- package/dist/runtime/server/utils/cms.d.ts +278 -0
- package/dist/runtime/server/utils/cms.mjs +3268 -0
- package/dist/runtime/server/utils/config.d.ts +27 -0
- package/dist/runtime/server/utils/config.mjs +38 -0
- package/dist/runtime/server/utils/context.d.ts +13 -0
- package/dist/runtime/server/utils/context.mjs +98 -0
- package/dist/runtime/server/utils/crypto.d.ts +16 -0
- package/dist/runtime/server/utils/crypto.mjs +56 -0
- package/dist/runtime/server/utils/dev-logs.d.ts +21 -0
- package/dist/runtime/server/utils/dev-logs.mjs +41 -0
- package/dist/runtime/server/utils/dev-proxy.d.ts +1 -0
- package/dist/runtime/server/utils/dev-proxy.mjs +69 -0
- package/dist/runtime/server/utils/domains.d.ts +46 -0
- package/dist/runtime/server/utils/domains.mjs +318 -0
- package/dist/runtime/server/utils/drizzle.d.ts +10 -0
- package/dist/runtime/server/utils/drizzle.mjs +36 -0
- package/dist/runtime/server/utils/email-templates.d.ts +44 -0
- package/dist/runtime/server/utils/email-templates.mjs +373 -0
- package/dist/runtime/server/utils/feed.d.ts +42 -0
- package/dist/runtime/server/utils/feed.mjs +161 -0
- package/dist/runtime/server/utils/impersonation.d.ts +67 -0
- package/dist/runtime/server/utils/impersonation.mjs +298 -0
- package/dist/runtime/server/utils/jobs.d.ts +70 -0
- package/dist/runtime/server/utils/jobs.mjs +125 -0
- package/dist/runtime/server/utils/kv-store.d.ts +9 -0
- package/dist/runtime/server/utils/kv-store.mjs +32 -0
- package/dist/runtime/server/utils/mail-suppression.d.ts +62 -0
- package/dist/runtime/server/utils/mail-suppression.mjs +328 -0
- package/dist/runtime/server/utils/mail.d.ts +90 -0
- package/dist/runtime/server/utils/mail.mjs +366 -0
- package/dist/runtime/server/utils/notifications.d.ts +51 -0
- package/dist/runtime/server/utils/notifications.mjs +215 -0
- package/dist/runtime/server/utils/organization.d.ts +54 -0
- package/dist/runtime/server/utils/organization.mjs +354 -0
- package/dist/runtime/server/utils/passwordless.d.ts +48 -0
- package/dist/runtime/server/utils/passwordless.mjs +179 -0
- package/dist/runtime/server/utils/payments.d.ts +121 -0
- package/dist/runtime/server/utils/payments.mjs +1067 -0
- package/dist/runtime/server/utils/queue-fallback.d.ts +2 -0
- package/dist/runtime/server/utils/queue-fallback.mjs +2 -0
- package/dist/runtime/server/utils/queue.d.ts +4 -0
- package/dist/runtime/server/utils/queue.mjs +3 -0
- package/dist/runtime/server/utils/rag.d.ts +25 -0
- package/dist/runtime/server/utils/rag.mjs +333 -0
- package/dist/runtime/server/utils/ratelimit.d.ts +45 -0
- package/dist/runtime/server/utils/ratelimit.mjs +182 -0
- package/dist/runtime/server/utils/realtime.d.ts +110 -0
- package/dist/runtime/server/utils/realtime.mjs +197 -0
- package/dist/runtime/server/utils/schema-fallback.d.ts +3 -0
- package/dist/runtime/server/utils/schema-fallback.mjs +2 -0
- package/dist/runtime/server/utils/session.d.ts +72 -0
- package/dist/runtime/server/utils/session.mjs +287 -0
- package/dist/runtime/server/utils/sso.d.ts +49 -0
- package/dist/runtime/server/utils/sso.mjs +131 -0
- package/dist/runtime/server/utils/storage.d.ts +111 -0
- package/dist/runtime/server/utils/storage.mjs +654 -0
- package/dist/runtime/server/utils/tenant.d.ts +44 -0
- package/dist/runtime/server/utils/tenant.mjs +285 -0
- package/dist/runtime/server/utils/totp.d.ts +33 -0
- package/dist/runtime/server/utils/totp.mjs +112 -0
- package/dist/runtime/server/utils/tracker.d.ts +72 -0
- package/dist/runtime/server/utils/tracker.mjs +139 -0
- package/dist/runtime/server/utils/turnstile.d.ts +17 -0
- package/dist/runtime/server/utils/turnstile.mjs +68 -0
- package/dist/runtime/server/utils/vapid.d.ts +35 -0
- package/dist/runtime/server/utils/vapid.mjs +211 -0
- package/dist/runtime/storage.d.ts +1 -0
- package/dist/runtime/storage.mjs +0 -0
- package/dist/runtime/types/ai.d.ts +68 -0
- package/dist/runtime/types/ai.mjs +0 -0
- package/dist/runtime/types/analytics.d.ts +30 -0
- package/dist/runtime/types/analytics.mjs +0 -0
- package/dist/runtime/types/auth-globals.d.ts +14 -0
- package/dist/runtime/types/auth.d.ts +153 -0
- package/dist/runtime/types/auth.mjs +0 -0
- package/dist/runtime/types/browser.d.ts +29 -0
- package/dist/runtime/types/browser.mjs +0 -0
- package/dist/runtime/types/d1.d.ts +44 -0
- package/dist/runtime/types/d1.mjs +0 -0
- package/dist/runtime/types/drizzle.d.ts +13 -0
- package/dist/runtime/types/drizzle.mjs +0 -0
- package/dist/runtime/types/durable-object.d.ts +51 -0
- package/dist/runtime/types/durable-object.mjs +0 -0
- package/dist/runtime/types/email.d.ts +66 -0
- package/dist/runtime/types/email.mjs +0 -0
- package/dist/runtime/types/hyperdrive.d.ts +18 -0
- package/dist/runtime/types/hyperdrive.mjs +0 -0
- package/dist/runtime/types/imports.d.ts +58 -0
- package/dist/runtime/types/index.d.ts +156 -0
- package/dist/runtime/types/index.mjs +13 -0
- package/dist/runtime/types/queue.d.ts +67 -0
- package/dist/runtime/types/queue.mjs +0 -0
- package/dist/runtime/types/r2.d.ts +108 -0
- package/dist/runtime/types/r2.mjs +0 -0
- package/dist/runtime/types/ratelimit.d.ts +21 -0
- package/dist/runtime/types/ratelimit.mjs +0 -0
- package/dist/runtime/types/vectorize.d.ts +74 -0
- package/dist/runtime/types/vectorize.mjs +0 -0
- package/dist/runtime/vectorize.d.ts +21 -0
- package/dist/runtime/vectorize.mjs +53 -0
- package/dist/types.d.ts +2750 -0
- package/dist/types.mjs +1 -0
- package/dist/utils/agents.d.ts +8 -0
- package/dist/utils/auth-schema.d.ts +11 -0
- package/dist/utils/bindings.d.ts +16 -0
- package/dist/utils/cms-schema.d.ts +1 -0
- package/dist/utils/cms-sources.d.ts +62 -0
- package/dist/utils/drizzle.d.ts +42 -0
- package/dist/utils/jobs.d.ts +8 -0
- package/dist/utils/local-db.d.ts +28 -0
- package/dist/utils/notifications-schema.d.ts +15 -0
- package/dist/utils/payments-schema.d.ts +11 -0
- package/dist/utils/pkg.d.ts +15 -0
- package/dist/utils/queue.d.ts +8 -0
- package/dist/utils/tenants-schema.d.ts +11 -0
- package/package.json +104 -0
|
@@ -0,0 +1,3268 @@
|
|
|
1
|
+
import { parseMarkdown as comarkParseMarkdown } from "comark";
|
|
2
|
+
import { render as comarkRender } from "comark/render";
|
|
3
|
+
import { createError, useRuntimeConfig, getCookie } from "nuxt/server";
|
|
4
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
5
|
+
import { getD1Binding } from "../../db.mjs";
|
|
6
|
+
import { getKVBinding } from "../../kv.mjs";
|
|
7
|
+
import { fireCMSWebhooks, fireCMSDeployTrigger } from "./cms-events.mjs";
|
|
8
|
+
import { getQarpeoConfig } from "./config.mjs";
|
|
9
|
+
import { getAuthContext } from "./context.mjs";
|
|
10
|
+
function getCMSDb(event) {
|
|
11
|
+
return getD1Binding("DB", event);
|
|
12
|
+
}
|
|
13
|
+
function nanoid(size = 21) {
|
|
14
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
15
|
+
const bytes = crypto.getRandomValues(new Uint8Array(size));
|
|
16
|
+
return Array.from(bytes, (b) => chars[b % chars.length]).join("");
|
|
17
|
+
}
|
|
18
|
+
export function slugifyHeading(text) {
|
|
19
|
+
return text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/^-+|-+$/g, "");
|
|
20
|
+
}
|
|
21
|
+
export function stripOrderPrefix(segment) {
|
|
22
|
+
return segment.replace(/^\d+\./, "");
|
|
23
|
+
}
|
|
24
|
+
export function isGroupSegment(segment) {
|
|
25
|
+
const stripped = stripOrderPrefix(segment);
|
|
26
|
+
return /^\([^)]+\)$/.test(stripped);
|
|
27
|
+
}
|
|
28
|
+
export function extractGroupTitle(segment) {
|
|
29
|
+
const stripped = stripOrderPrefix(segment);
|
|
30
|
+
const inner = stripped.replace(/^\(|\)$/g, "");
|
|
31
|
+
return inner.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
32
|
+
}
|
|
33
|
+
export function extractOrderPrefix(segment) {
|
|
34
|
+
const m = segment.match(/^(\d+)\./);
|
|
35
|
+
return m ? parseInt(m[1], 10) : void 0;
|
|
36
|
+
}
|
|
37
|
+
export function computeEntryPath(filePathRelative, collectionId) {
|
|
38
|
+
const normalized = filePathRelative.replace(/\\/g, "/").replace(/\.md$/, "");
|
|
39
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
40
|
+
const cleaned = segments.filter((seg) => !isGroupSegment(seg)).map(stripOrderPrefix).filter(Boolean);
|
|
41
|
+
if (cleaned[cleaned.length - 1] === "index") {
|
|
42
|
+
cleaned.pop();
|
|
43
|
+
}
|
|
44
|
+
if (collectionId && collectionId !== "content" && cleaned[0] !== collectionId) {
|
|
45
|
+
cleaned.unshift(collectionId);
|
|
46
|
+
}
|
|
47
|
+
if (cleaned.length === 0) return "/";
|
|
48
|
+
return "/" + cleaned.join("/");
|
|
49
|
+
}
|
|
50
|
+
export function matchPathPatterns(entryPath, patterns) {
|
|
51
|
+
if (!patterns) return false;
|
|
52
|
+
const list = Array.isArray(patterns) ? patterns : [patterns];
|
|
53
|
+
if (!list.length) return false;
|
|
54
|
+
const path = ("/" + entryPath.replace(/\\/g, "/").replace(/^\/+/, "")).replace(/\/+$/, "") || "/";
|
|
55
|
+
let matched = false;
|
|
56
|
+
for (const raw of list) {
|
|
57
|
+
const isNeg = raw.startsWith("!");
|
|
58
|
+
const rawPattern = isNeg ? raw.slice(1) : raw;
|
|
59
|
+
const pattern = ("/" + rawPattern.replace(/\\/g, "/").replace(/^\/+/, "")).replace(/\/+$/, "") || "/";
|
|
60
|
+
let isMatch = false;
|
|
61
|
+
if (pattern.endsWith("/**")) {
|
|
62
|
+
const base = pattern.slice(0, -3) || "/";
|
|
63
|
+
isMatch = path === base || path.startsWith(base + "/");
|
|
64
|
+
} else if (pattern.includes("*") || pattern.includes("?")) {
|
|
65
|
+
const regexStr = "^" + pattern.replace(/[.+^${}()|[\]\\]/g, (m) => "\\" + m).replace(/\*\*/g, ".*").replace(/(?<!\.)\*/g, "[^/]*").replace(/\?/g, "[^/]") + "$";
|
|
66
|
+
isMatch = new RegExp(regexStr).test(path);
|
|
67
|
+
} else {
|
|
68
|
+
isMatch = path === pattern;
|
|
69
|
+
}
|
|
70
|
+
if (isMatch) {
|
|
71
|
+
if (isNeg) return false;
|
|
72
|
+
matched = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return matched;
|
|
76
|
+
}
|
|
77
|
+
export function resolveCollectionForPath(entryPath, collectionsConfig, fallbackId = "content") {
|
|
78
|
+
if (collectionsConfig) {
|
|
79
|
+
for (const [colId, colConfig] of Object.entries(collectionsConfig)) {
|
|
80
|
+
if (colConfig.path && matchPathPatterns(entryPath, colConfig.path)) {
|
|
81
|
+
return colId;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return fallbackId;
|
|
86
|
+
}
|
|
87
|
+
export function extractTOC(md) {
|
|
88
|
+
const lines = md.split("\n");
|
|
89
|
+
const flat = [];
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
const m = line.match(/^(#{2,6})\s+(.+)/);
|
|
92
|
+
if (!m) continue;
|
|
93
|
+
const depth = m[1].length;
|
|
94
|
+
const rawText = m[2].trim().replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[`*_~]/g, "");
|
|
95
|
+
flat.push({ id: slugifyHeading(rawText), text: rawText, depth });
|
|
96
|
+
}
|
|
97
|
+
const result = [];
|
|
98
|
+
let current2 = null;
|
|
99
|
+
for (const item of flat) {
|
|
100
|
+
if (item.depth === 2) {
|
|
101
|
+
current2 = { ...item, children: [] };
|
|
102
|
+
result.push(current2);
|
|
103
|
+
} else if (current2 && item.depth >= 3) {
|
|
104
|
+
current2.children = current2.children || [];
|
|
105
|
+
current2.children.push(item);
|
|
106
|
+
} else {
|
|
107
|
+
result.push(item);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
function parseInlineProps(propsStr) {
|
|
113
|
+
const inner = propsStr.replace(/^\{|\}$/g, "");
|
|
114
|
+
const result = {};
|
|
115
|
+
const re = /(\w+)="([^"]*)"/g;
|
|
116
|
+
let m;
|
|
117
|
+
while ((m = re.exec(inner)) !== null) {
|
|
118
|
+
result[m[1]] = m[2];
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
function escapeAttr(val) {
|
|
123
|
+
return val.replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
124
|
+
}
|
|
125
|
+
export function preprocessMDC(md) {
|
|
126
|
+
let out = md;
|
|
127
|
+
out = out.replace(
|
|
128
|
+
/:::(\w[\w-]*)(\{[^}]*\})?\s*\n([\s\S]*?):::/g,
|
|
129
|
+
(_match, type, propsStr, body) => {
|
|
130
|
+
const attrs = parseInlineProps(propsStr || "");
|
|
131
|
+
const cls = `mdc-${type}`;
|
|
132
|
+
const attrStr = Object.entries(attrs).map(([k, v]) => ` data-${k}="${escapeAttr(v)}"`).join("");
|
|
133
|
+
return `<div class="${cls}"${attrStr}>
|
|
134
|
+
${body.trim()}
|
|
135
|
+
</div>`;
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
out = out.replace(/::(\w[\w-]*)\s*\n([\s\S]*?)::/g, (_match, type, body) => {
|
|
139
|
+
return `<div class="mdc-${type}">
|
|
140
|
+
${body.trim()}
|
|
141
|
+
</div>`;
|
|
142
|
+
});
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
export function getCMSConfig(event) {
|
|
146
|
+
const rc = useRuntimeConfig(event);
|
|
147
|
+
const cfg = rc?.qarpeo?.cms || {};
|
|
148
|
+
return {
|
|
149
|
+
admin: cfg.admin !== false,
|
|
150
|
+
path: cfg.path || "/cms",
|
|
151
|
+
apiPath: cfg.apiPath || "/api/cms",
|
|
152
|
+
contentDir: cfg.contentDir || "content",
|
|
153
|
+
hybrid: cfg.hybrid !== false,
|
|
154
|
+
roles: cfg.roles || ["admin", "editor"],
|
|
155
|
+
revisions: cfg.revisions || { max: 10 },
|
|
156
|
+
cache: cfg.cache !== false,
|
|
157
|
+
collections: cfg.collections || {},
|
|
158
|
+
components: cfg.components || {},
|
|
159
|
+
sources: cfg.sources || [],
|
|
160
|
+
exclude: cfg.exclude || void 0,
|
|
161
|
+
github: cfg.github || void 0,
|
|
162
|
+
deploy: cfg.deploy || void 0,
|
|
163
|
+
webhooks: cfg.webhooks || [],
|
|
164
|
+
webhookSecret: cfg.webhookSecret || void 0,
|
|
165
|
+
approval: cfg.approval || void 0,
|
|
166
|
+
scheduledPublish: cfg.scheduledPublish ?? false,
|
|
167
|
+
shiki: cfg.shiki !== void 0 ? cfg.shiki : true,
|
|
168
|
+
plugins: cfg.plugins || {},
|
|
169
|
+
locales: cfg.locales || ["en"],
|
|
170
|
+
defaultLocale: cfg.defaultLocale || "en"
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export function resolveCollectionApproval(collectionConfig, globalApproval) {
|
|
174
|
+
const raw = collectionConfig?.approval ?? globalApproval;
|
|
175
|
+
if (!raw) return null;
|
|
176
|
+
if (typeof raw === "object") {
|
|
177
|
+
return {
|
|
178
|
+
roles: raw.roles && raw.roles.length > 0 ? raw.roles : ["admin"],
|
|
179
|
+
allowSelfApproval: raw.allowSelfApproval ?? false
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
roles: ["admin"],
|
|
184
|
+
allowSelfApproval: false
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const _cachedPluginsMap = /* @__PURE__ */ new Map();
|
|
188
|
+
async function _resolveMarkdownPlugins(event) {
|
|
189
|
+
const rc = useRuntimeConfig(event);
|
|
190
|
+
const pluginCfg = getQarpeoConfig(event).cms?.plugins || rc?.public?.qarpeo?.cms?.plugins || {};
|
|
191
|
+
const cacheKey = JSON.stringify(pluginCfg);
|
|
192
|
+
if (_cachedPluginsMap.has(cacheKey)) return _cachedPluginsMap.get(cacheKey);
|
|
193
|
+
const plugins = [];
|
|
194
|
+
if (pluginCfg.shiki !== false) {
|
|
195
|
+
try {
|
|
196
|
+
const shikiMod = await import("comark/plugins/shiki");
|
|
197
|
+
const shikiPlugin = shikiMod.default;
|
|
198
|
+
const shikiOptions = typeof pluginCfg.shiki === "object" ? pluginCfg.shiki : {};
|
|
199
|
+
plugins.push(shikiPlugin(shikiOptions));
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
_cachedPluginsMap.set(cacheKey, plugins);
|
|
204
|
+
return plugins;
|
|
205
|
+
}
|
|
206
|
+
export async function parseMarkdownAST(md, event) {
|
|
207
|
+
if (!md) return { nodes: [], frontmatter: {}, meta: {} };
|
|
208
|
+
try {
|
|
209
|
+
const plugins = await _resolveMarkdownPlugins(event);
|
|
210
|
+
return await comarkParseMarkdown(md, { plugins });
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.error("[qarpeo:cms] parseMarkdownAST failed:", err);
|
|
213
|
+
return { nodes: [], frontmatter: {}, meta: {} };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export async function processMarkdown(md, event) {
|
|
217
|
+
if (!md) return { ast: { nodes: [], frontmatter: {}, meta: {} }, html: "" };
|
|
218
|
+
try {
|
|
219
|
+
const plugins = await _resolveMarkdownPlugins(event);
|
|
220
|
+
const ast = await comarkParseMarkdown(md, { plugins });
|
|
221
|
+
const html = (await comarkRender(ast, { format: "text/html" })).trim();
|
|
222
|
+
return { ast, html };
|
|
223
|
+
} catch {
|
|
224
|
+
return { ast: { nodes: [], frontmatter: {}, meta: {} }, html: md };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export async function renderMarkdown(md, event) {
|
|
228
|
+
if (!md) return "";
|
|
229
|
+
try {
|
|
230
|
+
const res = await processMarkdown(md, event);
|
|
231
|
+
return res.html;
|
|
232
|
+
} catch {
|
|
233
|
+
return md;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
export async function renderPageMarkdown(md) {
|
|
237
|
+
return renderMarkdown(md);
|
|
238
|
+
}
|
|
239
|
+
export function parseFrontmatter(raw) {
|
|
240
|
+
const clean = raw ? raw.replace(/^\uFEFF/, "") : "";
|
|
241
|
+
const fmMatch = clean.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)/);
|
|
242
|
+
if (!fmMatch) {
|
|
243
|
+
return { data: {}, content: clean };
|
|
244
|
+
}
|
|
245
|
+
const yamlBlock = fmMatch[1] || "";
|
|
246
|
+
const body = fmMatch[2] || "";
|
|
247
|
+
let data = {};
|
|
248
|
+
try {
|
|
249
|
+
const parsed = parseYaml(yamlBlock);
|
|
250
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
251
|
+
data = parsed;
|
|
252
|
+
}
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.warn("[qarpeo:cms] Warning: failed to parse YAML frontmatter:", err);
|
|
255
|
+
}
|
|
256
|
+
return { data, content: body };
|
|
257
|
+
}
|
|
258
|
+
export function formatFrontmatter(data, body) {
|
|
259
|
+
const cleanData = {};
|
|
260
|
+
for (const [k, v] of Object.entries(data)) {
|
|
261
|
+
if (v === void 0 || v === null || k.startsWith("_")) continue;
|
|
262
|
+
cleanData[k] = v;
|
|
263
|
+
}
|
|
264
|
+
const yamlStr = Object.keys(cleanData).length > 0 ? stringifyYaml(cleanData).trim() : "";
|
|
265
|
+
if (!yamlStr) {
|
|
266
|
+
return body;
|
|
267
|
+
}
|
|
268
|
+
return `---
|
|
269
|
+
${yamlStr}
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
${body}`;
|
|
273
|
+
}
|
|
274
|
+
export function formatEntryAsMarkdown(entry) {
|
|
275
|
+
const e = entry;
|
|
276
|
+
let metadata = {};
|
|
277
|
+
try {
|
|
278
|
+
if (e.metadata && typeof e.metadata === "object") {
|
|
279
|
+
metadata = e.metadata;
|
|
280
|
+
} else if (e.metadataJson || e.metadata_json) {
|
|
281
|
+
metadata = JSON.parse(e.metadataJson || e.metadata_json);
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
const cleanMetadata = {};
|
|
286
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
287
|
+
if (!k.startsWith("_")) {
|
|
288
|
+
cleanMetadata[k] = v;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const serializedMetadata = serializeReferencesForFrontmatter(cleanMetadata);
|
|
292
|
+
const rawTags = e.tags;
|
|
293
|
+
const tags = Array.isArray(rawTags) ? rawTags : typeof rawTags === "string" && rawTags.length > 0 ? rawTags.split(",").map((t) => t.trim()) : void 0;
|
|
294
|
+
const fmData = {
|
|
295
|
+
title: e.title,
|
|
296
|
+
slug: e.slug,
|
|
297
|
+
...e.description ? { description: e.description } : {},
|
|
298
|
+
...e.coverImage || e.cover_image ? { coverImage: e.coverImage || e.cover_image } : {},
|
|
299
|
+
...tags && tags.length > 0 ? { tags } : {},
|
|
300
|
+
...typeof e.order === "number" && e.order !== 0 ? { order: e.order } : {},
|
|
301
|
+
...e.status && e.status !== "published" ? { status: e.status } : {},
|
|
302
|
+
...serializedMetadata
|
|
303
|
+
};
|
|
304
|
+
const body = e.contentMd ?? e.content_md ?? "";
|
|
305
|
+
return formatFrontmatter(fmData, body);
|
|
306
|
+
}
|
|
307
|
+
function toBase64(str) {
|
|
308
|
+
if (typeof Buffer !== "undefined") {
|
|
309
|
+
return Buffer.from(str, "utf8").toString("base64");
|
|
310
|
+
}
|
|
311
|
+
const bytes = new TextEncoder().encode(str);
|
|
312
|
+
let binary = "";
|
|
313
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
314
|
+
binary += String.fromCharCode(bytes[i]);
|
|
315
|
+
}
|
|
316
|
+
return btoa(binary);
|
|
317
|
+
}
|
|
318
|
+
export async function syncEntryToGit(event, collectionId, entry, action) {
|
|
319
|
+
const config = getCMSConfig(event);
|
|
320
|
+
const colConfig = config.collections[collectionId] || {};
|
|
321
|
+
const mode = colConfig.mode || config.mode || "db";
|
|
322
|
+
if (mode !== "git-sync") {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
const result = {};
|
|
326
|
+
const e = entry;
|
|
327
|
+
let rawFilePath = e.filePath || e.file_path;
|
|
328
|
+
if (!rawFilePath) {
|
|
329
|
+
const targetDir = colConfig.directory || `${config.contentDir}/${collectionId}`;
|
|
330
|
+
rawFilePath = `${targetDir}/${e.slug}.md`;
|
|
331
|
+
}
|
|
332
|
+
let resolvedFsPath = rawFilePath;
|
|
333
|
+
let fileContent = "";
|
|
334
|
+
try {
|
|
335
|
+
const fs = await import("node:fs/promises");
|
|
336
|
+
const pathMod = await import("node:path");
|
|
337
|
+
resolvedFsPath = pathMod.isAbsolute(rawFilePath) ? rawFilePath : pathMod.resolve(process.cwd(), rawFilePath);
|
|
338
|
+
if (action === "delete") {
|
|
339
|
+
const exists = await fs.stat(resolvedFsPath).catch(() => null);
|
|
340
|
+
if (exists) {
|
|
341
|
+
await fs.unlink(resolvedFsPath);
|
|
342
|
+
result.localFs = true;
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
fileContent = formatEntryAsMarkdown(entry);
|
|
346
|
+
await fs.mkdir(pathMod.dirname(resolvedFsPath), { recursive: true });
|
|
347
|
+
await fs.writeFile(resolvedFsPath, fileContent, "utf8");
|
|
348
|
+
result.localFs = true;
|
|
349
|
+
const contentHash = await computeContentHash(fileContent);
|
|
350
|
+
result.filePath = resolvedFsPath;
|
|
351
|
+
result.contentHash = contentHash;
|
|
352
|
+
if (e.id) {
|
|
353
|
+
const db = getCMSDb(event);
|
|
354
|
+
await db.prepare(
|
|
355
|
+
"UPDATE _qarpeo_cms_entries SET file_path = ?, content_hash = ?, source = 'file' WHERE id = ?"
|
|
356
|
+
).bind(resolvedFsPath, contentHash, e.id).run();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} catch (err) {
|
|
360
|
+
}
|
|
361
|
+
const ghConfig = {
|
|
362
|
+
...config.github || {},
|
|
363
|
+
...colConfig.github || {}
|
|
364
|
+
};
|
|
365
|
+
const repo = ghConfig.repo || (typeof process !== "undefined" ? process.env.GITHUB_REPOSITORY : void 0);
|
|
366
|
+
const token = ghConfig.token || (typeof process !== "undefined" ? process.env.GITHUB_TOKEN || process.env.GH_TOKEN : void 0);
|
|
367
|
+
if (repo && token) {
|
|
368
|
+
try {
|
|
369
|
+
const [owner, repoName] = repo.split("/");
|
|
370
|
+
if (!owner || !repoName) {
|
|
371
|
+
console.warn(`[qarpeo:cms] Invalid GitHub repo format: "${repo}". Expected "owner/repo".`);
|
|
372
|
+
} else {
|
|
373
|
+
let gitPath = resolvedFsPath.replace(/\\/g, "/");
|
|
374
|
+
const match = gitPath.match(/(?:^|[\\/])((?:content|docs)[\\/].*)$/i);
|
|
375
|
+
if (match) {
|
|
376
|
+
gitPath = match[1];
|
|
377
|
+
} else if (colConfig.directory) {
|
|
378
|
+
gitPath = `${colConfig.directory}/${entry.slug}.md`;
|
|
379
|
+
} else {
|
|
380
|
+
gitPath = `${config.contentDir}/${collectionId}/${entry.slug}.md`;
|
|
381
|
+
}
|
|
382
|
+
gitPath = gitPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
383
|
+
const baseBranch = ghConfig.branch || "main";
|
|
384
|
+
const actionVerb = action === "create" ? "create" : action === "delete" ? "delete" : "update";
|
|
385
|
+
const template = ghConfig.commitMessage || "content({collection}): {action} {slug}";
|
|
386
|
+
const commitMsg = template.replace("{collection}", collectionId).replace("{slug}", entry.slug).replace("{action}", actionVerb);
|
|
387
|
+
const headers = {
|
|
388
|
+
Accept: "application/vnd.github.v3+json",
|
|
389
|
+
Authorization: `Bearer ${token}`,
|
|
390
|
+
"User-Agent": "qarpeo-cms",
|
|
391
|
+
"Content-Type": "application/json"
|
|
392
|
+
};
|
|
393
|
+
let existingSha;
|
|
394
|
+
try {
|
|
395
|
+
const checkRes = await fetch(
|
|
396
|
+
`https://api.github.com/repos/${owner}/${repoName}/contents/${gitPath}?ref=${baseBranch}`,
|
|
397
|
+
{ headers }
|
|
398
|
+
);
|
|
399
|
+
if (checkRes.ok) {
|
|
400
|
+
const checkData = await checkRes.json();
|
|
401
|
+
existingSha = checkData.sha;
|
|
402
|
+
}
|
|
403
|
+
} catch {
|
|
404
|
+
}
|
|
405
|
+
let targetBranch = baseBranch;
|
|
406
|
+
let prUrl;
|
|
407
|
+
if (ghConfig.pr) {
|
|
408
|
+
try {
|
|
409
|
+
const refRes = await fetch(
|
|
410
|
+
`https://api.github.com/repos/${owner}/${repoName}/git/ref/heads/${ghConfig.prBase || baseBranch}`,
|
|
411
|
+
{ headers }
|
|
412
|
+
);
|
|
413
|
+
if (refRes.ok) {
|
|
414
|
+
const refData = await refRes.json();
|
|
415
|
+
const baseSha = refData.object?.sha;
|
|
416
|
+
const newBranch = `${ghConfig.branchPrefix || "content/"}${collectionId}-${entry.slug}-${Date.now().toString(36)}`;
|
|
417
|
+
const createBranchRes = await fetch(
|
|
418
|
+
`https://api.github.com/repos/${owner}/${repoName}/git/refs`,
|
|
419
|
+
{
|
|
420
|
+
method: "POST",
|
|
421
|
+
headers,
|
|
422
|
+
body: JSON.stringify({
|
|
423
|
+
ref: `refs/heads/${newBranch}`,
|
|
424
|
+
sha: baseSha
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
);
|
|
428
|
+
if (createBranchRes.ok) {
|
|
429
|
+
targetBranch = newBranch;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
let commitSha;
|
|
436
|
+
if (action === "delete") {
|
|
437
|
+
if (existingSha) {
|
|
438
|
+
const delRes = await fetch(
|
|
439
|
+
`https://api.github.com/repos/${owner}/${repoName}/contents/${gitPath}`,
|
|
440
|
+
{
|
|
441
|
+
method: "DELETE",
|
|
442
|
+
headers,
|
|
443
|
+
body: JSON.stringify({
|
|
444
|
+
message: commitMsg,
|
|
445
|
+
sha: existingSha,
|
|
446
|
+
branch: targetBranch
|
|
447
|
+
})
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
if (delRes.ok) {
|
|
451
|
+
const delData = await delRes.json();
|
|
452
|
+
commitSha = delData.commit?.sha;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
if (!fileContent) {
|
|
457
|
+
fileContent = formatEntryAsMarkdown(entry);
|
|
458
|
+
}
|
|
459
|
+
const base64Content = toBase64(fileContent);
|
|
460
|
+
const putRes = await fetch(
|
|
461
|
+
`https://api.github.com/repos/${owner}/${repoName}/contents/${gitPath}`,
|
|
462
|
+
{
|
|
463
|
+
method: "PUT",
|
|
464
|
+
headers,
|
|
465
|
+
body: JSON.stringify({
|
|
466
|
+
message: commitMsg,
|
|
467
|
+
content: base64Content,
|
|
468
|
+
branch: targetBranch,
|
|
469
|
+
...existingSha ? { sha: existingSha } : {}
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
);
|
|
473
|
+
if (putRes.ok) {
|
|
474
|
+
const putData = await putRes.json();
|
|
475
|
+
commitSha = putData.commit?.sha;
|
|
476
|
+
} else {
|
|
477
|
+
const errData = await putRes.json().catch(() => ({}));
|
|
478
|
+
console.warn(
|
|
479
|
+
`[qarpeo:cms] GitHub commit failed (${putRes.status}):`,
|
|
480
|
+
errData?.message || putRes.statusText
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (ghConfig.pr && targetBranch !== baseBranch) {
|
|
485
|
+
try {
|
|
486
|
+
const prRes = await fetch(`https://api.github.com/repos/${owner}/${repoName}/pulls`, {
|
|
487
|
+
method: "POST",
|
|
488
|
+
headers,
|
|
489
|
+
body: JSON.stringify({
|
|
490
|
+
title: commitMsg,
|
|
491
|
+
head: targetBranch,
|
|
492
|
+
base: ghConfig.prBase || baseBranch,
|
|
493
|
+
body: `Automated content change from Qarpeo CMS for \`${collectionId}/${entry.slug}\`.`
|
|
494
|
+
})
|
|
495
|
+
});
|
|
496
|
+
if (prRes.ok) {
|
|
497
|
+
const prData = await prRes.json();
|
|
498
|
+
prUrl = prData.html_url;
|
|
499
|
+
}
|
|
500
|
+
} catch {
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
result.github = {
|
|
504
|
+
ok: true,
|
|
505
|
+
commitSha,
|
|
506
|
+
prUrl
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
} catch (err) {
|
|
510
|
+
console.warn(`[qarpeo:cms] GitHub sync error:`, err?.message || err);
|
|
511
|
+
result.github = {
|
|
512
|
+
ok: false,
|
|
513
|
+
error: err?.message || String(err)
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return result;
|
|
518
|
+
}
|
|
519
|
+
export async function computeContentHash(text) {
|
|
520
|
+
const enc = new TextEncoder();
|
|
521
|
+
const buf = await crypto.subtle.digest("SHA-256", enc.encode(text));
|
|
522
|
+
return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
523
|
+
}
|
|
524
|
+
export function getCMSTenantId(event) {
|
|
525
|
+
const tenantId = event?.context?.tenant?.id || event?.context?.tenantId || event?.context?.organizationId || event?.context?.orgId || null;
|
|
526
|
+
return tenantId ? String(tenantId) : null;
|
|
527
|
+
}
|
|
528
|
+
export function getCMSTenantPrefix(event) {
|
|
529
|
+
const tenantId = getCMSTenantId(event);
|
|
530
|
+
return tenantId ? `${tenantId}:` : "";
|
|
531
|
+
}
|
|
532
|
+
export async function getCMSCollection(event, collectionId) {
|
|
533
|
+
const config = getCMSConfig(event);
|
|
534
|
+
const tenantPrefix = getCMSTenantPrefix(event);
|
|
535
|
+
if (config.cache) {
|
|
536
|
+
try {
|
|
537
|
+
const kv = getKVBinding(event);
|
|
538
|
+
const cached = await kv.get(`${tenantPrefix}cms:schema:${collectionId}`, {
|
|
539
|
+
type: "json"
|
|
540
|
+
});
|
|
541
|
+
if (cached) return cached;
|
|
542
|
+
} catch {
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const db = getCMSDb(event);
|
|
546
|
+
const row = await db.prepare("SELECT * FROM _qarpeo_cms_collections WHERE id = ?").bind(collectionId).first();
|
|
547
|
+
if (!row) return null;
|
|
548
|
+
const collection = rowToCollection(row);
|
|
549
|
+
if (config.cache) {
|
|
550
|
+
try {
|
|
551
|
+
const kv = getKVBinding(event);
|
|
552
|
+
await kv.put(`${tenantPrefix}cms:schema:${collectionId}`, JSON.stringify(collection), {
|
|
553
|
+
expirationTtl: 300
|
|
554
|
+
});
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return collection;
|
|
559
|
+
}
|
|
560
|
+
function rowToCollection(row, config) {
|
|
561
|
+
let fields;
|
|
562
|
+
try {
|
|
563
|
+
fields = JSON.parse(row.schema_json || "{}");
|
|
564
|
+
} catch {
|
|
565
|
+
fields = {};
|
|
566
|
+
}
|
|
567
|
+
const id = row.id;
|
|
568
|
+
const resolvedConfig = config || getCMSConfig();
|
|
569
|
+
const colCfg = resolvedConfig.collections?.[id];
|
|
570
|
+
return {
|
|
571
|
+
id,
|
|
572
|
+
name: row.name,
|
|
573
|
+
description: row.description,
|
|
574
|
+
schemaJson: row.schema_json,
|
|
575
|
+
fields,
|
|
576
|
+
source: row.source,
|
|
577
|
+
order: row.order ?? 0,
|
|
578
|
+
singleton: colCfg?.singleton,
|
|
579
|
+
tenantId: row.tenant_id || void 0,
|
|
580
|
+
createdAt: row.created_at,
|
|
581
|
+
updatedAt: row.updated_at
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
export async function listCMSCollections(event) {
|
|
585
|
+
await ensureCMSSynced(event);
|
|
586
|
+
const db = getCMSDb(event);
|
|
587
|
+
const results = await db.prepare('SELECT * FROM _qarpeo_cms_collections ORDER BY "order" ASC, name ASC').all();
|
|
588
|
+
return (results.results || []).map(
|
|
589
|
+
(r) => rowToCollection(r)
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
export async function createCMSCollection(event, data) {
|
|
593
|
+
const db = getCMSDb(event);
|
|
594
|
+
const now = Date.now();
|
|
595
|
+
const schemaJson = JSON.stringify(data.fields || {});
|
|
596
|
+
await db.prepare(
|
|
597
|
+
"INSERT INTO _qarpeo_cms_collections (id, name, description, schema_json, source, created_at, updated_at) VALUES (?, ?, ?, ?, 'db', ?, ?)"
|
|
598
|
+
).bind(data.id, data.name, data.description ?? null, schemaJson, now, now).run();
|
|
599
|
+
return {
|
|
600
|
+
id: data.id,
|
|
601
|
+
name: data.name,
|
|
602
|
+
description: data.description ?? null,
|
|
603
|
+
schemaJson,
|
|
604
|
+
fields: data.fields,
|
|
605
|
+
source: "db",
|
|
606
|
+
createdAt: now,
|
|
607
|
+
updatedAt: now
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
export async function updateCMSCollection(event, id, data) {
|
|
611
|
+
const db = getCMSDb(event);
|
|
612
|
+
const now = Date.now();
|
|
613
|
+
const schemaJson = JSON.stringify(data.fields || {});
|
|
614
|
+
await db.prepare(
|
|
615
|
+
"UPDATE _qarpeo_cms_collections SET name = COALESCE(?, name), description = COALESCE(?, description), schema_json = ?, updated_at = ? WHERE id = ?"
|
|
616
|
+
).bind(data.name ?? null, data.description ?? null, schemaJson, now, id).run();
|
|
617
|
+
try {
|
|
618
|
+
const kv = getKVBinding(event);
|
|
619
|
+
const tenantPrefix = getCMSTenantPrefix(event);
|
|
620
|
+
await kv.delete(`${tenantPrefix}cms:schema:${id}`);
|
|
621
|
+
} catch {
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
export async function deleteCMSCollection(event, id) {
|
|
625
|
+
const db = getCMSDb(event);
|
|
626
|
+
const row = await db.prepare("SELECT source FROM _qarpeo_cms_collections WHERE id = ?").bind(id).first();
|
|
627
|
+
if (!row) throw createError({ statusCode: 404, message: `Collection '${id}' not found` });
|
|
628
|
+
if (row.source === "config") {
|
|
629
|
+
throw createError({
|
|
630
|
+
statusCode: 403,
|
|
631
|
+
message: "Cannot delete a config-seeded collection from the API. Remove it from nuxt.config.ts instead."
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
await db.prepare("DELETE FROM _qarpeo_cms_collections WHERE id = ?").bind(id).run();
|
|
635
|
+
try {
|
|
636
|
+
const kv = getKVBinding(event);
|
|
637
|
+
const tenantPrefix = getCMSTenantPrefix(event);
|
|
638
|
+
await kv.delete(`${tenantPrefix}cms:schema:${id}`);
|
|
639
|
+
} catch {
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function rowToEntry(row) {
|
|
643
|
+
let metadata;
|
|
644
|
+
try {
|
|
645
|
+
metadata = JSON.parse(row.metadata_json || "{}");
|
|
646
|
+
} catch {
|
|
647
|
+
metadata = {};
|
|
648
|
+
}
|
|
649
|
+
const toc = metadata?._toc || void 0;
|
|
650
|
+
const nav = metadata?._nav || void 0;
|
|
651
|
+
const tocObj = toc || { links: [] };
|
|
652
|
+
const ast = metadata?._ast || void 0;
|
|
653
|
+
if (metadata) {
|
|
654
|
+
delete metadata._ast;
|
|
655
|
+
delete metadata._toc;
|
|
656
|
+
delete metadata._nav;
|
|
657
|
+
}
|
|
658
|
+
const body = ast && Array.isArray(ast.nodes) ? {
|
|
659
|
+
...ast,
|
|
660
|
+
toc: tocObj
|
|
661
|
+
} : {
|
|
662
|
+
value: row.content_md || "",
|
|
663
|
+
content: row.content_md || "",
|
|
664
|
+
toc: tocObj
|
|
665
|
+
};
|
|
666
|
+
return {
|
|
667
|
+
...metadata,
|
|
668
|
+
id: row.id,
|
|
669
|
+
collectionId: row.collection_id,
|
|
670
|
+
slug: row.slug,
|
|
671
|
+
path: row.path || void 0,
|
|
672
|
+
title: row.title,
|
|
673
|
+
description: row.description,
|
|
674
|
+
coverImage: row.cover_image,
|
|
675
|
+
tags: row.tags,
|
|
676
|
+
order: row.order,
|
|
677
|
+
contentMd: row.content_md,
|
|
678
|
+
contentHtml: row.content_html,
|
|
679
|
+
body,
|
|
680
|
+
toc: toc || { links: [] },
|
|
681
|
+
navigation: nav,
|
|
682
|
+
metadataJson: row.metadata_json,
|
|
683
|
+
metadata,
|
|
684
|
+
status: row.status,
|
|
685
|
+
source: row.source,
|
|
686
|
+
filePath: row.file_path,
|
|
687
|
+
contentHash: row.content_hash,
|
|
688
|
+
authorId: row.author_id,
|
|
689
|
+
publishedAt: row.published_at,
|
|
690
|
+
tenantId: row.tenant_id || void 0,
|
|
691
|
+
locale: row.locale || "en",
|
|
692
|
+
version: row.version || "latest",
|
|
693
|
+
createdAt: row.created_at,
|
|
694
|
+
updatedAt: row.updated_at
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function isReferenceValue(v) {
|
|
698
|
+
return !!v && typeof v === "object" && !Array.isArray(v) && typeof v.collectionId === "string" && typeof v.slug === "string";
|
|
699
|
+
}
|
|
700
|
+
export async function getCollectionFields(event, collectionId) {
|
|
701
|
+
const config = getCMSConfig(event);
|
|
702
|
+
const configFields = config.collections?.[collectionId]?.fields;
|
|
703
|
+
let dbFields;
|
|
704
|
+
try {
|
|
705
|
+
const db = getCMSDb(event);
|
|
706
|
+
const row = await db.prepare("SELECT schema_json FROM _qarpeo_cms_collections WHERE id = ?").bind(collectionId).first();
|
|
707
|
+
if (row?.schema_json) {
|
|
708
|
+
dbFields = JSON.parse(row.schema_json);
|
|
709
|
+
}
|
|
710
|
+
} catch {
|
|
711
|
+
}
|
|
712
|
+
return { ...configFields || {}, ...dbFields || {} };
|
|
713
|
+
}
|
|
714
|
+
async function fetchEntryRef(event, collectionId, slugOrId) {
|
|
715
|
+
const db = getCMSDb(event);
|
|
716
|
+
const tenantId = getCMSTenantId(event);
|
|
717
|
+
const row = await db.prepare(
|
|
718
|
+
`SELECT id, collection_id, slug, title FROM _qarpeo_cms_entries
|
|
719
|
+
WHERE collection_id = ? AND (slug = ? OR id = ?)
|
|
720
|
+
${tenantId ? "AND (tenant_id = ? OR tenant_id IS NULL)" : ""}
|
|
721
|
+
LIMIT 1`
|
|
722
|
+
).bind(
|
|
723
|
+
...tenantId ? [collectionId, slugOrId, slugOrId, tenantId] : [collectionId, slugOrId, slugOrId]
|
|
724
|
+
).first();
|
|
725
|
+
if (!row) return null;
|
|
726
|
+
return {
|
|
727
|
+
id: row.id,
|
|
728
|
+
collectionId: row.collection_id,
|
|
729
|
+
slug: row.slug,
|
|
730
|
+
title: row.title
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
async function resolveReferenceInput(event, input, defaultTargetCollectionId, filter, fieldName) {
|
|
734
|
+
if (input == null || input === "") {
|
|
735
|
+
throw createError({ statusCode: 400, message: "Reference value is required" });
|
|
736
|
+
}
|
|
737
|
+
let collectionId;
|
|
738
|
+
let slugOrId;
|
|
739
|
+
if (typeof input === "string") {
|
|
740
|
+
const trimmed = input.trim();
|
|
741
|
+
if (!trimmed) {
|
|
742
|
+
throw createError({ statusCode: 400, message: "Reference value is required" });
|
|
743
|
+
}
|
|
744
|
+
const slashIdx = trimmed.indexOf("/");
|
|
745
|
+
if (slashIdx > 0) {
|
|
746
|
+
collectionId = trimmed.slice(0, slashIdx);
|
|
747
|
+
slugOrId = trimmed.slice(slashIdx + 1);
|
|
748
|
+
} else {
|
|
749
|
+
collectionId = defaultTargetCollectionId;
|
|
750
|
+
slugOrId = trimmed;
|
|
751
|
+
}
|
|
752
|
+
} else if (isReferenceValue(input)) {
|
|
753
|
+
collectionId = input.collectionId;
|
|
754
|
+
slugOrId = input.id || input.slug;
|
|
755
|
+
} else {
|
|
756
|
+
throw createError({
|
|
757
|
+
statusCode: 400,
|
|
758
|
+
message: 'Invalid reference value: expected an id, "collection/slug", or a reference object'
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
if (!collectionId || !slugOrId) {
|
|
762
|
+
throw createError({
|
|
763
|
+
statusCode: 400,
|
|
764
|
+
message: "Reference value is missing collection or slug/id"
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
const resolved = await fetchEntryRef(event, collectionId, slugOrId);
|
|
768
|
+
if (!resolved) {
|
|
769
|
+
throw createError({
|
|
770
|
+
statusCode: 400,
|
|
771
|
+
message: `Referenced entry "${slugOrId}" not found in collection "${collectionId}"`
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
if (filter && Object.keys(filter).length > 0) {
|
|
775
|
+
const fullEntry = await getCMSEntry(event, collectionId, resolved.id, { preview: true });
|
|
776
|
+
if (fullEntry) {
|
|
777
|
+
for (const [key, expected] of Object.entries(filter)) {
|
|
778
|
+
const actual = fullEntry.metadata?.[key];
|
|
779
|
+
if (actual !== expected) {
|
|
780
|
+
const label = fieldName ? `reference field "${fieldName}"` : "reference";
|
|
781
|
+
throw createError({
|
|
782
|
+
statusCode: 400,
|
|
783
|
+
message: `The ${label} target "${resolved.slug}" does not match the required filter (${key} must be ${String(expected)})`
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return resolved;
|
|
790
|
+
}
|
|
791
|
+
async function validateAndNormalizeReferenceFields(event, collectionId, metadata) {
|
|
792
|
+
const fields = await getCollectionFields(event, collectionId);
|
|
793
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
794
|
+
if (fieldDef.type !== "reference" || !fieldDef.reference) continue;
|
|
795
|
+
const refOpts = fieldDef.reference;
|
|
796
|
+
const multiple = refOpts.multiple === true;
|
|
797
|
+
const value = metadata[fieldName];
|
|
798
|
+
if (value == null || value === "") {
|
|
799
|
+
if (fieldDef.required) {
|
|
800
|
+
throw createError({
|
|
801
|
+
statusCode: 400,
|
|
802
|
+
message: `Reference field "${fieldName}" is required`
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
if (multiple) metadata[fieldName] = [];
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (multiple) {
|
|
809
|
+
const items = Array.isArray(value) ? value : [value];
|
|
810
|
+
const resolved = [];
|
|
811
|
+
for (const item of items) {
|
|
812
|
+
if (item == null || item === "") continue;
|
|
813
|
+
resolved.push(
|
|
814
|
+
await resolveReferenceInput(event, item, refOpts.to, refOpts.filter, fieldName)
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
metadata[fieldName] = resolved;
|
|
818
|
+
} else {
|
|
819
|
+
metadata[fieldName] = await resolveReferenceInput(
|
|
820
|
+
event,
|
|
821
|
+
value,
|
|
822
|
+
refOpts.to,
|
|
823
|
+
refOpts.filter,
|
|
824
|
+
fieldName
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return metadata;
|
|
829
|
+
}
|
|
830
|
+
function resolveComponentDefinition(fieldDef, config) {
|
|
831
|
+
if (fieldDef.type !== "component") return null;
|
|
832
|
+
const compOpts = fieldDef.component;
|
|
833
|
+
if (!compOpts) return null;
|
|
834
|
+
if (typeof compOpts === "string") {
|
|
835
|
+
return config.components?.[compOpts] ?? null;
|
|
836
|
+
}
|
|
837
|
+
if (compOpts.name) {
|
|
838
|
+
return config.components?.[compOpts.name] ?? null;
|
|
839
|
+
}
|
|
840
|
+
if (compOpts.fields) {
|
|
841
|
+
return { fields: compOpts.fields };
|
|
842
|
+
}
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
async function validateAndNormalizeComponentFields(event, collectionId, metadata) {
|
|
846
|
+
const config = getCMSConfig(event);
|
|
847
|
+
const fields = await getCollectionFields(event, collectionId);
|
|
848
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
849
|
+
if (fieldDef.type !== "component") continue;
|
|
850
|
+
const compDef = resolveComponentDefinition(fieldDef, config);
|
|
851
|
+
if (!compDef) continue;
|
|
852
|
+
const compOpts = typeof fieldDef.component === "object" ? fieldDef.component : null;
|
|
853
|
+
const isRepeatable = fieldDef.repeatable || (compOpts?.repeatable ?? false);
|
|
854
|
+
const value = metadata[fieldName];
|
|
855
|
+
if (value == null || value === "") {
|
|
856
|
+
if (fieldDef.required) {
|
|
857
|
+
throw createError({
|
|
858
|
+
statusCode: 400,
|
|
859
|
+
message: `Component field "${fieldName}" is required`
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
if (isRepeatable) metadata[fieldName] = [];
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (isRepeatable && !Array.isArray(value)) {
|
|
866
|
+
metadata[fieldName] = [value];
|
|
867
|
+
}
|
|
868
|
+
if (isRepeatable && compOpts?.max != null && Array.isArray(metadata[fieldName])) {
|
|
869
|
+
const arr = metadata[fieldName];
|
|
870
|
+
if (arr.length > compOpts.max) {
|
|
871
|
+
throw createError({
|
|
872
|
+
statusCode: 400,
|
|
873
|
+
message: `Component field "${fieldName}" exceeds maximum of ${compOpts.max} items`
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (isRepeatable && compOpts?.min != null && Array.isArray(metadata[fieldName])) {
|
|
878
|
+
const arr = metadata[fieldName];
|
|
879
|
+
if (arr.length < compOpts.min) {
|
|
880
|
+
throw createError({
|
|
881
|
+
statusCode: 400,
|
|
882
|
+
message: `Component field "${fieldName}" requires at least ${compOpts.min} items`
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return metadata;
|
|
888
|
+
}
|
|
889
|
+
export function defineCMSComponent(def) {
|
|
890
|
+
return def;
|
|
891
|
+
}
|
|
892
|
+
export async function resolveReference(event, ref) {
|
|
893
|
+
if (!ref || !ref.id && !ref.slug) return null;
|
|
894
|
+
const collectionId = ref.collectionId;
|
|
895
|
+
if (!collectionId) return null;
|
|
896
|
+
const slugOrId = ref.id || ref.slug;
|
|
897
|
+
if (!slugOrId) return null;
|
|
898
|
+
return getCMSEntry(event, collectionId, slugOrId);
|
|
899
|
+
}
|
|
900
|
+
async function populateReferenceFields(event, entry, populate) {
|
|
901
|
+
if (!entry.metadata || Object.keys(entry.metadata).length === 0) return entry;
|
|
902
|
+
const fields = await getCollectionFields(event, entry.collectionId);
|
|
903
|
+
const populatedMeta = { ...entry.metadata };
|
|
904
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
905
|
+
if (fieldDef.type !== "reference") continue;
|
|
906
|
+
if (Array.isArray(populate) && !populate.includes(fieldName)) continue;
|
|
907
|
+
const value = populatedMeta[fieldName];
|
|
908
|
+
if (value == null) continue;
|
|
909
|
+
if (Array.isArray(value)) {
|
|
910
|
+
populatedMeta[fieldName] = await Promise.all(
|
|
911
|
+
value.filter(isReferenceValue).map(async (r) => ({
|
|
912
|
+
...r,
|
|
913
|
+
entry: await resolveReference(event, r)
|
|
914
|
+
}))
|
|
915
|
+
);
|
|
916
|
+
} else if (isReferenceValue(value)) {
|
|
917
|
+
populatedMeta[fieldName] = { ...value, entry: await resolveReference(event, value) };
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return { ...entry, metadata: populatedMeta };
|
|
921
|
+
}
|
|
922
|
+
const REFERENCE_PATH_RE = /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/;
|
|
923
|
+
function serializeReferencesForFrontmatter(metadata) {
|
|
924
|
+
const out = {};
|
|
925
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
926
|
+
if (isReferenceValue(v)) {
|
|
927
|
+
out[k] = `${v.collectionId}/${v.slug}`;
|
|
928
|
+
} else if (Array.isArray(v) && v.length > 0 && v.every((item) => isReferenceValue(item))) {
|
|
929
|
+
out[k] = v.map(
|
|
930
|
+
(item) => `${item.collectionId}/${item.slug}`
|
|
931
|
+
);
|
|
932
|
+
} else {
|
|
933
|
+
out[k] = v;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return out;
|
|
937
|
+
}
|
|
938
|
+
async function resolveReferenceFieldsFromFrontmatter(event, collectionId, fm) {
|
|
939
|
+
const fields = await getCollectionFields(event, collectionId);
|
|
940
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
941
|
+
if (fieldDef.type !== "reference" || !fieldDef.reference) continue;
|
|
942
|
+
const refOpts = fieldDef.reference;
|
|
943
|
+
const multiple = refOpts.multiple === true;
|
|
944
|
+
const value = fm[fieldName];
|
|
945
|
+
if (value == null) continue;
|
|
946
|
+
const resolveString = async (str) => {
|
|
947
|
+
if (!REFERENCE_PATH_RE.test(str)) return null;
|
|
948
|
+
const slashIdx = str.indexOf("/");
|
|
949
|
+
const refCollectionId = str.slice(0, slashIdx);
|
|
950
|
+
const refSlug = str.slice(slashIdx + 1);
|
|
951
|
+
return fetchEntryRef(event, refCollectionId, refSlug);
|
|
952
|
+
};
|
|
953
|
+
if (multiple) {
|
|
954
|
+
const items = Array.isArray(value) ? value : [value];
|
|
955
|
+
const resolved = [];
|
|
956
|
+
for (const item of items) {
|
|
957
|
+
if (typeof item === "string") {
|
|
958
|
+
const r = await resolveString(item);
|
|
959
|
+
if (r) resolved.push(r);
|
|
960
|
+
} else if (isReferenceValue(item)) {
|
|
961
|
+
resolved.push(item);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (resolved.length > 0) fm[fieldName] = resolved;
|
|
965
|
+
} else if (typeof value === "string" && REFERENCE_PATH_RE.test(value)) {
|
|
966
|
+
const r = await resolveString(value);
|
|
967
|
+
if (r) fm[fieldName] = r;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
function runCMSBackgroundTask(event, task) {
|
|
972
|
+
const ctx = event?.context?.cloudflare?.context || event?.node?.req?.context?.cloudflare?.context || event?.context?.waitUntil;
|
|
973
|
+
if (ctx && typeof ctx.waitUntil === "function") {
|
|
974
|
+
ctx.waitUntil(
|
|
975
|
+
task.catch((err) => console.warn("[qarpeo:cms:cache] Background task error:", err))
|
|
976
|
+
);
|
|
977
|
+
} else if (typeof ctx === "function") {
|
|
978
|
+
ctx(task.catch((err) => console.warn("[qarpeo:cms:cache] Background task error:", err)));
|
|
979
|
+
} else {
|
|
980
|
+
task.catch(() => {
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
async function getCachedEntry(event, collectionId, slugOrId) {
|
|
985
|
+
const config = getCMSConfig(event);
|
|
986
|
+
if (!config.cache) return null;
|
|
987
|
+
try {
|
|
988
|
+
const kv = getKVBinding(event);
|
|
989
|
+
const tenantPrefix = getCMSTenantPrefix(event);
|
|
990
|
+
const cached = await kv.get(`${tenantPrefix}cms:entry:${collectionId}:${slugOrId}`, {
|
|
991
|
+
type: "json"
|
|
992
|
+
});
|
|
993
|
+
return cached;
|
|
994
|
+
} catch {
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
async function setCachedEntry(event, collectionId, slugOrId, entry) {
|
|
999
|
+
const config = getCMSConfig(event);
|
|
1000
|
+
if (!config.cache) return;
|
|
1001
|
+
const tenantPrefix = getCMSTenantPrefix(event);
|
|
1002
|
+
const task = (async () => {
|
|
1003
|
+
try {
|
|
1004
|
+
const kv = getKVBinding(event);
|
|
1005
|
+
await kv.put(`${tenantPrefix}cms:entry:${collectionId}:${slugOrId}`, JSON.stringify(entry), {
|
|
1006
|
+
expirationTtl: 60
|
|
1007
|
+
});
|
|
1008
|
+
} catch {
|
|
1009
|
+
}
|
|
1010
|
+
})();
|
|
1011
|
+
runCMSBackgroundTask(event, task);
|
|
1012
|
+
}
|
|
1013
|
+
async function getCachedList(event, key) {
|
|
1014
|
+
const config = getCMSConfig(event);
|
|
1015
|
+
if (!config.cache) return null;
|
|
1016
|
+
try {
|
|
1017
|
+
const kv = getKVBinding(event);
|
|
1018
|
+
const tenantPrefix = getCMSTenantPrefix(event);
|
|
1019
|
+
const cached = await kv.get(`${tenantPrefix}${key}`, { type: "json" });
|
|
1020
|
+
return cached;
|
|
1021
|
+
} catch {
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
async function setCachedList(event, key, data) {
|
|
1026
|
+
const config = getCMSConfig(event);
|
|
1027
|
+
if (!config.cache) return;
|
|
1028
|
+
const tenantPrefix = getCMSTenantPrefix(event);
|
|
1029
|
+
const task = (async () => {
|
|
1030
|
+
try {
|
|
1031
|
+
const kv = getKVBinding(event);
|
|
1032
|
+
await kv.put(`${tenantPrefix}${key}`, JSON.stringify(data), { expirationTtl: 30 });
|
|
1033
|
+
} catch {
|
|
1034
|
+
}
|
|
1035
|
+
})();
|
|
1036
|
+
runCMSBackgroundTask(event, task);
|
|
1037
|
+
}
|
|
1038
|
+
export async function invalidateCMSEntryCache(event, collectionId, slug) {
|
|
1039
|
+
const config = getCMSConfig(event);
|
|
1040
|
+
if (!config.cache) return;
|
|
1041
|
+
const tenantPrefix = getCMSTenantPrefix(event);
|
|
1042
|
+
const task = (async () => {
|
|
1043
|
+
try {
|
|
1044
|
+
const kv = getKVBinding(event);
|
|
1045
|
+
if (slug) {
|
|
1046
|
+
await kv.delete(`${tenantPrefix}cms:entry:${collectionId}:${slug}`);
|
|
1047
|
+
}
|
|
1048
|
+
const listRes = await kv.list({ prefix: `${tenantPrefix}cms:list:${collectionId}:` });
|
|
1049
|
+
if (listRes?.keys?.length) {
|
|
1050
|
+
await Promise.all(listRes.keys.map((k) => kv.delete(k.name)));
|
|
1051
|
+
}
|
|
1052
|
+
} catch {
|
|
1053
|
+
}
|
|
1054
|
+
})();
|
|
1055
|
+
runCMSBackgroundTask(event, task);
|
|
1056
|
+
}
|
|
1057
|
+
export async function verifyGitHubSignature(rawBody, signatureHeader, secret) {
|
|
1058
|
+
if (!signatureHeader || !secret) return false;
|
|
1059
|
+
const cleanSig = signatureHeader.replace(/^sha256=/, "").trim();
|
|
1060
|
+
try {
|
|
1061
|
+
const key = await crypto.subtle.importKey(
|
|
1062
|
+
"raw",
|
|
1063
|
+
new TextEncoder().encode(secret),
|
|
1064
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
1065
|
+
false,
|
|
1066
|
+
["sign"]
|
|
1067
|
+
);
|
|
1068
|
+
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(rawBody));
|
|
1069
|
+
const expected = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1070
|
+
if (expected.length !== cleanSig.length) return false;
|
|
1071
|
+
let diff = 0;
|
|
1072
|
+
for (let i = 0; i < expected.length; i++) {
|
|
1073
|
+
diff |= expected.charCodeAt(i) ^ cleanSig.charCodeAt(i);
|
|
1074
|
+
}
|
|
1075
|
+
return diff === 0;
|
|
1076
|
+
} catch {
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
export async function queryCMSEntries(event, collectionId, opts = {}) {
|
|
1081
|
+
const status = opts.status || "published";
|
|
1082
|
+
const isCacheable = status === "published";
|
|
1083
|
+
const listCacheKey = isCacheable && !opts.populate ? `cms:list:${collectionId}:${opts.page || 1}:${opts.limit || 10}:${opts.sort || ""}:${opts.tag || ""}:${opts.where ? JSON.stringify(opts.where) : ""}` : null;
|
|
1084
|
+
if (listCacheKey) {
|
|
1085
|
+
const cached = await getCachedList(event, listCacheKey);
|
|
1086
|
+
if (cached) return cached;
|
|
1087
|
+
}
|
|
1088
|
+
const db = getCMSDb(event);
|
|
1089
|
+
const page = Math.max(1, opts.page || 1);
|
|
1090
|
+
const limit = Math.min(100, opts.limit || 10);
|
|
1091
|
+
const offset = (page - 1) * limit;
|
|
1092
|
+
const conditions = ["collection_id = ?"];
|
|
1093
|
+
const binds = [collectionId];
|
|
1094
|
+
const tenantId = getCMSTenantId(event);
|
|
1095
|
+
if (tenantId) {
|
|
1096
|
+
conditions.push("(tenant_id = ? OR tenant_id IS NULL)");
|
|
1097
|
+
binds.push(tenantId);
|
|
1098
|
+
}
|
|
1099
|
+
if (status !== "all") {
|
|
1100
|
+
conditions.push("status = ?");
|
|
1101
|
+
binds.push(status);
|
|
1102
|
+
}
|
|
1103
|
+
if (opts.tag) {
|
|
1104
|
+
conditions.push("(tags LIKE ? OR tags LIKE ? OR tags LIKE ? OR tags = ?)");
|
|
1105
|
+
binds.push(`${opts.tag},%`, `%,${opts.tag},%`, `%,${opts.tag}`, opts.tag);
|
|
1106
|
+
}
|
|
1107
|
+
if (opts.where && typeof opts.where === "object") {
|
|
1108
|
+
for (const [key, value] of Object.entries(opts.where)) {
|
|
1109
|
+
if (value === void 0 || !/^[a-zA-Z0-9_]+$/.test(key)) continue;
|
|
1110
|
+
const jsonPath = `$.${key}`;
|
|
1111
|
+
if (value === null) {
|
|
1112
|
+
conditions.push("json_extract(metadata_json, ?) IS NULL");
|
|
1113
|
+
binds.push(jsonPath);
|
|
1114
|
+
} else if (typeof value === "boolean") {
|
|
1115
|
+
conditions.push("json_extract(metadata_json, ?) = ?");
|
|
1116
|
+
binds.push(jsonPath, value ? 1 : 0);
|
|
1117
|
+
} else {
|
|
1118
|
+
conditions.push("json_extract(metadata_json, ?) = ?");
|
|
1119
|
+
binds.push(jsonPath, value);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
const where = `WHERE ${conditions.join(" AND ")}`;
|
|
1124
|
+
const [sortField, sortDir] = (opts.sort || "published_at:desc").split(":");
|
|
1125
|
+
const safeSortField = ["published_at", "created_at", "updated_at", "title", "order"].includes(
|
|
1126
|
+
sortField
|
|
1127
|
+
) ? sortField : "published_at";
|
|
1128
|
+
const safeDir = sortDir === "asc" ? "ASC" : "DESC";
|
|
1129
|
+
const [countResult, dataResult] = await Promise.all([
|
|
1130
|
+
db.prepare(`SELECT COUNT(*) as total FROM _qarpeo_cms_entries ${where}`).bind(...binds).first(),
|
|
1131
|
+
db.prepare(
|
|
1132
|
+
`SELECT * FROM _qarpeo_cms_entries ${where} ORDER BY ${safeSortField} ${safeDir} LIMIT ? OFFSET ?`
|
|
1133
|
+
).bind(...binds, limit, offset).all()
|
|
1134
|
+
]);
|
|
1135
|
+
const total = countResult?.total ?? 0;
|
|
1136
|
+
const entries = (dataResult.results || []).map(
|
|
1137
|
+
rowToEntry
|
|
1138
|
+
);
|
|
1139
|
+
const populatedEntries = opts.populate ? await Promise.all(entries.map((e) => populateReferenceFields(event, e, opts.populate))) : entries;
|
|
1140
|
+
const result = { entries: populatedEntries, total, page, limit };
|
|
1141
|
+
if (listCacheKey) {
|
|
1142
|
+
await setCachedList(event, listCacheKey, result);
|
|
1143
|
+
}
|
|
1144
|
+
return result;
|
|
1145
|
+
}
|
|
1146
|
+
export async function getCMSEntry(event, collectionId, slugOrId, opts = {}) {
|
|
1147
|
+
const db = getCMSDb(event);
|
|
1148
|
+
let allowDraft = false;
|
|
1149
|
+
const previewCookie = event ? getCookie(event, "qarpeo_preview") : null;
|
|
1150
|
+
let cookieObj = null;
|
|
1151
|
+
if (previewCookie) {
|
|
1152
|
+
try {
|
|
1153
|
+
cookieObj = typeof previewCookie === "string" ? JSON.parse(previewCookie) : previewCookie;
|
|
1154
|
+
} catch {
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
const isPreviewRequested = opts.preview || Boolean(cookieObj);
|
|
1158
|
+
if (isPreviewRequested) {
|
|
1159
|
+
const auth = await getAuthContext(event).catch(() => null);
|
|
1160
|
+
const config = getCMSConfig(event);
|
|
1161
|
+
if (auth?.user && config.roles.some((r) => r === auth.role || auth.isAdmin)) {
|
|
1162
|
+
allowDraft = true;
|
|
1163
|
+
}
|
|
1164
|
+
if (!allowDraft) {
|
|
1165
|
+
const token = opts.previewToken || cookieObj?.token;
|
|
1166
|
+
const expires = opts.previewExpires || cookieObj?.expires;
|
|
1167
|
+
if (token && expires) {
|
|
1168
|
+
allowDraft = await verifyPreviewToken(
|
|
1169
|
+
event,
|
|
1170
|
+
collectionId,
|
|
1171
|
+
slugOrId,
|
|
1172
|
+
token,
|
|
1173
|
+
expires
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if (!allowDraft) {
|
|
1179
|
+
const cached = await getCachedEntry(event, collectionId, slugOrId);
|
|
1180
|
+
if (cached) return cached;
|
|
1181
|
+
}
|
|
1182
|
+
const statusClause = allowDraft ? "AND status IN ('draft', 'published', 'scheduled')" : "AND status = 'published'";
|
|
1183
|
+
const statusOrder = allowDraft ? "ORDER BY CASE WHEN status = 'draft' THEN 1 WHEN status = 'scheduled' THEN 2 ELSE 3 END" : "";
|
|
1184
|
+
const tenantId = getCMSTenantId(event);
|
|
1185
|
+
const tenantClause = tenantId ? "AND (tenant_id = ? OR tenant_id IS NULL)" : "";
|
|
1186
|
+
const binds = tenantId ? [collectionId, slugOrId, slugOrId, tenantId] : [collectionId, slugOrId, slugOrId];
|
|
1187
|
+
const row = await db.prepare(
|
|
1188
|
+
`SELECT * FROM _qarpeo_cms_entries WHERE collection_id = ? AND (slug = ? OR id = ?) ${statusClause} ${tenantClause} ${statusOrder} LIMIT 1`
|
|
1189
|
+
).bind(...binds).first();
|
|
1190
|
+
const entry = row ? rowToEntry(row) : null;
|
|
1191
|
+
if (entry && (!entry.body || !Array.isArray(entry.body.nodes)) && entry.contentMd) {
|
|
1192
|
+
const ast = await parseMarkdownAST(entry.contentMd, event);
|
|
1193
|
+
entry.body = {
|
|
1194
|
+
...ast,
|
|
1195
|
+
value: entry.contentMd,
|
|
1196
|
+
content: entry.contentMd,
|
|
1197
|
+
toc: entry.toc || { links: [] }
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
if (entry && !allowDraft) {
|
|
1201
|
+
await setCachedEntry(event, collectionId, slugOrId, entry);
|
|
1202
|
+
if (entry.slug && entry.slug !== slugOrId) {
|
|
1203
|
+
await setCachedEntry(event, collectionId, entry.slug, entry);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (entry && opts.populate) {
|
|
1207
|
+
return populateReferenceFields(event, entry, opts.populate);
|
|
1208
|
+
}
|
|
1209
|
+
return entry;
|
|
1210
|
+
}
|
|
1211
|
+
export async function createCMSEntry(event, collectionId, data) {
|
|
1212
|
+
const db = getCMSDb(event);
|
|
1213
|
+
const cmsConfig = getCMSConfig(event);
|
|
1214
|
+
const isSingleton = cmsConfig.collections?.[collectionId]?.singleton;
|
|
1215
|
+
if (isSingleton) {
|
|
1216
|
+
const existing = await db.prepare("SELECT COUNT(*) as total FROM _qarpeo_cms_entries WHERE collection_id = ?").bind(collectionId).first();
|
|
1217
|
+
if (existing && existing.total > 0) {
|
|
1218
|
+
throw createError({
|
|
1219
|
+
statusCode: 409,
|
|
1220
|
+
message: `Collection '${collectionId}' is a singleton and already has an entry.`
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
const id = nanoid();
|
|
1225
|
+
const now = Date.now();
|
|
1226
|
+
const { ast, html: contentHtml } = await processMarkdown(data.contentMd);
|
|
1227
|
+
const metadata = data.metadata || {};
|
|
1228
|
+
if (ast) {
|
|
1229
|
+
;
|
|
1230
|
+
metadata._ast = ast;
|
|
1231
|
+
}
|
|
1232
|
+
await validateAndNormalizeReferenceFields(event, collectionId, metadata);
|
|
1233
|
+
await validateAndNormalizeComponentFields(event, collectionId, metadata);
|
|
1234
|
+
const metadataJson = JSON.stringify(metadata);
|
|
1235
|
+
const publishedAt = data.status === "published" ? data.publishedAt ?? now : data.status === "scheduled" && data.publishedAt ? data.publishedAt : data.publishedAt ?? null;
|
|
1236
|
+
const tenantId = getCMSTenantId(event);
|
|
1237
|
+
await db.prepare(
|
|
1238
|
+
`INSERT INTO _qarpeo_cms_entries
|
|
1239
|
+
(id, collection_id, slug, title, description, cover_image, tags, "order", content_md, content_html, metadata_json, status, source, author_id, published_at, created_at, updated_at, tenant_id)
|
|
1240
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'db', ?, ?, ?, ?, ?)`
|
|
1241
|
+
).bind(
|
|
1242
|
+
id,
|
|
1243
|
+
collectionId,
|
|
1244
|
+
data.slug,
|
|
1245
|
+
data.title,
|
|
1246
|
+
data.description ?? null,
|
|
1247
|
+
data.coverImage ?? null,
|
|
1248
|
+
data.tags ?? null,
|
|
1249
|
+
data.order ?? 0,
|
|
1250
|
+
data.contentMd,
|
|
1251
|
+
contentHtml,
|
|
1252
|
+
metadataJson,
|
|
1253
|
+
data.status || "draft",
|
|
1254
|
+
data.authorId ?? null,
|
|
1255
|
+
publishedAt,
|
|
1256
|
+
now,
|
|
1257
|
+
now,
|
|
1258
|
+
tenantId
|
|
1259
|
+
).run();
|
|
1260
|
+
const entry = {
|
|
1261
|
+
id,
|
|
1262
|
+
collectionId,
|
|
1263
|
+
slug: data.slug,
|
|
1264
|
+
title: data.title,
|
|
1265
|
+
description: data.description ?? null,
|
|
1266
|
+
coverImage: data.coverImage ?? null,
|
|
1267
|
+
tags: data.tags ?? null,
|
|
1268
|
+
order: data.order ?? 0,
|
|
1269
|
+
contentMd: data.contentMd,
|
|
1270
|
+
contentHtml,
|
|
1271
|
+
metadataJson,
|
|
1272
|
+
metadata,
|
|
1273
|
+
body: ast ? { ...ast, value: data.contentMd, content: data.contentMd } : void 0,
|
|
1274
|
+
status: data.status || "draft",
|
|
1275
|
+
source: "db",
|
|
1276
|
+
filePath: null,
|
|
1277
|
+
contentHash: null,
|
|
1278
|
+
authorId: data.authorId ?? null,
|
|
1279
|
+
publishedAt,
|
|
1280
|
+
tenantId: tenantId ?? null,
|
|
1281
|
+
createdAt: now,
|
|
1282
|
+
updatedAt: now
|
|
1283
|
+
};
|
|
1284
|
+
const gitResult = await syncEntryToGit(event, collectionId, entry, "create");
|
|
1285
|
+
if (gitResult?.filePath) {
|
|
1286
|
+
entry.filePath = gitResult.filePath;
|
|
1287
|
+
entry.contentHash = gitResult.contentHash || null;
|
|
1288
|
+
entry.source = "file";
|
|
1289
|
+
}
|
|
1290
|
+
await invalidateCMSEntryCache(event, collectionId, data.slug);
|
|
1291
|
+
fireCMSWebhooks(getCMSConfig(event), collectionId, "create", entry).catch(console.warn);
|
|
1292
|
+
return entry;
|
|
1293
|
+
}
|
|
1294
|
+
export async function updateCMSEntry(event, entryId, data) {
|
|
1295
|
+
const db = getCMSDb(event);
|
|
1296
|
+
const now = Date.now();
|
|
1297
|
+
const existing = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
|
|
1298
|
+
if (!existing) throw createError({ statusCode: 404, message: "Entry not found" });
|
|
1299
|
+
const tenantId = getCMSTenantId(event);
|
|
1300
|
+
if (tenantId && existing.tenant_id && existing.tenant_id !== tenantId) {
|
|
1301
|
+
throw createError({
|
|
1302
|
+
statusCode: 403,
|
|
1303
|
+
message: "Forbidden: Cannot modify an entry belonging to another tenant"
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
const contentMd = data.contentMd ?? existing.content_md;
|
|
1307
|
+
let contentHtml = existing.content_html;
|
|
1308
|
+
let newAst = null;
|
|
1309
|
+
if (data.contentMd) {
|
|
1310
|
+
const res = await processMarkdown(data.contentMd);
|
|
1311
|
+
contentHtml = res.html;
|
|
1312
|
+
newAst = res.ast;
|
|
1313
|
+
}
|
|
1314
|
+
let metadataObj = data.metadata;
|
|
1315
|
+
if (!metadataObj && existing.metadata_json) {
|
|
1316
|
+
try {
|
|
1317
|
+
metadataObj = JSON.parse(existing.metadata_json);
|
|
1318
|
+
} catch {
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
if (metadataObj && newAst) {
|
|
1322
|
+
;
|
|
1323
|
+
metadataObj._ast = newAst;
|
|
1324
|
+
}
|
|
1325
|
+
if (metadataObj && data.metadata) {
|
|
1326
|
+
await validateAndNormalizeReferenceFields(event, existing.collection_id, metadataObj);
|
|
1327
|
+
await validateAndNormalizeComponentFields(event, existing.collection_id, metadataObj);
|
|
1328
|
+
}
|
|
1329
|
+
const metadataJson = metadataObj ? JSON.stringify(metadataObj) : existing.metadata_json;
|
|
1330
|
+
const status = data.status ?? existing.status;
|
|
1331
|
+
let publishedAt;
|
|
1332
|
+
if (data.publishedAt !== void 0) {
|
|
1333
|
+
publishedAt = data.publishedAt;
|
|
1334
|
+
} else if (status === "published") {
|
|
1335
|
+
publishedAt = existing.published_at ?? now;
|
|
1336
|
+
} else if (status === "scheduled") {
|
|
1337
|
+
publishedAt = existing.published_at ?? now;
|
|
1338
|
+
} else {
|
|
1339
|
+
publishedAt = existing.published_at;
|
|
1340
|
+
}
|
|
1341
|
+
await db.prepare(
|
|
1342
|
+
"INSERT INTO _qarpeo_cms_revisions (id, entry_id, title, content_md, metadata_json, author_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
1343
|
+
).bind(
|
|
1344
|
+
nanoid(),
|
|
1345
|
+
entryId,
|
|
1346
|
+
existing.title,
|
|
1347
|
+
existing.content_md,
|
|
1348
|
+
existing.metadata_json,
|
|
1349
|
+
existing.author_id,
|
|
1350
|
+
now
|
|
1351
|
+
).run();
|
|
1352
|
+
await db.prepare(
|
|
1353
|
+
`UPDATE _qarpeo_cms_entries SET
|
|
1354
|
+
slug = COALESCE(?, slug), title = COALESCE(?, title),
|
|
1355
|
+
description = COALESCE(?, description), cover_image = COALESCE(?, cover_image),
|
|
1356
|
+
tags = COALESCE(?, tags), "order" = COALESCE(?, "order"),
|
|
1357
|
+
content_md = ?, content_html = ?, metadata_json = ?,
|
|
1358
|
+
status = ?, source = 'db', published_at = ?, updated_at = ?
|
|
1359
|
+
WHERE id = ?`
|
|
1360
|
+
).bind(
|
|
1361
|
+
data.slug ?? null,
|
|
1362
|
+
data.title ?? null,
|
|
1363
|
+
data.description ?? null,
|
|
1364
|
+
data.coverImage ?? null,
|
|
1365
|
+
data.tags ?? null,
|
|
1366
|
+
data.order ?? null,
|
|
1367
|
+
contentMd,
|
|
1368
|
+
contentHtml,
|
|
1369
|
+
metadataJson,
|
|
1370
|
+
status,
|
|
1371
|
+
publishedAt,
|
|
1372
|
+
now,
|
|
1373
|
+
entryId
|
|
1374
|
+
).run();
|
|
1375
|
+
const config = getCMSConfig(event);
|
|
1376
|
+
const maxRevisions = typeof config.revisions === "object" ? config.revisions.max ?? 10 : 10;
|
|
1377
|
+
await pruneRevisions(event, entryId, maxRevisions);
|
|
1378
|
+
const updated = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
|
|
1379
|
+
const updatedEntry = rowToEntry(updated);
|
|
1380
|
+
const gitResult = await syncEntryToGit(event, updatedEntry.collectionId, updatedEntry, "update");
|
|
1381
|
+
if (gitResult?.filePath) {
|
|
1382
|
+
updatedEntry.filePath = gitResult.filePath;
|
|
1383
|
+
updatedEntry.contentHash = gitResult.contentHash || null;
|
|
1384
|
+
updatedEntry.source = "file";
|
|
1385
|
+
}
|
|
1386
|
+
await invalidateCMSEntryCache(event, updatedEntry.collectionId, updatedEntry.slug);
|
|
1387
|
+
fireCMSWebhooks(getCMSConfig(event), updatedEntry.collectionId, "update", updatedEntry).catch(
|
|
1388
|
+
console.warn
|
|
1389
|
+
);
|
|
1390
|
+
return updatedEntry;
|
|
1391
|
+
}
|
|
1392
|
+
async function nullifyOrphanedReferences(event, deletedEntryId) {
|
|
1393
|
+
const db = getCMSDb(event);
|
|
1394
|
+
try {
|
|
1395
|
+
const tenantId = getCMSTenantId(event);
|
|
1396
|
+
const tenantClause = tenantId ? " AND (tenant_id = ? OR tenant_id IS NULL)" : "";
|
|
1397
|
+
const binds = tenantId ? [`%"id":"${deletedEntryId}"%`, tenantId] : [`%"id":"${deletedEntryId}"%`];
|
|
1398
|
+
const rows = (await db.prepare(
|
|
1399
|
+
`SELECT id, collection_id, slug, metadata_json FROM _qarpeo_cms_entries
|
|
1400
|
+
WHERE json_extract(metadata_json, '$') IS NOT NULL
|
|
1401
|
+
AND metadata_json LIKE ?${tenantClause}`
|
|
1402
|
+
).bind(...binds).all()).results;
|
|
1403
|
+
for (const row of rows) {
|
|
1404
|
+
let meta;
|
|
1405
|
+
try {
|
|
1406
|
+
meta = JSON.parse(row.metadata_json);
|
|
1407
|
+
} catch {
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
let changed = false;
|
|
1411
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
1412
|
+
if (isReferenceValue(value) && value.id === deletedEntryId) {
|
|
1413
|
+
meta[key] = null;
|
|
1414
|
+
changed = true;
|
|
1415
|
+
} else if (Array.isArray(value)) {
|
|
1416
|
+
const filtered = value.filter(
|
|
1417
|
+
(item) => !(isReferenceValue(item) && item.id === deletedEntryId)
|
|
1418
|
+
);
|
|
1419
|
+
if (filtered.length !== value.length) {
|
|
1420
|
+
meta[key] = filtered;
|
|
1421
|
+
changed = true;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (changed) {
|
|
1426
|
+
await db.prepare("UPDATE _qarpeo_cms_entries SET metadata_json = ? WHERE id = ?").bind(JSON.stringify(meta), row.id).run();
|
|
1427
|
+
await invalidateCMSEntryCache(event, row.collection_id, row.slug);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
} catch (err) {
|
|
1431
|
+
console.warn("[qarpeo:cms] nullifyOrphanedReferences failed:", err);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
export async function deleteCMSEntry(event, entryId) {
|
|
1435
|
+
const db = getCMSDb(event);
|
|
1436
|
+
const existing = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
|
|
1437
|
+
if (!existing) throw createError({ statusCode: 404, message: "Entry not found" });
|
|
1438
|
+
const tenantId = getCMSTenantId(event);
|
|
1439
|
+
if (tenantId && existing.tenant_id && existing.tenant_id !== tenantId) {
|
|
1440
|
+
throw createError({
|
|
1441
|
+
statusCode: 403,
|
|
1442
|
+
message: "Forbidden: Cannot delete an entry belonging to another tenant"
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
await db.batch([
|
|
1446
|
+
db.prepare("DELETE FROM _qarpeo_cms_revisions WHERE entry_id = ?").bind(entryId),
|
|
1447
|
+
db.prepare("DELETE FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId)
|
|
1448
|
+
]);
|
|
1449
|
+
if (existing) {
|
|
1450
|
+
await nullifyOrphanedReferences(event, entryId);
|
|
1451
|
+
await invalidateCMSEntryCache(event, existing.collection_id, existing.slug);
|
|
1452
|
+
await syncEntryToGit(event, existing.collection_id, rowToEntry(existing), "delete");
|
|
1453
|
+
fireCMSWebhooks(
|
|
1454
|
+
getCMSConfig(event),
|
|
1455
|
+
existing.collection_id,
|
|
1456
|
+
"delete",
|
|
1457
|
+
rowToEntry(existing)
|
|
1458
|
+
).catch(console.warn);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
export async function publishCMSEntry(event, entryId, targetStatus, targetPublishedAt) {
|
|
1462
|
+
const db = getCMSDb(event);
|
|
1463
|
+
const now = Date.now();
|
|
1464
|
+
const existing = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
|
|
1465
|
+
if (!existing) {
|
|
1466
|
+
throw createError({ statusCode: 404, message: "Entry not found" });
|
|
1467
|
+
}
|
|
1468
|
+
const tenantId = getCMSTenantId(event);
|
|
1469
|
+
if (tenantId && existing.tenant_id && existing.tenant_id !== tenantId) {
|
|
1470
|
+
throw createError({
|
|
1471
|
+
statusCode: 403,
|
|
1472
|
+
message: "Forbidden: Cannot publish an entry belonging to another tenant"
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
const publishedAt = targetStatus === "published" ? targetPublishedAt ?? existing.published_at ?? now : targetStatus === "scheduled" ? targetPublishedAt ?? existing.published_at ?? now : null;
|
|
1476
|
+
const collectionId = existing.collection_id;
|
|
1477
|
+
const config = getCMSConfig(event);
|
|
1478
|
+
const colConfig = config.collections[collectionId] || {};
|
|
1479
|
+
const approval = resolveCollectionApproval(colConfig, config.approval);
|
|
1480
|
+
let metadataJson = existing.metadata_json || "{}";
|
|
1481
|
+
if ((targetStatus === "published" || targetStatus === "scheduled") && approval) {
|
|
1482
|
+
const auth = await getAuthContext(event);
|
|
1483
|
+
const userRole = auth?.role || "user";
|
|
1484
|
+
const isAuthorized = auth?.isAdmin || approval.roles?.includes(userRole);
|
|
1485
|
+
if (!isAuthorized) {
|
|
1486
|
+
throw createError({
|
|
1487
|
+
statusCode: 403,
|
|
1488
|
+
message: `${targetStatus === "scheduled" ? "Scheduling" : "Publishing"} in "${collectionId}" requires one of the following approver roles: ${approval.roles?.join(", ")}.`
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
if (!approval.allowSelfApproval && existing.author_id && auth?.user?.id === existing.author_id) {
|
|
1492
|
+
throw createError({
|
|
1493
|
+
statusCode: 403,
|
|
1494
|
+
message: `Self-approval is disabled for "${collectionId}". Another team member must approve this entry.`
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
let metaObj = {};
|
|
1498
|
+
try {
|
|
1499
|
+
metaObj = JSON.parse(metadataJson);
|
|
1500
|
+
} catch {
|
|
1501
|
+
}
|
|
1502
|
+
metaObj._approval = {
|
|
1503
|
+
...metaObj._approval || {},
|
|
1504
|
+
approvedBy: auth?.user?.id || null,
|
|
1505
|
+
approvedByName: auth?.user?.name || auth?.user?.email || "Admin",
|
|
1506
|
+
approvedAt: now
|
|
1507
|
+
};
|
|
1508
|
+
metadataJson = JSON.stringify(metaObj);
|
|
1509
|
+
}
|
|
1510
|
+
await db.prepare(
|
|
1511
|
+
"UPDATE _qarpeo_cms_entries SET status = ?, published_at = ?, metadata_json = ?, updated_at = ? WHERE id = ?"
|
|
1512
|
+
).bind(targetStatus, publishedAt, metadataJson, now, entryId).run();
|
|
1513
|
+
const updatedRow = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
|
|
1514
|
+
if (updatedRow) {
|
|
1515
|
+
const updated = rowToEntry(updatedRow);
|
|
1516
|
+
await syncEntryToGit(
|
|
1517
|
+
event,
|
|
1518
|
+
updated.collectionId,
|
|
1519
|
+
updated,
|
|
1520
|
+
targetStatus === "published" ? "publish" : "update"
|
|
1521
|
+
);
|
|
1522
|
+
const webhookEvent = targetStatus === "published" ? "publish" : targetStatus === "scheduled" ? "update" : "unpublish";
|
|
1523
|
+
fireCMSWebhooks(config, updated.collectionId, webhookEvent, updated).catch(console.warn);
|
|
1524
|
+
if (targetStatus === "published" || targetStatus === "draft") {
|
|
1525
|
+
fireCMSDeployTrigger(
|
|
1526
|
+
config,
|
|
1527
|
+
updated.collectionId,
|
|
1528
|
+
targetStatus === "published" ? "publish" : "unpublish"
|
|
1529
|
+
).catch(console.warn);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
export async function publishDueScheduledEntries(event) {
|
|
1534
|
+
const db = getCMSDb(event);
|
|
1535
|
+
const now = Date.now();
|
|
1536
|
+
const config = getCMSConfig(event);
|
|
1537
|
+
const rows = await db.prepare(
|
|
1538
|
+
"SELECT * FROM _qarpeo_cms_entries WHERE status = 'scheduled' AND published_at IS NOT NULL AND published_at <= ?"
|
|
1539
|
+
).bind(now).all();
|
|
1540
|
+
const entriesToPublish = rows.results || [];
|
|
1541
|
+
const publishedEntries = [];
|
|
1542
|
+
for (const row of entriesToPublish) {
|
|
1543
|
+
const entryId = row.id;
|
|
1544
|
+
const collectionId = row.collection_id;
|
|
1545
|
+
await db.prepare("UPDATE _qarpeo_cms_entries SET status = 'published', updated_at = ? WHERE id = ?").bind(now, entryId).run();
|
|
1546
|
+
const updatedRow = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
|
|
1547
|
+
if (updatedRow) {
|
|
1548
|
+
const entry = rowToEntry(updatedRow);
|
|
1549
|
+
publishedEntries.push(entry);
|
|
1550
|
+
await syncEntryToGit(event, collectionId, entry, "publish").catch(console.warn);
|
|
1551
|
+
fireCMSWebhooks(config, collectionId, "publish", entry).catch(console.warn);
|
|
1552
|
+
fireCMSDeployTrigger(config, collectionId, "publish").catch(console.warn);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return publishedEntries;
|
|
1556
|
+
}
|
|
1557
|
+
export async function submitCMSEntryForReview(event, entryId, note) {
|
|
1558
|
+
const db = getCMSDb(event);
|
|
1559
|
+
const now = Date.now();
|
|
1560
|
+
const existing = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
|
|
1561
|
+
if (!existing) throw createError({ statusCode: 404, message: "Entry not found" });
|
|
1562
|
+
const tenantId = getCMSTenantId(event);
|
|
1563
|
+
if (tenantId && existing.tenant_id && existing.tenant_id !== tenantId) {
|
|
1564
|
+
throw createError({
|
|
1565
|
+
statusCode: 403,
|
|
1566
|
+
message: "Forbidden: Cannot modify an entry belonging to another tenant"
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
const auth = await getAuthContext(event);
|
|
1570
|
+
let metaObj = {};
|
|
1571
|
+
try {
|
|
1572
|
+
metaObj = JSON.parse(existing.metadata_json || "{}");
|
|
1573
|
+
} catch {
|
|
1574
|
+
}
|
|
1575
|
+
metaObj._approval = {
|
|
1576
|
+
...metaObj._approval || {},
|
|
1577
|
+
submittedBy: auth?.user?.id || null,
|
|
1578
|
+
submittedByName: auth?.user?.name || auth?.user?.email || "Editor",
|
|
1579
|
+
submittedAt: now,
|
|
1580
|
+
submissionNote: note || null
|
|
1581
|
+
};
|
|
1582
|
+
const metadataJson = JSON.stringify(metaObj);
|
|
1583
|
+
await db.prepare(
|
|
1584
|
+
"INSERT INTO _qarpeo_cms_revisions (id, entry_id, title, content_md, metadata_json, author_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
1585
|
+
).bind(
|
|
1586
|
+
nanoid(),
|
|
1587
|
+
entryId,
|
|
1588
|
+
existing.title,
|
|
1589
|
+
existing.content_md,
|
|
1590
|
+
metadataJson,
|
|
1591
|
+
auth?.user?.id || existing.author_id,
|
|
1592
|
+
now
|
|
1593
|
+
).run();
|
|
1594
|
+
await db.prepare(
|
|
1595
|
+
"UPDATE _qarpeo_cms_entries SET status = 'pending_review', metadata_json = ?, updated_at = ? WHERE id = ?"
|
|
1596
|
+
).bind(metadataJson, now, entryId).run();
|
|
1597
|
+
const updated = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
|
|
1598
|
+
return rowToEntry(updated);
|
|
1599
|
+
}
|
|
1600
|
+
export async function rejectCMSEntryReview(event, entryId, reason) {
|
|
1601
|
+
const db = getCMSDb(event);
|
|
1602
|
+
const now = Date.now();
|
|
1603
|
+
const existing = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
|
|
1604
|
+
if (!existing) throw createError({ statusCode: 404, message: "Entry not found" });
|
|
1605
|
+
const tenantId = getCMSTenantId(event);
|
|
1606
|
+
if (tenantId && existing.tenant_id && existing.tenant_id !== tenantId) {
|
|
1607
|
+
throw createError({
|
|
1608
|
+
statusCode: 403,
|
|
1609
|
+
message: "Forbidden: Cannot modify an entry belonging to another tenant"
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
const collectionId = existing.collection_id;
|
|
1613
|
+
const config = getCMSConfig(event);
|
|
1614
|
+
const colConfig = config.collections[collectionId] || {};
|
|
1615
|
+
const approval = resolveCollectionApproval(colConfig, config.approval);
|
|
1616
|
+
const auth = await getAuthContext(event);
|
|
1617
|
+
if (approval) {
|
|
1618
|
+
const userRole = auth?.role || "user";
|
|
1619
|
+
const isAuthorized = auth?.isAdmin || approval.roles?.includes(userRole);
|
|
1620
|
+
if (!isAuthorized) {
|
|
1621
|
+
throw createError({
|
|
1622
|
+
statusCode: 403,
|
|
1623
|
+
message: `Only reviewers with roles [${approval.roles?.join(", ")}] can request changes on entries in "${collectionId}".`
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
let metaObj = {};
|
|
1628
|
+
try {
|
|
1629
|
+
metaObj = JSON.parse(existing.metadata_json || "{}");
|
|
1630
|
+
} catch {
|
|
1631
|
+
}
|
|
1632
|
+
metaObj._approval = {
|
|
1633
|
+
...metaObj._approval || {},
|
|
1634
|
+
rejectedBy: auth?.user?.id || null,
|
|
1635
|
+
rejectedByName: auth?.user?.name || auth?.user?.email || "Reviewer",
|
|
1636
|
+
rejectedAt: now,
|
|
1637
|
+
rejectionReason: reason || null
|
|
1638
|
+
};
|
|
1639
|
+
const metadataJson = JSON.stringify(metaObj);
|
|
1640
|
+
await db.prepare(
|
|
1641
|
+
"UPDATE _qarpeo_cms_entries SET status = 'draft', metadata_json = ?, updated_at = ? WHERE id = ?"
|
|
1642
|
+
).bind(metadataJson, now, entryId).run();
|
|
1643
|
+
const updated = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
|
|
1644
|
+
return rowToEntry(updated);
|
|
1645
|
+
}
|
|
1646
|
+
export async function pruneRevisions(event, entryId, max) {
|
|
1647
|
+
const db = getCMSDb(event);
|
|
1648
|
+
await db.prepare(
|
|
1649
|
+
`DELETE FROM _qarpeo_cms_revisions WHERE entry_id = ? AND id NOT IN (
|
|
1650
|
+
SELECT id FROM _qarpeo_cms_revisions WHERE entry_id = ? ORDER BY created_at DESC LIMIT ?
|
|
1651
|
+
)`
|
|
1652
|
+
).bind(entryId, entryId, max).run();
|
|
1653
|
+
}
|
|
1654
|
+
async function getCMSSecret(event) {
|
|
1655
|
+
const rc = useRuntimeConfig(event);
|
|
1656
|
+
const secret = rc.appSecret || rc.qarpeo?.sessionPassword || rc.sessionPassword || rc.session?.password || globalThis.process?.env?.NUXT_APP_SECRET || globalThis.process?.env?.NUXT_SESSION_PASSWORD || "qarpeo-cms-preview-secret";
|
|
1657
|
+
const enc = new TextEncoder();
|
|
1658
|
+
return crypto.subtle.importKey(
|
|
1659
|
+
"raw",
|
|
1660
|
+
enc.encode(secret),
|
|
1661
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
1662
|
+
false,
|
|
1663
|
+
["sign", "verify"]
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
export async function generatePreviewToken(event, collection, slugOrId, ttlMs = 24 * 60 * 60 * 1e3) {
|
|
1667
|
+
const expires = Date.now() + ttlMs;
|
|
1668
|
+
const key = await getCMSSecret(event);
|
|
1669
|
+
const msg = new TextEncoder().encode(`${collection}:${slugOrId}:${expires}`);
|
|
1670
|
+
const sig = await crypto.subtle.sign("HMAC", key, msg);
|
|
1671
|
+
const token = btoa(String.fromCharCode(...new Uint8Array(sig)));
|
|
1672
|
+
return { token, expires };
|
|
1673
|
+
}
|
|
1674
|
+
export async function verifyPreviewToken(event, collection, slugOrId, token, expiresStr) {
|
|
1675
|
+
try {
|
|
1676
|
+
const expires = typeof expiresStr === "number" ? expiresStr : parseInt(String(expiresStr), 10);
|
|
1677
|
+
if (Number.isNaN(expires) || Date.now() > expires) return false;
|
|
1678
|
+
const key = await getCMSSecret(event);
|
|
1679
|
+
const sigBytes = Uint8Array.from(atob(token), (c) => c.charCodeAt(0));
|
|
1680
|
+
const msg = new TextEncoder().encode(`${collection}:${slugOrId}:${expires}`);
|
|
1681
|
+
const isValid = await crypto.subtle.verify("HMAC", key, sigBytes, msg);
|
|
1682
|
+
if (isValid) return true;
|
|
1683
|
+
try {
|
|
1684
|
+
const db = getCMSDb(event);
|
|
1685
|
+
const row = await db.prepare(
|
|
1686
|
+
"SELECT id, slug FROM _qarpeo_cms_entries WHERE collection_id = ? AND (id = ? OR slug = ?) LIMIT 1"
|
|
1687
|
+
).bind(collection, slugOrId, slugOrId).first();
|
|
1688
|
+
if (row) {
|
|
1689
|
+
const alt = row.id === slugOrId ? row.slug : row.id;
|
|
1690
|
+
if (alt) {
|
|
1691
|
+
const altMsg = new TextEncoder().encode(`${collection}:${alt}:${expires}`);
|
|
1692
|
+
return await crypto.subtle.verify("HMAC", key, sigBytes, altMsg);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
} catch {
|
|
1696
|
+
}
|
|
1697
|
+
return false;
|
|
1698
|
+
} catch {
|
|
1699
|
+
return false;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
export async function ensureCMSTables(event) {
|
|
1703
|
+
const db = getCMSDb(event);
|
|
1704
|
+
const statements = [
|
|
1705
|
+
`CREATE TABLE IF NOT EXISTS _qarpeo_cms_collections (id TEXT PRIMARY KEY, tenant_id TEXT, name TEXT NOT NULL, description TEXT, schema_json TEXT NOT NULL DEFAULT '{}', source TEXT NOT NULL DEFAULT 'db', "order" INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);`,
|
|
1706
|
+
`CREATE TABLE IF NOT EXISTS _qarpeo_cms_entries (id TEXT PRIMARY KEY, tenant_id TEXT, collection_id TEXT NOT NULL, slug TEXT NOT NULL, title TEXT NOT NULL, description TEXT, cover_image TEXT, tags TEXT, "order" INTEGER NOT NULL DEFAULT 0, content_md TEXT NOT NULL DEFAULT '', content_html TEXT NOT NULL DEFAULT '', metadata_json TEXT DEFAULT '{}', status TEXT NOT NULL DEFAULT 'draft', source TEXT NOT NULL DEFAULT 'db', path TEXT, file_path TEXT, content_hash TEXT, author_id TEXT, locale TEXT NOT NULL DEFAULT 'en', version TEXT NOT NULL DEFAULT 'latest', published_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);`,
|
|
1707
|
+
`CREATE TABLE IF NOT EXISTS _qarpeo_cms_revisions (id TEXT PRIMARY KEY, entry_id TEXT NOT NULL, title TEXT NOT NULL, content_md TEXT NOT NULL, metadata_json TEXT DEFAULT '{}', author_id TEXT, created_at INTEGER NOT NULL);`,
|
|
1708
|
+
// Matching Drizzle schema indexes
|
|
1709
|
+
`CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_collection_idx ON _qarpeo_cms_entries(collection_id);`,
|
|
1710
|
+
`CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_tenant_idx ON _qarpeo_cms_entries(tenant_id, collection_id);`,
|
|
1711
|
+
`CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_slug_idx ON _qarpeo_cms_entries(collection_id, slug);`,
|
|
1712
|
+
`CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_status_idx ON _qarpeo_cms_entries(status);`,
|
|
1713
|
+
`CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_order_idx ON _qarpeo_cms_entries(collection_id, "order");`,
|
|
1714
|
+
`CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_path_idx ON _qarpeo_cms_entries(collection_id, path);`,
|
|
1715
|
+
`CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_locale_idx ON _qarpeo_cms_entries(collection_id, locale);`,
|
|
1716
|
+
`CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_path_locale_idx ON _qarpeo_cms_entries(path, locale);`,
|
|
1717
|
+
`CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_version_idx ON _qarpeo_cms_entries(collection_id, version);`,
|
|
1718
|
+
`CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_path_version_idx ON _qarpeo_cms_entries(path, version);`,
|
|
1719
|
+
`CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_published_at_idx ON _qarpeo_cms_entries(published_at);`,
|
|
1720
|
+
`CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_list_idx ON _qarpeo_cms_entries(collection_id, status, published_at);`,
|
|
1721
|
+
`CREATE INDEX IF NOT EXISTS _qarpeo_cms_revisions_entry_idx ON _qarpeo_cms_revisions(entry_id, created_at);`
|
|
1722
|
+
];
|
|
1723
|
+
for (const sql of statements) {
|
|
1724
|
+
try {
|
|
1725
|
+
await db.exec(sql);
|
|
1726
|
+
} catch (err) {
|
|
1727
|
+
console.warn("[qarpeo:cms] ensureCMSTables warning on statement:", sql, err);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
try {
|
|
1731
|
+
await db.exec("ALTER TABLE _qarpeo_cms_entries ADD COLUMN path TEXT;");
|
|
1732
|
+
} catch {
|
|
1733
|
+
}
|
|
1734
|
+
try {
|
|
1735
|
+
await db.exec(
|
|
1736
|
+
'ALTER TABLE _qarpeo_cms_collections ADD COLUMN "order" INTEGER NOT NULL DEFAULT 0;'
|
|
1737
|
+
);
|
|
1738
|
+
} catch {
|
|
1739
|
+
}
|
|
1740
|
+
try {
|
|
1741
|
+
await db.exec('ALTER TABLE _qarpeo_cms_entries ADD COLUMN "order" INTEGER NOT NULL DEFAULT 0;');
|
|
1742
|
+
} catch {
|
|
1743
|
+
}
|
|
1744
|
+
try {
|
|
1745
|
+
await db.exec("ALTER TABLE _qarpeo_cms_entries ADD COLUMN tenant_id TEXT;");
|
|
1746
|
+
} catch {
|
|
1747
|
+
}
|
|
1748
|
+
try {
|
|
1749
|
+
await db.exec("ALTER TABLE _qarpeo_cms_collections ADD COLUMN tenant_id TEXT;");
|
|
1750
|
+
} catch {
|
|
1751
|
+
}
|
|
1752
|
+
try {
|
|
1753
|
+
await db.exec("ALTER TABLE _qarpeo_cms_entries ADD COLUMN locale TEXT NOT NULL DEFAULT 'en';");
|
|
1754
|
+
} catch {
|
|
1755
|
+
}
|
|
1756
|
+
try {
|
|
1757
|
+
await db.exec("ALTER TABLE _qarpeo_cms_entries ADD COLUMN version TEXT NOT NULL DEFAULT 'latest';");
|
|
1758
|
+
} catch {
|
|
1759
|
+
}
|
|
1760
|
+
try {
|
|
1761
|
+
await db.exec(
|
|
1762
|
+
"CREATE VIRTUAL TABLE IF NOT EXISTS _qarpeo_cms_entries_fts USING fts5(id UNINDEXED, slug, title, description, content_md, tags, tokenize = 'unicode61');"
|
|
1763
|
+
);
|
|
1764
|
+
} catch (err) {
|
|
1765
|
+
console.warn("[qarpeo:cms] ensureCMSTables FTS virtual table notice:", err);
|
|
1766
|
+
}
|
|
1767
|
+
try {
|
|
1768
|
+
await db.exec(
|
|
1769
|
+
"CREATE TRIGGER IF NOT EXISTS _qarpeo_cms_entries_fts_ai AFTER INSERT ON _qarpeo_cms_entries BEGIN INSERT INTO _qarpeo_cms_entries_fts(id, slug, title, description, content_md, tags) VALUES (new.id, new.slug, new.title, coalesce(new.description, ''), new.content_md, coalesce(new.tags, '')); END;"
|
|
1770
|
+
);
|
|
1771
|
+
} catch {
|
|
1772
|
+
}
|
|
1773
|
+
try {
|
|
1774
|
+
await db.exec(
|
|
1775
|
+
"CREATE TRIGGER IF NOT EXISTS _qarpeo_cms_entries_fts_ad AFTER DELETE ON _qarpeo_cms_entries BEGIN DELETE FROM _qarpeo_cms_entries_fts WHERE id = old.id; END;"
|
|
1776
|
+
);
|
|
1777
|
+
} catch {
|
|
1778
|
+
}
|
|
1779
|
+
try {
|
|
1780
|
+
await db.exec(
|
|
1781
|
+
"CREATE TRIGGER IF NOT EXISTS _qarpeo_cms_entries_fts_au AFTER UPDATE ON _qarpeo_cms_entries BEGIN DELETE FROM _qarpeo_cms_entries_fts WHERE id = old.id; INSERT INTO _qarpeo_cms_entries_fts(id, slug, title, description, content_md, tags) VALUES (new.id, new.slug, new.title, coalesce(new.description, ''), new.content_md, coalesce(new.tags, '')); END;"
|
|
1782
|
+
);
|
|
1783
|
+
} catch {
|
|
1784
|
+
}
|
|
1785
|
+
try {
|
|
1786
|
+
await db.exec(
|
|
1787
|
+
"INSERT INTO _qarpeo_cms_entries_fts(id, slug, title, description, content_md, tags) SELECT e.id, e.slug, e.title, coalesce(e.description, ''), e.content_md, coalesce(e.tags, '') FROM _qarpeo_cms_entries e WHERE NOT EXISTS (SELECT 1 FROM _qarpeo_cms_entries_fts fts WHERE fts.id = e.id);"
|
|
1788
|
+
);
|
|
1789
|
+
} catch {
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
let lastSyncTime = 0;
|
|
1793
|
+
let syncPromise = null;
|
|
1794
|
+
let hasInitialSynced = false;
|
|
1795
|
+
async function _runCMSSync(event) {
|
|
1796
|
+
try {
|
|
1797
|
+
if (!globalThis.__QARPEO_DEV_ENV__) {
|
|
1798
|
+
try {
|
|
1799
|
+
const { getOrInitDevProxy } = await import("./dev-proxy.mjs");
|
|
1800
|
+
const proxy = await getOrInitDevProxy();
|
|
1801
|
+
if (proxy?.env) {
|
|
1802
|
+
;
|
|
1803
|
+
globalThis.__QARPEO_DEV_ENV__ = proxy.env;
|
|
1804
|
+
}
|
|
1805
|
+
} catch (proxyErr) {
|
|
1806
|
+
console.warn("[qarpeo:cms] Could not init dev proxy in ensureCMSSynced:", proxyErr);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
await ensureCMSTables(event);
|
|
1810
|
+
const config = getCMSConfig(event);
|
|
1811
|
+
await seedCollectionsFromConfig(event, config.collections);
|
|
1812
|
+
await syncLocalContentFiles(
|
|
1813
|
+
event,
|
|
1814
|
+
config.contentDir,
|
|
1815
|
+
config.collections,
|
|
1816
|
+
config.sources
|
|
1817
|
+
);
|
|
1818
|
+
} catch (err) {
|
|
1819
|
+
console.error("[qarpeo:cms] Auto-sync failed:", err);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
let prodTablesChecked = false;
|
|
1823
|
+
export async function ensureCMSSynced(event) {
|
|
1824
|
+
if (process.env.NODE_ENV !== "production" || import.meta.dev) {
|
|
1825
|
+
if (!hasInitialSynced) {
|
|
1826
|
+
if (!syncPromise) {
|
|
1827
|
+
syncPromise = _runCMSSync(event).finally(() => {
|
|
1828
|
+
hasInitialSynced = true;
|
|
1829
|
+
lastSyncTime = Date.now();
|
|
1830
|
+
syncPromise = null;
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
await syncPromise;
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
if (syncPromise) {
|
|
1837
|
+
await syncPromise;
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
const now = Date.now();
|
|
1841
|
+
if (now - lastSyncTime > 3e4) {
|
|
1842
|
+
lastSyncTime = now;
|
|
1843
|
+
syncPromise = _runCMSSync(event).finally(() => {
|
|
1844
|
+
syncPromise = null;
|
|
1845
|
+
});
|
|
1846
|
+
await syncPromise;
|
|
1847
|
+
}
|
|
1848
|
+
} else {
|
|
1849
|
+
if (!prodTablesChecked) {
|
|
1850
|
+
prodTablesChecked = true;
|
|
1851
|
+
try {
|
|
1852
|
+
await ensureCMSTables(event);
|
|
1853
|
+
} catch (err) {
|
|
1854
|
+
console.warn("[qarpeo:cms] Production ensureCMSTables check failed:", err);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
export async function seedCollectionsFromConfig(event, collectionsConfig) {
|
|
1860
|
+
const db = getCMSDb(event);
|
|
1861
|
+
const now = Date.now();
|
|
1862
|
+
for (const [id, cfg] of Object.entries(collectionsConfig)) {
|
|
1863
|
+
const schemaJson = JSON.stringify(cfg.fields || {});
|
|
1864
|
+
const colName = cfg.name || id.charAt(0).toUpperCase() + id.slice(1).replace(/-/g, " ");
|
|
1865
|
+
const existing = await db.prepare("SELECT id, source, schema_json FROM _qarpeo_cms_collections WHERE id = ?").bind(id).first();
|
|
1866
|
+
if (!existing) {
|
|
1867
|
+
await db.prepare(
|
|
1868
|
+
"INSERT INTO _qarpeo_cms_collections (id, name, description, schema_json, source, created_at, updated_at) VALUES (?, ?, ?, ?, 'config', ?, ?)"
|
|
1869
|
+
).bind(id, colName, cfg.description ?? null, schemaJson, now, now).run();
|
|
1870
|
+
} else if (existing.source === "config" && existing.schema_json !== schemaJson) {
|
|
1871
|
+
await db.prepare(
|
|
1872
|
+
"UPDATE _qarpeo_cms_collections SET name = ?, description = ?, schema_json = ?, updated_at = ? WHERE id = ?"
|
|
1873
|
+
).bind(colName, cfg.description ?? null, schemaJson, now, id).run();
|
|
1874
|
+
try {
|
|
1875
|
+
const kv = getKVBinding(event);
|
|
1876
|
+
const tenantPrefix = getCMSTenantPrefix(event);
|
|
1877
|
+
await kv.delete(`${tenantPrefix}cms:schema:${id}`);
|
|
1878
|
+
} catch {
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
import { matchesSourceFilters, matchesGlobFilter } from "./cms-filter.mjs";
|
|
1884
|
+
export { matchesSourceFilters, matchesGlobFilter };
|
|
1885
|
+
export async function syncLocalContentFiles(event, contentDir, collectionsConfig = {}, sources = []) {
|
|
1886
|
+
let readdir, readFile, existsSync;
|
|
1887
|
+
try {
|
|
1888
|
+
const fs = await import("node:fs/promises");
|
|
1889
|
+
const fsSync = await import("node:fs");
|
|
1890
|
+
readdir = (dir) => fs.readdir(dir, { withFileTypes: true });
|
|
1891
|
+
readFile = (p) => fs.readFile(p, "utf8");
|
|
1892
|
+
existsSync = fsSync.existsSync;
|
|
1893
|
+
} catch {
|
|
1894
|
+
return { synced: 0, skipped: 0, inserted: 0 };
|
|
1895
|
+
}
|
|
1896
|
+
async function readNavJson(dir) {
|
|
1897
|
+
const navJsonPath = `${dir}/.navigation.json`;
|
|
1898
|
+
if (existsSync(navJsonPath)) {
|
|
1899
|
+
try {
|
|
1900
|
+
const raw = await readFile(navJsonPath);
|
|
1901
|
+
const clean = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
|
|
1902
|
+
return JSON.parse(clean);
|
|
1903
|
+
} catch {
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
const navYmlPath = `${dir}/.navigation.yml`;
|
|
1907
|
+
if (existsSync(navYmlPath)) {
|
|
1908
|
+
try {
|
|
1909
|
+
const raw = await readFile(navYmlPath);
|
|
1910
|
+
const { parse: parseYaml2 } = await import("yaml");
|
|
1911
|
+
return parseYaml2(raw) || {};
|
|
1912
|
+
} catch {
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
return {};
|
|
1916
|
+
}
|
|
1917
|
+
async function readSchemaJson(dir) {
|
|
1918
|
+
const schemaPath = `${dir}/.schema.json`;
|
|
1919
|
+
if (existsSync(schemaPath)) {
|
|
1920
|
+
try {
|
|
1921
|
+
const raw = await readFile(schemaPath);
|
|
1922
|
+
const clean = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
|
|
1923
|
+
const parsed = JSON.parse(clean);
|
|
1924
|
+
return {
|
|
1925
|
+
scope: parsed.scope || "cascade",
|
|
1926
|
+
fields: parsed.fields || {}
|
|
1927
|
+
};
|
|
1928
|
+
} catch {
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
return null;
|
|
1932
|
+
}
|
|
1933
|
+
const db = getCMSDb(event);
|
|
1934
|
+
const cmsConfig = getCMSConfig(event);
|
|
1935
|
+
let synced = 0, skipped = 0, inserted = 0;
|
|
1936
|
+
const now = Date.now();
|
|
1937
|
+
try {
|
|
1938
|
+
const existingFileRows = await db.prepare("SELECT id, file_path FROM _qarpeo_cms_entries WHERE source = 'file'").all();
|
|
1939
|
+
for (const r of existingFileRows?.results || []) {
|
|
1940
|
+
if (r.file_path && !existsSync(r.file_path)) {
|
|
1941
|
+
await db.prepare("DELETE FROM _qarpeo_cms_entries WHERE id = ?").bind(r.id).run();
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
} catch {
|
|
1945
|
+
}
|
|
1946
|
+
async function syncSource(source) {
|
|
1947
|
+
if (!source?.resolvedDir || !existsSync(source.resolvedDir)) return;
|
|
1948
|
+
let orderCounter = 0;
|
|
1949
|
+
const processDir = async (dir, relBase, sectionNavMeta) => {
|
|
1950
|
+
let entries;
|
|
1951
|
+
try {
|
|
1952
|
+
entries = await readdir(dir);
|
|
1953
|
+
} catch {
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
entries.sort((a, b) => a.name.localeCompare(b.name, void 0, { numeric: true }));
|
|
1957
|
+
let navMeta = await readNavJson(dir);
|
|
1958
|
+
if (navMeta.private === true || navMeta.draft === true || navMeta.exclude === true) {
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
const isTopLevelSection = Boolean(relBase && !relBase.includes("/"));
|
|
1962
|
+
const currentSectionNavMeta = sectionNavMeta || (isTopLevelSection ? {
|
|
1963
|
+
title: navMeta.title || stripOrderPrefix(relBase).charAt(0).toUpperCase() + stripOrderPrefix(relBase).slice(1).replace(/-/g, " "),
|
|
1964
|
+
icon: navMeta.icon,
|
|
1965
|
+
order: navMeta.order ?? extractOrderPrefix(relBase),
|
|
1966
|
+
navigation: navMeta.navigation,
|
|
1967
|
+
hidden: navMeta.hidden
|
|
1968
|
+
} : void 0);
|
|
1969
|
+
for (const entry of entries) {
|
|
1970
|
+
if (entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
|
|
1971
|
+
const subRel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
1972
|
+
if (entry.isDirectory()) {
|
|
1973
|
+
await processDir(`${dir}/${entry.name}`, subRel, currentSectionNavMeta);
|
|
1974
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1975
|
+
const combinedExclude = [
|
|
1976
|
+
...Array.isArray(cmsConfig?.exclude) ? cmsConfig.exclude : cmsConfig?.exclude ? [cmsConfig.exclude] : [],
|
|
1977
|
+
...Array.isArray(source.exclude) ? source.exclude : source.exclude ? [source.exclude] : []
|
|
1978
|
+
];
|
|
1979
|
+
if (!matchesSourceFilters(subRel, source.include, combinedExclude)) continue;
|
|
1980
|
+
const filePath = `${dir}/${entry.name}`;
|
|
1981
|
+
const rawContent = await readFile(filePath);
|
|
1982
|
+
const fileHash = await computeContentHash(rawContent);
|
|
1983
|
+
const { data: fm, content: body } = parseFrontmatter(rawContent);
|
|
1984
|
+
const rawRelSegments = subRel.replace(/\.md$/, "").split("/").filter(Boolean);
|
|
1985
|
+
const isReadme = entry.name.toLowerCase() === "readme.md";
|
|
1986
|
+
const isIndex = entry.name.replace(/\.md$/, "") === "index" || entry.name.replace(/\.md$/, "") === "01.index" || isReadme;
|
|
1987
|
+
const groupSegment = rawRelSegments.slice(0, -1).find(isGroupSegment);
|
|
1988
|
+
let groupMeta;
|
|
1989
|
+
if (groupSegment) {
|
|
1990
|
+
const groupOrder = extractOrderPrefix(groupSegment);
|
|
1991
|
+
const defaultGroupTitle = extractGroupTitle(groupSegment);
|
|
1992
|
+
const groupKey = stripOrderPrefix(groupSegment).replace(/^\(|\)$/g, "");
|
|
1993
|
+
const currentDirName = dir.replace(/\\/g, "/").split("/").filter(Boolean).pop();
|
|
1994
|
+
const isGroupDir = currentDirName === groupSegment;
|
|
1995
|
+
groupMeta = {
|
|
1996
|
+
key: groupKey,
|
|
1997
|
+
title: isGroupDir && navMeta.title ? navMeta.title : defaultGroupTitle,
|
|
1998
|
+
icon: isGroupDir && navMeta.icon ? navMeta.icon : void 0,
|
|
1999
|
+
order: groupOrder
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
const cleanedRelSegments = rawRelSegments.filter((seg) => !isGroupSegment(seg)).map(stripOrderPrefix).filter(Boolean);
|
|
2003
|
+
if (isIndex && cleanedRelSegments.length > 0) {
|
|
2004
|
+
cleanedRelSegments.pop();
|
|
2005
|
+
}
|
|
2006
|
+
const prefixSegments = (source.normalizedPrefix || "/").split("/").filter(Boolean);
|
|
2007
|
+
const combinedSegments = [...prefixSegments, ...cleanedRelSegments];
|
|
2008
|
+
const entryPath = "/" + combinedSegments.join("/");
|
|
2009
|
+
const defaultCollectionId = prefixSegments[0] || "content";
|
|
2010
|
+
const collectionId = resolveCollectionForPath(
|
|
2011
|
+
entryPath,
|
|
2012
|
+
collectionsConfig,
|
|
2013
|
+
defaultCollectionId
|
|
2014
|
+
);
|
|
2015
|
+
const slug = fm.slug || cleanedRelSegments[cleanedRelSegments.length - 1] || prefixSegments[prefixSegments.length - 1] || "index";
|
|
2016
|
+
const title = fm.title || slug;
|
|
2017
|
+
const srcOrder = typeof source.order === "number" ? source.order : 0;
|
|
2018
|
+
const existingCol = await db.prepare("SELECT id FROM _qarpeo_cms_collections WHERE id = ?").bind(collectionId).first();
|
|
2019
|
+
if (!existingCol) {
|
|
2020
|
+
const colName = collectionId.charAt(0).toUpperCase() + collectionId.slice(1).replace(/-/g, " ");
|
|
2021
|
+
await db.prepare(
|
|
2022
|
+
`INSERT INTO _qarpeo_cms_collections (id, name, description, schema_json, source, "order", created_at, updated_at) VALUES (?, ?, ?, '{}', 'file', ?, ?, ?)`
|
|
2023
|
+
).bind(
|
|
2024
|
+
collectionId,
|
|
2025
|
+
colName,
|
|
2026
|
+
`Auto-discovered ${colName} collection`,
|
|
2027
|
+
srcOrder,
|
|
2028
|
+
now,
|
|
2029
|
+
now
|
|
2030
|
+
).run();
|
|
2031
|
+
}
|
|
2032
|
+
const toc = extractTOC(body);
|
|
2033
|
+
const { ast, html: contentHtml } = await processMarkdown(body);
|
|
2034
|
+
const description = fm.description || null;
|
|
2035
|
+
const coverImage = fm.coverImage || fm.cover_image || null;
|
|
2036
|
+
const tags = Array.isArray(fm.tags) ? fm.tags.join(",") : fm.tags || null;
|
|
2037
|
+
const folderNum = rawRelSegments.length > 1 ? extractOrderPrefix(rawRelSegments[0]) : void 0;
|
|
2038
|
+
const effectiveFolderNum = folderNum ?? (typeof currentSectionNavMeta?.order === "number" ? currentSectionNavMeta.order : void 0);
|
|
2039
|
+
const folderOrder = effectiveFolderNum != null ? effectiveFolderNum * 1e3 : 5e4;
|
|
2040
|
+
let subfolderOffset = 0;
|
|
2041
|
+
if (rawRelSegments.length > 2) {
|
|
2042
|
+
const subDir = rawRelSegments[1];
|
|
2043
|
+
const subDirNum = extractOrderPrefix(subDir);
|
|
2044
|
+
const subOrder = subDirNum ?? (typeof groupMeta?.order === "number" ? groupMeta.order : typeof navMeta?.order === "number" ? navMeta.order : void 0);
|
|
2045
|
+
subfolderOffset = 500 + (subOrder != null ? subOrder * 50 : 100);
|
|
2046
|
+
}
|
|
2047
|
+
const fileNum = extractOrderPrefix(entry.name);
|
|
2048
|
+
const itemOrder = typeof fm.order === "number" ? fm.order : isIndex ? 0 : fileNum ?? (subfolderOffset > 0 ? 10 + orderCounter++ : 100 + orderCounter++);
|
|
2049
|
+
const order = srcOrder * 1e5 + folderOrder + subfolderOffset + itemOrder;
|
|
2050
|
+
const itemTitle = fm.navigation?.title || fm.title || slug;
|
|
2051
|
+
const {
|
|
2052
|
+
slug: _s,
|
|
2053
|
+
title: _t,
|
|
2054
|
+
description: _d,
|
|
2055
|
+
coverImage: _ci,
|
|
2056
|
+
tags: _ta,
|
|
2057
|
+
order: _o,
|
|
2058
|
+
...rest
|
|
2059
|
+
} = fm;
|
|
2060
|
+
void _s;
|
|
2061
|
+
void _t;
|
|
2062
|
+
void _d;
|
|
2063
|
+
void _ci;
|
|
2064
|
+
void _ta;
|
|
2065
|
+
void _o;
|
|
2066
|
+
const effectiveSection = currentSectionNavMeta?.title ? { title: currentSectionNavMeta.title, icon: currentSectionNavMeta.icon } : navMeta.title ? { title: navMeta.title, icon: navMeta.icon } : void 0;
|
|
2067
|
+
const isDraft = Boolean(
|
|
2068
|
+
fm.draft === true || fm.private === true || fm.status === "draft" || navMeta.draft === true || navMeta.private === true
|
|
2069
|
+
);
|
|
2070
|
+
const entryStatus = isDraft ? "draft" : "published";
|
|
2071
|
+
const resolvedRest = { ...rest };
|
|
2072
|
+
try {
|
|
2073
|
+
await resolveReferenceFieldsFromFrontmatter(event, collectionId, resolvedRest);
|
|
2074
|
+
} catch {
|
|
2075
|
+
}
|
|
2076
|
+
const metadataJson = JSON.stringify({
|
|
2077
|
+
...resolvedRest,
|
|
2078
|
+
navigation: fm.navigation,
|
|
2079
|
+
hidden: fm.hidden,
|
|
2080
|
+
draft: isDraft,
|
|
2081
|
+
_ast: ast,
|
|
2082
|
+
_toc: { links: toc },
|
|
2083
|
+
_nav: navMeta,
|
|
2084
|
+
_section: effectiveSection,
|
|
2085
|
+
_sectionOrder: effectiveFolderNum,
|
|
2086
|
+
_group: groupMeta,
|
|
2087
|
+
_itemTitle: itemTitle,
|
|
2088
|
+
_isIndex: isIndex,
|
|
2089
|
+
_source: source.from || "unknown",
|
|
2090
|
+
_sourceLabel: source.label || source.from || "source",
|
|
2091
|
+
_sourcePriority: source.priority ?? 0,
|
|
2092
|
+
_sourceOrder: source.order ?? srcOrder
|
|
2093
|
+
});
|
|
2094
|
+
const existing = await db.prepare(
|
|
2095
|
+
"SELECT id, source, content_hash, metadata_json, file_path, status FROM _qarpeo_cms_entries WHERE collection_id = ? AND path = ?"
|
|
2096
|
+
).bind(collectionId, entryPath).first();
|
|
2097
|
+
if (!existing) {
|
|
2098
|
+
const id = nanoid();
|
|
2099
|
+
await db.prepare(
|
|
2100
|
+
`INSERT INTO _qarpeo_cms_entries
|
|
2101
|
+
(id, collection_id, slug, path, title, description, cover_image, tags, "order", content_md, content_html, metadata_json, status, source, file_path, content_hash, published_at, created_at, updated_at)
|
|
2102
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'file', ?, ?, ?, ?, ?)`
|
|
2103
|
+
).bind(
|
|
2104
|
+
id,
|
|
2105
|
+
collectionId,
|
|
2106
|
+
slug,
|
|
2107
|
+
entryPath,
|
|
2108
|
+
title,
|
|
2109
|
+
description,
|
|
2110
|
+
coverImage,
|
|
2111
|
+
tags,
|
|
2112
|
+
order,
|
|
2113
|
+
body,
|
|
2114
|
+
contentHtml,
|
|
2115
|
+
metadataJson,
|
|
2116
|
+
entryStatus,
|
|
2117
|
+
filePath,
|
|
2118
|
+
fileHash,
|
|
2119
|
+
isDraft ? null : now,
|
|
2120
|
+
now,
|
|
2121
|
+
now
|
|
2122
|
+
).run();
|
|
2123
|
+
inserted++;
|
|
2124
|
+
} else if (existing.source === "file" && (existing.content_hash !== fileHash || existing.metadata_json !== metadataJson || existing.file_path !== filePath || existing.status !== entryStatus)) {
|
|
2125
|
+
await db.prepare(
|
|
2126
|
+
`UPDATE _qarpeo_cms_entries SET
|
|
2127
|
+
status = ?, slug = ?, path = ?, title = ?, description = ?, cover_image = ?, tags = ?, "order" = ?,
|
|
2128
|
+
content_md = ?, content_html = ?, metadata_json = ?,
|
|
2129
|
+
file_path = ?, content_hash = ?, source = 'file', updated_at = ?
|
|
2130
|
+
WHERE id = ?`
|
|
2131
|
+
).bind(
|
|
2132
|
+
entryStatus,
|
|
2133
|
+
slug,
|
|
2134
|
+
entryPath,
|
|
2135
|
+
title,
|
|
2136
|
+
description,
|
|
2137
|
+
coverImage,
|
|
2138
|
+
tags,
|
|
2139
|
+
order,
|
|
2140
|
+
body,
|
|
2141
|
+
contentHtml,
|
|
2142
|
+
metadataJson,
|
|
2143
|
+
filePath,
|
|
2144
|
+
fileHash,
|
|
2145
|
+
now,
|
|
2146
|
+
existing.id
|
|
2147
|
+
).run();
|
|
2148
|
+
synced++;
|
|
2149
|
+
} else {
|
|
2150
|
+
skipped++;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
};
|
|
2155
|
+
await processDir(source.resolvedDir, "");
|
|
2156
|
+
}
|
|
2157
|
+
const lowPrioritySources = (sources || []).filter((s) => (s.priority ?? 0) < 10);
|
|
2158
|
+
const highPrioritySources = (sources || []).filter((s) => (s.priority ?? 0) >= 10);
|
|
2159
|
+
for (const src of lowPrioritySources) {
|
|
2160
|
+
await syncSource(src);
|
|
2161
|
+
}
|
|
2162
|
+
const targetCollections = {};
|
|
2163
|
+
for (const [id, cfg] of Object.entries(collectionsConfig)) {
|
|
2164
|
+
targetCollections[id] = {
|
|
2165
|
+
directory: cfg.directory || `${contentDir}/${id}`,
|
|
2166
|
+
name: cfg.name,
|
|
2167
|
+
type: cfg.type || "content",
|
|
2168
|
+
order: typeof cfg.order === "number" ? cfg.order : 0
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
const rootMdFiles = [];
|
|
2172
|
+
try {
|
|
2173
|
+
const rootItems = await readdir(contentDir);
|
|
2174
|
+
for (const item of rootItems) {
|
|
2175
|
+
if (item.isDirectory()) {
|
|
2176
|
+
const colId = stripOrderPrefix(item.name).toLowerCase();
|
|
2177
|
+
if (!targetCollections[colId]) {
|
|
2178
|
+
targetCollections[colId] = {
|
|
2179
|
+
directory: `${contentDir}/${item.name}`,
|
|
2180
|
+
name: item.name.charAt(0).toUpperCase() + item.name.slice(1).replace(/-/g, " "),
|
|
2181
|
+
type: "content",
|
|
2182
|
+
order: extractOrderPrefix(item.name) ?? 0
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
} else if (item.isFile() && (item.name.endsWith(".md") || item.name.endsWith(".json") || item.name.endsWith(".yml") || item.name.endsWith(".yaml"))) {
|
|
2186
|
+
if (item.name.endsWith(".md")) {
|
|
2187
|
+
rootMdFiles.push(item);
|
|
2188
|
+
} else {
|
|
2189
|
+
const colId = item.name.replace(/\.(json|ya?ml)$/, "").toLowerCase();
|
|
2190
|
+
if (!targetCollections[colId]) {
|
|
2191
|
+
targetCollections[colId] = {
|
|
2192
|
+
directory: `${contentDir}/${item.name}`,
|
|
2193
|
+
name: colId.charAt(0).toUpperCase() + colId.slice(1).replace(/-/g, " "),
|
|
2194
|
+
type: "content",
|
|
2195
|
+
order: 0,
|
|
2196
|
+
isSingleDataFile: true
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
} catch {
|
|
2203
|
+
return { synced: 0, skipped: 0, inserted: 0 };
|
|
2204
|
+
}
|
|
2205
|
+
if (rootMdFiles.length > 0 && !targetCollections["content"]) {
|
|
2206
|
+
targetCollections["content"] = {
|
|
2207
|
+
directory: contentDir,
|
|
2208
|
+
name: "Content",
|
|
2209
|
+
type: "page",
|
|
2210
|
+
order: 100
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
for (const [collectionId, meta] of Object.entries(targetCollections)) {
|
|
2214
|
+
const isPageType = meta.type === "page";
|
|
2215
|
+
const folderSchema = await readSchemaJson(meta.directory);
|
|
2216
|
+
const initialFields = folderSchema?.fields ? JSON.stringify(folderSchema.fields) : "{}";
|
|
2217
|
+
const existingCol = await db.prepare("SELECT id, source, schema_json FROM _qarpeo_cms_collections WHERE id = ?").bind(collectionId).first();
|
|
2218
|
+
if (!existingCol) {
|
|
2219
|
+
const colName = meta.name || collectionId.charAt(0).toUpperCase() + collectionId.slice(1).replace(/-/g, " ");
|
|
2220
|
+
await db.prepare(
|
|
2221
|
+
`INSERT INTO _qarpeo_cms_collections (id, name, description, schema_json, source, "order", created_at, updated_at) VALUES (?, ?, ?, ?, 'file', ?, ?, ?)`
|
|
2222
|
+
).bind(
|
|
2223
|
+
collectionId,
|
|
2224
|
+
colName,
|
|
2225
|
+
`Auto-discovered ${colName} collection`,
|
|
2226
|
+
initialFields,
|
|
2227
|
+
meta.order,
|
|
2228
|
+
now,
|
|
2229
|
+
now
|
|
2230
|
+
).run();
|
|
2231
|
+
} else if (folderSchema?.fields && existingCol.schema_json !== initialFields && existingCol.source === "file") {
|
|
2232
|
+
await db.prepare("UPDATE _qarpeo_cms_collections SET schema_json = ?, updated_at = ? WHERE id = ?").bind(initialFields, now, collectionId).run();
|
|
2233
|
+
}
|
|
2234
|
+
if (meta.isSingleDataFile) {
|
|
2235
|
+
const filePath = meta.directory;
|
|
2236
|
+
let rawContent = "";
|
|
2237
|
+
try {
|
|
2238
|
+
rawContent = await readFile(filePath);
|
|
2239
|
+
} catch {
|
|
2240
|
+
continue;
|
|
2241
|
+
}
|
|
2242
|
+
const fileHash = await computeContentHash(rawContent);
|
|
2243
|
+
let parsed = {};
|
|
2244
|
+
if (filePath.endsWith(".json")) {
|
|
2245
|
+
try {
|
|
2246
|
+
parsed = JSON.parse(rawContent);
|
|
2247
|
+
} catch {
|
|
2248
|
+
}
|
|
2249
|
+
} else {
|
|
2250
|
+
try {
|
|
2251
|
+
const { parse: parseYaml2 } = await import("yaml");
|
|
2252
|
+
parsed = parseYaml2(rawContent) || {};
|
|
2253
|
+
} catch {
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
const slug = parsed.slug || collectionId;
|
|
2257
|
+
const title = parsed.title || slug;
|
|
2258
|
+
const description = parsed.description || null;
|
|
2259
|
+
const entryPath = `/${collectionId}`;
|
|
2260
|
+
const metadataJson = JSON.stringify(parsed);
|
|
2261
|
+
const existing = await db.prepare(
|
|
2262
|
+
"SELECT id, source, content_hash, metadata_json, file_path FROM _qarpeo_cms_entries WHERE collection_id = ? AND slug = ?"
|
|
2263
|
+
).bind(collectionId, slug).first();
|
|
2264
|
+
if (!existing) {
|
|
2265
|
+
const id = nanoid();
|
|
2266
|
+
await db.prepare(
|
|
2267
|
+
`INSERT INTO _qarpeo_cms_entries
|
|
2268
|
+
(id, collection_id, slug, path, title, description, cover_image, tags, "order", content_md, content_html, metadata_json, status, source, file_path, content_hash, published_at, created_at, updated_at)
|
|
2269
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, '', '', ?, 'published', 'file', ?, ?, ?, ?, ?)`
|
|
2270
|
+
).bind(
|
|
2271
|
+
id,
|
|
2272
|
+
collectionId,
|
|
2273
|
+
slug,
|
|
2274
|
+
entryPath,
|
|
2275
|
+
title,
|
|
2276
|
+
description,
|
|
2277
|
+
null,
|
|
2278
|
+
null,
|
|
2279
|
+
metadataJson,
|
|
2280
|
+
filePath,
|
|
2281
|
+
fileHash,
|
|
2282
|
+
now,
|
|
2283
|
+
now,
|
|
2284
|
+
now
|
|
2285
|
+
).run();
|
|
2286
|
+
inserted++;
|
|
2287
|
+
} else if (existing.source === "file" && (existing.content_hash !== fileHash || existing.metadata_json !== metadataJson)) {
|
|
2288
|
+
await db.prepare(
|
|
2289
|
+
`UPDATE _qarpeo_cms_entries SET
|
|
2290
|
+
slug = ?, path = ?, title = ?, description = ?,
|
|
2291
|
+
metadata_json = ?, file_path = ?, content_hash = ?, source = 'file', updated_at = ?
|
|
2292
|
+
WHERE id = ?`
|
|
2293
|
+
).bind(
|
|
2294
|
+
slug,
|
|
2295
|
+
entryPath,
|
|
2296
|
+
title,
|
|
2297
|
+
description,
|
|
2298
|
+
metadataJson,
|
|
2299
|
+
filePath,
|
|
2300
|
+
fileHash,
|
|
2301
|
+
now,
|
|
2302
|
+
existing.id
|
|
2303
|
+
).run();
|
|
2304
|
+
synced++;
|
|
2305
|
+
} else {
|
|
2306
|
+
skipped++;
|
|
2307
|
+
}
|
|
2308
|
+
continue;
|
|
2309
|
+
}
|
|
2310
|
+
if (isPageType) {
|
|
2311
|
+
let orderCounter = 0;
|
|
2312
|
+
const processDir = async (dir, relBase) => {
|
|
2313
|
+
let entries;
|
|
2314
|
+
try {
|
|
2315
|
+
entries = await readdir(dir);
|
|
2316
|
+
} catch {
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
entries.sort((a, b) => a.name.localeCompare(b.name, void 0, { numeric: true }));
|
|
2320
|
+
for (const entry of entries) {
|
|
2321
|
+
if (entry.name.startsWith(".")) continue;
|
|
2322
|
+
if (entry.isDirectory()) {
|
|
2323
|
+
if (collectionId === "content") continue;
|
|
2324
|
+
await processDir(`${dir}/${entry.name}`, `${relBase}/${entry.name}`);
|
|
2325
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2326
|
+
const filePath = `${dir}/${entry.name}`;
|
|
2327
|
+
const rawContent = await readFile(filePath);
|
|
2328
|
+
const fileHash = await computeContentHash(rawContent);
|
|
2329
|
+
const { data: fm, content: body } = parseFrontmatter(rawContent);
|
|
2330
|
+
const relFilePath = `${relBase}/${entry.name}`.replace(/^\//, "");
|
|
2331
|
+
const slug = fm.slug || stripOrderPrefix(entry.name.replace(/\.md$/, ""));
|
|
2332
|
+
const title = fm.title || slug;
|
|
2333
|
+
const navJson = await readNavJson(dir);
|
|
2334
|
+
const navMeta = {
|
|
2335
|
+
title: navJson.title,
|
|
2336
|
+
icon: navJson.icon
|
|
2337
|
+
};
|
|
2338
|
+
const rawRelSegments = relFilePath.replace(/\.md$/, "").split("/").filter(Boolean);
|
|
2339
|
+
const groupSegment = rawRelSegments.slice(0, -1).find(isGroupSegment);
|
|
2340
|
+
let groupMeta;
|
|
2341
|
+
if (groupSegment) {
|
|
2342
|
+
const groupOrder = extractOrderPrefix(groupSegment);
|
|
2343
|
+
const defaultGroupTitle = extractGroupTitle(groupSegment);
|
|
2344
|
+
const groupKey = stripOrderPrefix(groupSegment).replace(/^\(|\)$/g, "");
|
|
2345
|
+
const currentDirName = dir.replace(/\\/g, "/").split("/").filter(Boolean).pop();
|
|
2346
|
+
const isGroupDir = currentDirName === groupSegment;
|
|
2347
|
+
groupMeta = {
|
|
2348
|
+
key: groupKey,
|
|
2349
|
+
title: isGroupDir && navMeta.title ? navMeta.title : defaultGroupTitle,
|
|
2350
|
+
icon: isGroupDir && navMeta.icon ? navMeta.icon : void 0,
|
|
2351
|
+
order: groupOrder
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
const entryPath = computeEntryPath(relFilePath, collectionId);
|
|
2355
|
+
const targetCollectionId = resolveCollectionForPath(
|
|
2356
|
+
entryPath,
|
|
2357
|
+
collectionsConfig,
|
|
2358
|
+
collectionId
|
|
2359
|
+
);
|
|
2360
|
+
const toc = extractTOC(body);
|
|
2361
|
+
const { ast, html: contentHtml } = await processMarkdown(body);
|
|
2362
|
+
const description = fm.description || null;
|
|
2363
|
+
const coverImage = fm.coverImage || fm.cover_image || null;
|
|
2364
|
+
const tags = Array.isArray(fm.tags) ? fm.tags.join(",") : fm.tags || null;
|
|
2365
|
+
const order = fm.order ?? orderCounter++;
|
|
2366
|
+
const isIndex = entry.name.replace(/\.md$/, "") === "01.index" || entry.name.replace(/\.md$/, "") === "index";
|
|
2367
|
+
const itemTitle = fm.navigation?.title || fm.title || slug;
|
|
2368
|
+
const {
|
|
2369
|
+
slug: _s,
|
|
2370
|
+
title: _t,
|
|
2371
|
+
description: _d,
|
|
2372
|
+
coverImage: _ci,
|
|
2373
|
+
tags: _ta,
|
|
2374
|
+
order: _o,
|
|
2375
|
+
...rest
|
|
2376
|
+
} = fm;
|
|
2377
|
+
void _s;
|
|
2378
|
+
void _t;
|
|
2379
|
+
void _d;
|
|
2380
|
+
void _ci;
|
|
2381
|
+
void _ta;
|
|
2382
|
+
void _o;
|
|
2383
|
+
const metadataJson = JSON.stringify({
|
|
2384
|
+
...rest,
|
|
2385
|
+
_ast: ast,
|
|
2386
|
+
_toc: { links: toc },
|
|
2387
|
+
_nav: navMeta,
|
|
2388
|
+
_section: navMeta.title ? { title: navMeta.title, icon: navMeta.icon } : void 0,
|
|
2389
|
+
_group: groupMeta,
|
|
2390
|
+
_itemTitle: itemTitle,
|
|
2391
|
+
_isIndex: isIndex
|
|
2392
|
+
});
|
|
2393
|
+
const existing = await db.prepare(
|
|
2394
|
+
"SELECT id, source, content_hash, metadata_json, file_path FROM _qarpeo_cms_entries WHERE collection_id = ? AND path = ?"
|
|
2395
|
+
).bind(targetCollectionId, entryPath).first();
|
|
2396
|
+
if (!existing) {
|
|
2397
|
+
const id = nanoid();
|
|
2398
|
+
await db.prepare(
|
|
2399
|
+
`INSERT INTO _qarpeo_cms_entries
|
|
2400
|
+
(id, collection_id, slug, path, title, description, cover_image, tags, "order", content_md, content_html, metadata_json, status, source, file_path, content_hash, published_at, created_at, updated_at)
|
|
2401
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'published', 'file', ?, ?, ?, ?, ?)`
|
|
2402
|
+
).bind(
|
|
2403
|
+
id,
|
|
2404
|
+
targetCollectionId,
|
|
2405
|
+
slug,
|
|
2406
|
+
entryPath,
|
|
2407
|
+
title,
|
|
2408
|
+
description,
|
|
2409
|
+
coverImage,
|
|
2410
|
+
tags,
|
|
2411
|
+
order,
|
|
2412
|
+
body,
|
|
2413
|
+
contentHtml,
|
|
2414
|
+
metadataJson,
|
|
2415
|
+
filePath,
|
|
2416
|
+
fileHash,
|
|
2417
|
+
now,
|
|
2418
|
+
now,
|
|
2419
|
+
now
|
|
2420
|
+
).run();
|
|
2421
|
+
inserted++;
|
|
2422
|
+
} else if (existing.source === "file" && (existing.content_hash !== fileHash || existing.metadata_json !== metadataJson || existing.file_path !== filePath)) {
|
|
2423
|
+
await db.prepare(
|
|
2424
|
+
`UPDATE _qarpeo_cms_entries SET
|
|
2425
|
+
slug = ?, path = ?, title = ?, description = ?, cover_image = ?, tags = ?, "order" = ?,
|
|
2426
|
+
content_md = ?, content_html = ?, metadata_json = ?,
|
|
2427
|
+
file_path = ?, content_hash = ?, source = 'file', updated_at = ?
|
|
2428
|
+
WHERE id = ?`
|
|
2429
|
+
).bind(
|
|
2430
|
+
slug,
|
|
2431
|
+
entryPath,
|
|
2432
|
+
title,
|
|
2433
|
+
description,
|
|
2434
|
+
coverImage,
|
|
2435
|
+
tags,
|
|
2436
|
+
order,
|
|
2437
|
+
body,
|
|
2438
|
+
contentHtml,
|
|
2439
|
+
metadataJson,
|
|
2440
|
+
filePath,
|
|
2441
|
+
fileHash,
|
|
2442
|
+
now,
|
|
2443
|
+
existing.id
|
|
2444
|
+
).run();
|
|
2445
|
+
synced++;
|
|
2446
|
+
} else {
|
|
2447
|
+
skipped++;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
};
|
|
2452
|
+
await processDir(meta.directory, "");
|
|
2453
|
+
} else {
|
|
2454
|
+
let entries;
|
|
2455
|
+
try {
|
|
2456
|
+
entries = await readdir(meta.directory);
|
|
2457
|
+
} catch {
|
|
2458
|
+
continue;
|
|
2459
|
+
}
|
|
2460
|
+
for (const entry of entries) {
|
|
2461
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
2462
|
+
const filePath = `${meta.directory}/${entry.name}`;
|
|
2463
|
+
const rawContent = await readFile(filePath);
|
|
2464
|
+
const fileHash = await computeContentHash(rawContent);
|
|
2465
|
+
const { data: fm, content: body } = parseFrontmatter(rawContent);
|
|
2466
|
+
const slug = fm.slug || entry.name.replace(/\.md$/, "");
|
|
2467
|
+
const title = fm.title || slug;
|
|
2468
|
+
const { ast, html: contentHtml } = await processMarkdown(body);
|
|
2469
|
+
const description = fm.description || null;
|
|
2470
|
+
const coverImage = fm.coverImage || fm.cover_image || null;
|
|
2471
|
+
const tags = Array.isArray(fm.tags) ? fm.tags.join(",") : fm.tags || null;
|
|
2472
|
+
const order = fm.order || 0;
|
|
2473
|
+
const {
|
|
2474
|
+
slug: _s,
|
|
2475
|
+
title: _t,
|
|
2476
|
+
description: _d,
|
|
2477
|
+
coverImage: _ci,
|
|
2478
|
+
tags: _ta,
|
|
2479
|
+
order: _o,
|
|
2480
|
+
...rest
|
|
2481
|
+
} = fm;
|
|
2482
|
+
void _s;
|
|
2483
|
+
void _t;
|
|
2484
|
+
void _d;
|
|
2485
|
+
void _ci;
|
|
2486
|
+
void _ta;
|
|
2487
|
+
void _o;
|
|
2488
|
+
const metadataJson = JSON.stringify({
|
|
2489
|
+
...rest,
|
|
2490
|
+
_ast: ast,
|
|
2491
|
+
_source: "local",
|
|
2492
|
+
_sourceLabel: "content",
|
|
2493
|
+
_sourcePriority: 10
|
|
2494
|
+
});
|
|
2495
|
+
const existing = await db.prepare(
|
|
2496
|
+
"SELECT id, source, content_hash FROM _qarpeo_cms_entries WHERE collection_id = ? AND slug = ?"
|
|
2497
|
+
).bind(collectionId, slug).first();
|
|
2498
|
+
if (!existing) {
|
|
2499
|
+
const id = nanoid();
|
|
2500
|
+
await db.prepare(
|
|
2501
|
+
`INSERT INTO _qarpeo_cms_entries
|
|
2502
|
+
(id, collection_id, slug, title, description, cover_image, tags, "order", content_md, content_html, metadata_json, status, source, file_path, content_hash, published_at, created_at, updated_at)
|
|
2503
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'published', 'file', ?, ?, ?, ?, ?)`
|
|
2504
|
+
).bind(
|
|
2505
|
+
id,
|
|
2506
|
+
collectionId,
|
|
2507
|
+
slug,
|
|
2508
|
+
title,
|
|
2509
|
+
description,
|
|
2510
|
+
coverImage,
|
|
2511
|
+
tags,
|
|
2512
|
+
order,
|
|
2513
|
+
body,
|
|
2514
|
+
contentHtml,
|
|
2515
|
+
metadataJson,
|
|
2516
|
+
filePath,
|
|
2517
|
+
fileHash,
|
|
2518
|
+
now,
|
|
2519
|
+
now,
|
|
2520
|
+
now
|
|
2521
|
+
).run();
|
|
2522
|
+
inserted++;
|
|
2523
|
+
} else if (existing.source === "file" && existing.content_hash !== fileHash) {
|
|
2524
|
+
await db.prepare(
|
|
2525
|
+
`UPDATE _qarpeo_cms_entries SET
|
|
2526
|
+
title = ?, description = ?, cover_image = ?, tags = ?, "order" = ?,
|
|
2527
|
+
content_md = ?, content_html = ?, metadata_json = ?,
|
|
2528
|
+
file_path = ?, content_hash = ?, source = 'file', updated_at = ?
|
|
2529
|
+
WHERE id = ?`
|
|
2530
|
+
).bind(
|
|
2531
|
+
title,
|
|
2532
|
+
description,
|
|
2533
|
+
coverImage,
|
|
2534
|
+
tags,
|
|
2535
|
+
order,
|
|
2536
|
+
body,
|
|
2537
|
+
contentHtml,
|
|
2538
|
+
metadataJson,
|
|
2539
|
+
filePath,
|
|
2540
|
+
fileHash,
|
|
2541
|
+
now,
|
|
2542
|
+
existing.id
|
|
2543
|
+
).run();
|
|
2544
|
+
synced++;
|
|
2545
|
+
} else {
|
|
2546
|
+
skipped++;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
for (const src of highPrioritySources) {
|
|
2552
|
+
await syncSource(src);
|
|
2553
|
+
}
|
|
2554
|
+
return { synced, skipped, inserted };
|
|
2555
|
+
}
|
|
2556
|
+
export async function getCMSEntryByPath(event, collectionId, path, locale, options = {}) {
|
|
2557
|
+
await ensureCMSSynced(event);
|
|
2558
|
+
const db = getCMSDb(event);
|
|
2559
|
+
const cleanPath = path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
2560
|
+
const tenantId = getCMSTenantId(event);
|
|
2561
|
+
const tenantClause = tenantId ? " AND (tenant_id = ? OR tenant_id IS NULL)" : "";
|
|
2562
|
+
let allowDraft = false;
|
|
2563
|
+
const previewCookie = event ? getCookie(event, "qarpeo_preview") : null;
|
|
2564
|
+
let cookieObj = null;
|
|
2565
|
+
if (previewCookie) {
|
|
2566
|
+
try {
|
|
2567
|
+
cookieObj = typeof previewCookie === "string" ? JSON.parse(previewCookie) : previewCookie;
|
|
2568
|
+
} catch {
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
const isPreviewRequested = options.preview || Boolean(cookieObj);
|
|
2572
|
+
if (isPreviewRequested) {
|
|
2573
|
+
const auth = await getAuthContext(event).catch(() => null);
|
|
2574
|
+
const config = getCMSConfig(event);
|
|
2575
|
+
if (auth?.user && config.roles.some((r) => r === auth.role || auth.isAdmin)) {
|
|
2576
|
+
allowDraft = true;
|
|
2577
|
+
}
|
|
2578
|
+
if (!allowDraft) {
|
|
2579
|
+
const token = options.previewToken || cookieObj?.token;
|
|
2580
|
+
const expires = options.previewExpires || cookieObj?.expires;
|
|
2581
|
+
if (token && expires) {
|
|
2582
|
+
const candidate = await db.prepare(
|
|
2583
|
+
`SELECT id, slug, collection_id FROM _qarpeo_cms_entries WHERE (collection_id = ? OR ? = 'content') AND (path = ? OR path = ?) ${tenantClause} LIMIT 1`
|
|
2584
|
+
).bind(collectionId, collectionId, cleanPath, `${cleanPath}/`).first();
|
|
2585
|
+
if (candidate) {
|
|
2586
|
+
allowDraft = await verifyPreviewToken(
|
|
2587
|
+
event,
|
|
2588
|
+
candidate.collection_id,
|
|
2589
|
+
candidate.id,
|
|
2590
|
+
token,
|
|
2591
|
+
expires
|
|
2592
|
+
) || await verifyPreviewToken(
|
|
2593
|
+
event,
|
|
2594
|
+
candidate.collection_id,
|
|
2595
|
+
candidate.slug,
|
|
2596
|
+
token,
|
|
2597
|
+
expires
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
const statusFilter = allowDraft ? "status IN ('draft', 'published', 'scheduled')" : "status = 'published'";
|
|
2604
|
+
const statusOrder = allowDraft ? "CASE WHEN status = 'draft' THEN 1 WHEN status = 'scheduled' THEN 2 ELSE 3 END," : "";
|
|
2605
|
+
let row = null;
|
|
2606
|
+
if (locale) {
|
|
2607
|
+
const bindsLocale = tenantId ? [collectionId, cleanPath, `${cleanPath}/`, locale, tenantId] : [collectionId, cleanPath, `${cleanPath}/`, locale];
|
|
2608
|
+
row = await db.prepare(
|
|
2609
|
+
`SELECT * FROM _qarpeo_cms_entries WHERE collection_id = ? AND (path = ? OR path = ?) AND locale = ? AND ${statusFilter}${tenantClause} ORDER BY ${statusOrder} created_at DESC LIMIT 1`
|
|
2610
|
+
).bind(...bindsLocale).first();
|
|
2611
|
+
}
|
|
2612
|
+
const binds1 = tenantId ? [collectionId, cleanPath, `${cleanPath}/`, tenantId] : [collectionId, cleanPath, `${cleanPath}/`];
|
|
2613
|
+
if (!row) {
|
|
2614
|
+
row = await db.prepare(
|
|
2615
|
+
`SELECT * FROM _qarpeo_cms_entries WHERE collection_id = ? AND (path = ? OR path = ?) AND ${statusFilter}${tenantClause} ORDER BY ${statusOrder} CASE WHEN locale = 'en' THEN 1 ELSE 2 END LIMIT 1`
|
|
2616
|
+
).bind(...binds1).first();
|
|
2617
|
+
}
|
|
2618
|
+
if (!row && cleanPath.startsWith(`/${collectionId}`)) {
|
|
2619
|
+
const unPrefixed = cleanPath.slice(`/${collectionId}`.length) || "/";
|
|
2620
|
+
const binds2 = tenantId ? [collectionId, unPrefixed, `${unPrefixed}/`, tenantId] : [collectionId, unPrefixed, `${unPrefixed}/`];
|
|
2621
|
+
row = await db.prepare(
|
|
2622
|
+
`SELECT * FROM _qarpeo_cms_entries WHERE collection_id = ? AND (path = ? OR path = ?) AND ${statusFilter}${tenantClause} ORDER BY ${statusOrder} created_at DESC LIMIT 1`
|
|
2623
|
+
).bind(...binds2).first();
|
|
2624
|
+
}
|
|
2625
|
+
if (!row) {
|
|
2626
|
+
const binds3 = tenantId ? [cleanPath, `${cleanPath}/`, tenantId] : [cleanPath, `${cleanPath}/`];
|
|
2627
|
+
row = await db.prepare(
|
|
2628
|
+
`SELECT * FROM _qarpeo_cms_entries WHERE (path = ? OR path = ?) AND ${statusFilter}${tenantClause} ORDER BY ${statusOrder} created_at DESC LIMIT 1`
|
|
2629
|
+
).bind(...binds3).first();
|
|
2630
|
+
}
|
|
2631
|
+
if (!row) return null;
|
|
2632
|
+
const entry = rowToEntry(row);
|
|
2633
|
+
if ((!entry.body || !Array.isArray(entry.body.nodes)) && entry.contentMd) {
|
|
2634
|
+
const ast = await parseMarkdownAST(entry.contentMd);
|
|
2635
|
+
entry.body = {
|
|
2636
|
+
...ast,
|
|
2637
|
+
value: entry.contentMd,
|
|
2638
|
+
content: entry.contentMd,
|
|
2639
|
+
toc: entry.toc || { links: [] }
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
return entry;
|
|
2643
|
+
}
|
|
2644
|
+
export async function getCMSEntrySurroundings(event, collectionId, path, fields = []) {
|
|
2645
|
+
await ensureCMSSynced(event);
|
|
2646
|
+
const db = getCMSDb(event);
|
|
2647
|
+
const cleanPath = path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
2648
|
+
const pick = (entry) => {
|
|
2649
|
+
if (!entry) return null;
|
|
2650
|
+
if (!fields.length) return entry;
|
|
2651
|
+
const partial = { id: entry.id, slug: entry.slug, path: entry.path, title: entry.title };
|
|
2652
|
+
for (const f of fields) {
|
|
2653
|
+
if (f in entry) partial[f] = entry[f];
|
|
2654
|
+
}
|
|
2655
|
+
return partial;
|
|
2656
|
+
};
|
|
2657
|
+
try {
|
|
2658
|
+
const navTree = await getCMSNavigationTree(event, collectionId);
|
|
2659
|
+
const orderedPaths = [];
|
|
2660
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
2661
|
+
const collectPaths = (items) => {
|
|
2662
|
+
for (const item of items) {
|
|
2663
|
+
if (item.path && item.type !== "group") {
|
|
2664
|
+
const norm = item.path.length > 1 && item.path.endsWith("/") ? item.path.slice(0, -1) : item.path;
|
|
2665
|
+
if (!seenPaths.has(norm)) {
|
|
2666
|
+
seenPaths.add(norm);
|
|
2667
|
+
orderedPaths.push(norm);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
if (item.children?.length) {
|
|
2671
|
+
collectPaths(item.children);
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
collectPaths(navTree);
|
|
2676
|
+
let idx2 = orderedPaths.indexOf(cleanPath);
|
|
2677
|
+
if (idx2 === -1) {
|
|
2678
|
+
idx2 = orderedPaths.indexOf(`${cleanPath}/`);
|
|
2679
|
+
}
|
|
2680
|
+
if (idx2 !== -1) {
|
|
2681
|
+
const prevPath = idx2 > 0 ? orderedPaths[idx2 - 1] : null;
|
|
2682
|
+
const nextPath = idx2 < orderedPaths.length - 1 ? orderedPaths[idx2 + 1] : null;
|
|
2683
|
+
const [prevEntry, nextEntry] = await Promise.all([
|
|
2684
|
+
prevPath ? getCMSEntryByPath(event, collectionId, prevPath) : Promise.resolve(null),
|
|
2685
|
+
nextPath ? getCMSEntryByPath(event, collectionId, nextPath) : Promise.resolve(null)
|
|
2686
|
+
]);
|
|
2687
|
+
return [pick(prevEntry), pick(nextEntry)];
|
|
2688
|
+
}
|
|
2689
|
+
} catch (err) {
|
|
2690
|
+
console.warn("[qarpeo:cms] getCMSEntrySurroundings navTree lookup failed:", err);
|
|
2691
|
+
}
|
|
2692
|
+
const tenantId = getCMSTenantId(event);
|
|
2693
|
+
const tenantClause = tenantId ? " AND (tenant_id = ? OR tenant_id IS NULL)" : "";
|
|
2694
|
+
const binds = tenantId ? [collectionId, tenantId] : [collectionId];
|
|
2695
|
+
const all = await db.prepare(
|
|
2696
|
+
`SELECT * FROM _qarpeo_cms_entries WHERE collection_id = ? AND status = 'published'${tenantClause} ORDER BY "order" ASC`
|
|
2697
|
+
).bind(...binds).all();
|
|
2698
|
+
const rows = (all.results || []).map(rowToEntry);
|
|
2699
|
+
const idx = rows.findIndex((e) => e.path === cleanPath || e.path === `${cleanPath}/`);
|
|
2700
|
+
if (idx === -1) return [null, null];
|
|
2701
|
+
return [pick(rows[idx - 1]), pick(rows[idx + 1])];
|
|
2702
|
+
}
|
|
2703
|
+
function pruneTreeLevels(items, currentLevel, maxLevel) {
|
|
2704
|
+
if (currentLevel >= maxLevel) {
|
|
2705
|
+
for (const item of items) {
|
|
2706
|
+
delete item.children;
|
|
2707
|
+
}
|
|
2708
|
+
return items;
|
|
2709
|
+
}
|
|
2710
|
+
for (const item of items) {
|
|
2711
|
+
if (item.children) {
|
|
2712
|
+
pruneTreeLevels(item.children, currentLevel + 1, maxLevel);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
return items;
|
|
2716
|
+
}
|
|
2717
|
+
const CMS_FIRST_CLASS_COLUMNS = /* @__PURE__ */ new Set([
|
|
2718
|
+
"id",
|
|
2719
|
+
"slug",
|
|
2720
|
+
"path",
|
|
2721
|
+
"title",
|
|
2722
|
+
"description",
|
|
2723
|
+
"tags",
|
|
2724
|
+
"status",
|
|
2725
|
+
"source"
|
|
2726
|
+
]);
|
|
2727
|
+
function matchesWhereFilter(row, meta, where) {
|
|
2728
|
+
return Object.entries(where).every(([key, expected]) => {
|
|
2729
|
+
if (CMS_FIRST_CLASS_COLUMNS.has(key)) {
|
|
2730
|
+
const actual2 = row[key] ?? null;
|
|
2731
|
+
return actual2 === expected;
|
|
2732
|
+
}
|
|
2733
|
+
const actual = meta[key] !== void 0 ? meta[key] : null;
|
|
2734
|
+
return actual === expected;
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
export async function getCMSNavigationTree(event, collectionId, options) {
|
|
2738
|
+
await ensureCMSSynced(event);
|
|
2739
|
+
const db = getCMSDb(event);
|
|
2740
|
+
if (collectionId === "content") {
|
|
2741
|
+
const config = getCMSConfig(event);
|
|
2742
|
+
const configuredKeys = Object.keys(config.collections || {});
|
|
2743
|
+
const collections = await listCMSCollections(event);
|
|
2744
|
+
const valid = collections.filter((c) => {
|
|
2745
|
+
if (c.id.startsWith("_") || c.id === "content") return false;
|
|
2746
|
+
const colConf = config.collections?.[c.id];
|
|
2747
|
+
if (colConf && (colConf.navigation === false || colConf.hidden === true)) {
|
|
2748
|
+
return false;
|
|
2749
|
+
}
|
|
2750
|
+
return true;
|
|
2751
|
+
});
|
|
2752
|
+
if (configuredKeys.length) {
|
|
2753
|
+
valid.sort((a, b) => {
|
|
2754
|
+
const ia = configuredKeys.indexOf(a.id);
|
|
2755
|
+
const ib = configuredKeys.indexOf(b.id);
|
|
2756
|
+
if (ia !== -1 && ib !== -1) return ia - ib;
|
|
2757
|
+
if (ia !== -1) return -1;
|
|
2758
|
+
if (ib !== -1) return 1;
|
|
2759
|
+
return (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name);
|
|
2760
|
+
});
|
|
2761
|
+
} else {
|
|
2762
|
+
valid.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name));
|
|
2763
|
+
}
|
|
2764
|
+
let items = valid.map((c) => ({
|
|
2765
|
+
title: c.name,
|
|
2766
|
+
path: `/${c.id}`
|
|
2767
|
+
}));
|
|
2768
|
+
const targetStatus2 = options?.status || "published";
|
|
2769
|
+
const navRootTenantId = getCMSTenantId(event);
|
|
2770
|
+
let rootSql = `SELECT id, slug, path, title, "order", metadata_json, status FROM _qarpeo_cms_entries WHERE collection_id = 'content'`;
|
|
2771
|
+
const rootBinds = [];
|
|
2772
|
+
if (targetStatus2 !== "all") {
|
|
2773
|
+
rootSql += " AND status = ?";
|
|
2774
|
+
rootBinds.push(targetStatus2);
|
|
2775
|
+
}
|
|
2776
|
+
if (navRootTenantId) {
|
|
2777
|
+
rootSql += " AND (tenant_id = ? OR tenant_id IS NULL)";
|
|
2778
|
+
rootBinds.push(navRootTenantId);
|
|
2779
|
+
}
|
|
2780
|
+
rootSql += ' ORDER BY "order" ASC';
|
|
2781
|
+
const rootEntries = await db.prepare(rootSql).bind(...rootBinds).all();
|
|
2782
|
+
const rows2 = rootEntries.results || [];
|
|
2783
|
+
for (const r of rows2) {
|
|
2784
|
+
let meta = {};
|
|
2785
|
+
try {
|
|
2786
|
+
meta = JSON.parse(r.metadata_json || "{}");
|
|
2787
|
+
} catch {
|
|
2788
|
+
}
|
|
2789
|
+
if (meta.navigation === false || meta.hidden === true || meta.navigation?.hidden === true || meta._nav?.navigation === false || meta._nav?.hidden === true) {
|
|
2790
|
+
continue;
|
|
2791
|
+
}
|
|
2792
|
+
if (options?.where && !matchesWhereFilter(r, meta, options.where)) {
|
|
2793
|
+
continue;
|
|
2794
|
+
}
|
|
2795
|
+
const entryPath = r.path || (r.slug === "index" ? "/" : `/${r.slug}`);
|
|
2796
|
+
const title = meta.navigation?.title || meta._itemTitle || r.title;
|
|
2797
|
+
const icon = meta.navigation?.icon || meta._nav?.icon;
|
|
2798
|
+
items.push({
|
|
2799
|
+
title,
|
|
2800
|
+
path: entryPath,
|
|
2801
|
+
to: entryPath,
|
|
2802
|
+
icon
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2805
|
+
if (options?.skip != null || options?.limit != null) {
|
|
2806
|
+
const skip = options?.skip ?? 0;
|
|
2807
|
+
const limit = options?.limit ?? items.length;
|
|
2808
|
+
items = items.slice(skip, skip + limit);
|
|
2809
|
+
}
|
|
2810
|
+
return items;
|
|
2811
|
+
}
|
|
2812
|
+
const targetStatus = options?.status || "published";
|
|
2813
|
+
const navTenantId = getCMSTenantId(event);
|
|
2814
|
+
let sql = 'SELECT id, slug, path, title, "order", metadata_json, status FROM _qarpeo_cms_entries WHERE collection_id = ?';
|
|
2815
|
+
const binds = [collectionId];
|
|
2816
|
+
if (targetStatus !== "all") {
|
|
2817
|
+
sql += " AND status = ?";
|
|
2818
|
+
binds.push(targetStatus);
|
|
2819
|
+
}
|
|
2820
|
+
if (navTenantId) {
|
|
2821
|
+
sql += " AND (tenant_id = ? OR tenant_id IS NULL)";
|
|
2822
|
+
binds.push(navTenantId);
|
|
2823
|
+
}
|
|
2824
|
+
if (options?.locale) {
|
|
2825
|
+
sql += " AND (locale = ? OR locale = 'en' OR locale IS NULL)";
|
|
2826
|
+
binds.push(options.locale);
|
|
2827
|
+
}
|
|
2828
|
+
if (options?.path) {
|
|
2829
|
+
const searchPath = ("/" + options.path.replace(/\\/g, "/").replace(/^\/+/, "")).replace(
|
|
2830
|
+
/\/+$/,
|
|
2831
|
+
""
|
|
2832
|
+
);
|
|
2833
|
+
sql += " AND (path = ? OR path LIKE ?)";
|
|
2834
|
+
binds.push(searchPath, `${searchPath}/%`);
|
|
2835
|
+
}
|
|
2836
|
+
sql += ' ORDER BY "order" ASC';
|
|
2837
|
+
const all = await db.prepare(sql).bind(...binds).all();
|
|
2838
|
+
const rows = all.results || [];
|
|
2839
|
+
const sectionsMap = /* @__PURE__ */ new Map();
|
|
2840
|
+
for (const r of rows) {
|
|
2841
|
+
let meta = {};
|
|
2842
|
+
try {
|
|
2843
|
+
meta = JSON.parse(r.metadata_json || "{}");
|
|
2844
|
+
} catch {
|
|
2845
|
+
}
|
|
2846
|
+
const sectionMeta = meta._section || {};
|
|
2847
|
+
const isHiddenFromNav = meta.navigation === false || meta.hidden === true || meta.navigation?.hidden === true || meta._nav?.navigation === false || meta._nav?.hidden === true || sectionMeta?.navigation === false || sectionMeta?.hidden === true;
|
|
2848
|
+
if (isHiddenFromNav) {
|
|
2849
|
+
continue;
|
|
2850
|
+
}
|
|
2851
|
+
if (options?.where && !matchesWhereFilter(r, meta, options.where)) {
|
|
2852
|
+
continue;
|
|
2853
|
+
}
|
|
2854
|
+
const entryPath = r.path || `/${collectionId}/${r.slug}`;
|
|
2855
|
+
const segments = entryPath.split("/").filter(Boolean);
|
|
2856
|
+
const sectionPath = segments.length >= 2 ? `/${segments[0]}/${segments[1]}` : entryPath;
|
|
2857
|
+
const defaultSectionTitle = segments.length >= 2 ? segments[1].charAt(0).toUpperCase() + segments[1].slice(1).replace(/-/g, " ") : r.title;
|
|
2858
|
+
if (!sectionsMap.has(sectionPath)) {
|
|
2859
|
+
sectionsMap.set(sectionPath, {
|
|
2860
|
+
title: sectionMeta.title || defaultSectionTitle,
|
|
2861
|
+
path: sectionPath,
|
|
2862
|
+
icon: sectionMeta.icon,
|
|
2863
|
+
children: [],
|
|
2864
|
+
_order: meta._sectionOrder ?? (r.order ? Math.floor(r.order / 1e3) : 500)
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
const section = sectionsMap.get(sectionPath);
|
|
2868
|
+
const itemTitle = meta._itemTitle || r.title;
|
|
2869
|
+
if (sectionMeta.title) {
|
|
2870
|
+
section.title = sectionMeta.title;
|
|
2871
|
+
if (sectionMeta.icon) section.icon = sectionMeta.icon;
|
|
2872
|
+
}
|
|
2873
|
+
const isSubFeature = segments.length >= 4 && !meta._isIndex && segments[0] === "docs" && segments[1] === "features";
|
|
2874
|
+
let targetParent = section;
|
|
2875
|
+
if (isSubFeature) {
|
|
2876
|
+
const subFeaturePath = `/${segments[0]}/${segments[1]}/${segments[2]}`;
|
|
2877
|
+
let subFeatureNode = section.children?.find((c) => c.path === subFeaturePath);
|
|
2878
|
+
if (!subFeatureNode) {
|
|
2879
|
+
const subFeatureTitle = segments[2].charAt(0).toUpperCase() + segments[2].slice(1).replace(/-/g, " ");
|
|
2880
|
+
const newNode = {
|
|
2881
|
+
title: subFeatureTitle,
|
|
2882
|
+
path: subFeaturePath,
|
|
2883
|
+
children: []
|
|
2884
|
+
};
|
|
2885
|
+
newNode._groupOrder = 500;
|
|
2886
|
+
section.children = section.children || [];
|
|
2887
|
+
section.children.push(newNode);
|
|
2888
|
+
subFeatureNode = newNode;
|
|
2889
|
+
}
|
|
2890
|
+
if (!subFeatureNode.children) {
|
|
2891
|
+
subFeatureNode.children = [];
|
|
2892
|
+
}
|
|
2893
|
+
targetParent = subFeatureNode;
|
|
2894
|
+
}
|
|
2895
|
+
if (meta._group) {
|
|
2896
|
+
const groupKey = meta._group.key;
|
|
2897
|
+
let groupItem = targetParent.children?.find((c) => c._groupKey === groupKey);
|
|
2898
|
+
if (!groupItem) {
|
|
2899
|
+
groupItem = {
|
|
2900
|
+
title: meta._group.title,
|
|
2901
|
+
icon: meta._group.icon,
|
|
2902
|
+
type: "group",
|
|
2903
|
+
children: [],
|
|
2904
|
+
_groupKey: groupKey,
|
|
2905
|
+
_groupOrder: meta._group.order ?? 999
|
|
2906
|
+
};
|
|
2907
|
+
targetParent.children = targetParent.children || [];
|
|
2908
|
+
targetParent.children.push(groupItem);
|
|
2909
|
+
}
|
|
2910
|
+
const gi = groupItem;
|
|
2911
|
+
gi.children = gi.children || [];
|
|
2912
|
+
gi.children.push({
|
|
2913
|
+
title: itemTitle,
|
|
2914
|
+
path: entryPath,
|
|
2915
|
+
icon: meta.navigation?.icon || meta._nav?.icon
|
|
2916
|
+
});
|
|
2917
|
+
continue;
|
|
2918
|
+
}
|
|
2919
|
+
const minDirectLen = isSubFeature ? 4 : 3;
|
|
2920
|
+
if (segments.length >= minDirectLen) {
|
|
2921
|
+
if (segments.length === minDirectLen) {
|
|
2922
|
+
targetParent.children = targetParent.children || [];
|
|
2923
|
+
if (meta._isIndex && targetParent !== section) {
|
|
2924
|
+
targetParent.title = itemTitle;
|
|
2925
|
+
if (meta.navigation?.icon || meta._nav?.icon) {
|
|
2926
|
+
targetParent.icon = meta.navigation?.icon || meta._nav?.icon;
|
|
2927
|
+
}
|
|
2928
|
+
continue;
|
|
2929
|
+
}
|
|
2930
|
+
targetParent.children.push({
|
|
2931
|
+
title: itemTitle,
|
|
2932
|
+
path: entryPath,
|
|
2933
|
+
icon: meta.navigation?.icon || meta._nav?.icon
|
|
2934
|
+
});
|
|
2935
|
+
} else {
|
|
2936
|
+
const subGroupKey = segments.slice(0, segments.length - 1).join("/");
|
|
2937
|
+
let subGroup = targetParent.children?.find((c) => c._subGroupKey === subGroupKey);
|
|
2938
|
+
if (!subGroup) {
|
|
2939
|
+
const subTitle = meta._nav?.title || sectionMeta.title || segments[segments.length - 2].charAt(0).toUpperCase() + segments[segments.length - 2].slice(1);
|
|
2940
|
+
const subOrder = typeof meta._nav?.order === "number" ? 10 + meta._nav.order : 800;
|
|
2941
|
+
subGroup = {
|
|
2942
|
+
title: subTitle,
|
|
2943
|
+
icon: meta._nav?.icon || sectionMeta.icon,
|
|
2944
|
+
type: "group",
|
|
2945
|
+
children: [],
|
|
2946
|
+
_subGroupKey: subGroupKey,
|
|
2947
|
+
_groupOrder: subOrder
|
|
2948
|
+
};
|
|
2949
|
+
targetParent.children = targetParent.children || [];
|
|
2950
|
+
targetParent.children.push(subGroup);
|
|
2951
|
+
}
|
|
2952
|
+
if (subGroup) {
|
|
2953
|
+
subGroup.children = subGroup.children || [];
|
|
2954
|
+
subGroup.children.push({
|
|
2955
|
+
title: itemTitle,
|
|
2956
|
+
path: entryPath,
|
|
2957
|
+
icon: meta.navigation?.icon || meta._nav?.icon
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
} else {
|
|
2962
|
+
targetParent.children = targetParent.children || [];
|
|
2963
|
+
targetParent.children.unshift({
|
|
2964
|
+
title: itemTitle,
|
|
2965
|
+
path: entryPath,
|
|
2966
|
+
icon: meta.navigation?.icon || meta._nav?.icon
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
function cleanAndSortNode(item) {
|
|
2971
|
+
if (item.path && !item.to) {
|
|
2972
|
+
item.to = item.path;
|
|
2973
|
+
}
|
|
2974
|
+
if (item.children) {
|
|
2975
|
+
item.children.sort((a, b) => {
|
|
2976
|
+
if (a.path === item.path) return -1;
|
|
2977
|
+
if (b.path === item.path) return 1;
|
|
2978
|
+
const orderA = a._groupOrder ?? 500;
|
|
2979
|
+
const orderB = b._groupOrder ?? 500;
|
|
2980
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
2981
|
+
return 0;
|
|
2982
|
+
});
|
|
2983
|
+
for (const c of item.children) {
|
|
2984
|
+
if (c.path && !c.to) {
|
|
2985
|
+
c.to = c.path;
|
|
2986
|
+
}
|
|
2987
|
+
delete c._groupKey;
|
|
2988
|
+
delete c._groupOrder;
|
|
2989
|
+
delete c._subGroupKey;
|
|
2990
|
+
cleanAndSortNode(c);
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
for (const s of sectionsMap.values()) {
|
|
2995
|
+
cleanAndSortNode(s);
|
|
2996
|
+
}
|
|
2997
|
+
const sectionsList = Array.from(sectionsMap.values());
|
|
2998
|
+
sectionsList.sort((a, b) => {
|
|
2999
|
+
const orderA = a._order ?? 500;
|
|
3000
|
+
const orderB = b._order ?? 500;
|
|
3001
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
3002
|
+
return a.title.localeCompare(b.title);
|
|
3003
|
+
});
|
|
3004
|
+
for (const s of sectionsList) {
|
|
3005
|
+
if (s.path && !s.to) {
|
|
3006
|
+
s.to = s.path;
|
|
3007
|
+
}
|
|
3008
|
+
delete s._order;
|
|
3009
|
+
}
|
|
3010
|
+
let result = sectionsList;
|
|
3011
|
+
if (options?.unwrap) {
|
|
3012
|
+
if (result.length === 1 && result[0].children?.length) {
|
|
3013
|
+
result = result[0].children;
|
|
3014
|
+
} else {
|
|
3015
|
+
const collectionRoot = result.find((item) => item.path === `/${collectionId}`);
|
|
3016
|
+
if (collectionRoot?.children?.length) {
|
|
3017
|
+
result = collectionRoot.children;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
if (options?.path) {
|
|
3022
|
+
const searchPath = ("/" + options.path.replace(/\\/g, "/").replace(/^\/+/, "")).replace(
|
|
3023
|
+
/\/+$/,
|
|
3024
|
+
""
|
|
3025
|
+
);
|
|
3026
|
+
const matchedSection = sectionsList.find((s) => s.path === searchPath);
|
|
3027
|
+
if (matchedSection && matchedSection.children?.length) {
|
|
3028
|
+
result = matchedSection.children;
|
|
3029
|
+
} else {
|
|
3030
|
+
const findNode = (items) => {
|
|
3031
|
+
for (const item of items) {
|
|
3032
|
+
if (item.path === searchPath) return item;
|
|
3033
|
+
if (item.children) {
|
|
3034
|
+
const found = findNode(item.children);
|
|
3035
|
+
if (found) return found;
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
};
|
|
3039
|
+
const deepNode = findNode(sectionsList);
|
|
3040
|
+
if (deepNode) {
|
|
3041
|
+
result = deepNode.children?.length ? deepNode.children : [deepNode];
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
if (options?.levels != null && options.levels > 0) {
|
|
3046
|
+
pruneTreeLevels(result, 1, options.levels);
|
|
3047
|
+
}
|
|
3048
|
+
if (options?.skip != null || options?.limit != null) {
|
|
3049
|
+
const skip = options?.skip ?? 0;
|
|
3050
|
+
const limit = options?.limit ?? result.length;
|
|
3051
|
+
result = result.slice(skip, skip + limit);
|
|
3052
|
+
}
|
|
3053
|
+
return result;
|
|
3054
|
+
}
|
|
3055
|
+
export function buildFtsQuery(query, operator = "AND") {
|
|
3056
|
+
if (!query) return "";
|
|
3057
|
+
const clean = query.replace(/[^\p{L}\p{N}_\s]/gu, " ").trim();
|
|
3058
|
+
const rawTokens = clean.split(/\s+/).filter(Boolean);
|
|
3059
|
+
if (!rawTokens.length) return "";
|
|
3060
|
+
const reserved = /* @__PURE__ */ new Set(["and", "or", "not", "near"]);
|
|
3061
|
+
const tokens = rawTokens.filter((t) => !reserved.has(t.toLowerCase()));
|
|
3062
|
+
const effectiveTokens = tokens.length > 0 ? tokens : rawTokens;
|
|
3063
|
+
return effectiveTokens.map((t) => `"${t.replace(/"/g, '""')}"*`).join(` ${operator} `);
|
|
3064
|
+
}
|
|
3065
|
+
export async function rebuildCMSFTS(event) {
|
|
3066
|
+
const db = getCMSDb(event);
|
|
3067
|
+
try {
|
|
3068
|
+
await db.exec("DELETE FROM _qarpeo_cms_entries_fts;");
|
|
3069
|
+
await db.exec(
|
|
3070
|
+
"INSERT INTO _qarpeo_cms_entries_fts(id, slug, title, description, content_md, tags) SELECT id, slug, title, coalesce(description, ''), content_md, coalesce(tags, '') FROM _qarpeo_cms_entries;"
|
|
3071
|
+
);
|
|
3072
|
+
const res = await db.prepare("SELECT COUNT(*) as total FROM _qarpeo_cms_entries_fts").first();
|
|
3073
|
+
return { total: Number(res?.total || 0) };
|
|
3074
|
+
} catch (err) {
|
|
3075
|
+
console.warn("[qarpeo:cms] rebuildCMSFTS failed:", err);
|
|
3076
|
+
return { total: 0 };
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
export async function searchDocs(event, options = {}) {
|
|
3080
|
+
await ensureCMSSynced(event);
|
|
3081
|
+
const db = getCMSDb(event);
|
|
3082
|
+
const q = (options.q || "").trim();
|
|
3083
|
+
if (!q) {
|
|
3084
|
+
return { results: [], total: 0, query: "", mode: "fts" };
|
|
3085
|
+
}
|
|
3086
|
+
const limit = Math.min(100, Math.max(1, options.limit || 20));
|
|
3087
|
+
const locale = options.locale;
|
|
3088
|
+
const tenantId = getCMSTenantId(event);
|
|
3089
|
+
const words = q.toLowerCase().split(/\s+/).filter(Boolean);
|
|
3090
|
+
let rows = [];
|
|
3091
|
+
let modeUsed = "fts";
|
|
3092
|
+
if (options.mode !== "like") {
|
|
3093
|
+
try {
|
|
3094
|
+
const ftsQuery = buildFtsQuery(q, "AND");
|
|
3095
|
+
if (ftsQuery) {
|
|
3096
|
+
let ftsSql = `
|
|
3097
|
+
SELECT e.id, e.collection_id, e.slug, e.path, e.title, e.description, e.content_md, e.metadata_json, e.locale, fts.rank as fts_rank
|
|
3098
|
+
FROM _qarpeo_cms_entries e
|
|
3099
|
+
JOIN _qarpeo_cms_entries_fts fts ON e.id = fts.id
|
|
3100
|
+
WHERE fts MATCH ? AND e.status = 'published'
|
|
3101
|
+
`;
|
|
3102
|
+
const ftsBinds = [ftsQuery];
|
|
3103
|
+
if (tenantId) {
|
|
3104
|
+
ftsSql += " AND (e.tenant_id = ? OR e.tenant_id IS NULL)";
|
|
3105
|
+
ftsBinds.push(tenantId);
|
|
3106
|
+
}
|
|
3107
|
+
if (options.collection) {
|
|
3108
|
+
ftsSql += " AND e.collection_id = ?";
|
|
3109
|
+
ftsBinds.push(options.collection);
|
|
3110
|
+
} else {
|
|
3111
|
+
ftsSql += " AND (e.collection_id = 'docs' OR e.collection_id = 'content' OR e.path LIKE '/docs%' OR e.collection_id LIKE '%doc%')";
|
|
3112
|
+
}
|
|
3113
|
+
if (locale) {
|
|
3114
|
+
ftsSql += " AND (e.locale = ? OR e.locale = 'en')";
|
|
3115
|
+
ftsBinds.push(locale);
|
|
3116
|
+
}
|
|
3117
|
+
if (options.version && options.version !== "all") {
|
|
3118
|
+
if (options.version === "latest") {
|
|
3119
|
+
ftsSql += " AND (e.version = 'latest' OR e.version IS NULL OR e.path NOT LIKE '%/docs/v%')";
|
|
3120
|
+
} else {
|
|
3121
|
+
ftsSql += " AND (e.version = ? OR e.path LIKE ?)";
|
|
3122
|
+
ftsBinds.push(options.version, `%/docs/${options.version}%`);
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
ftsSql += " ORDER BY fts.rank LIMIT 50";
|
|
3126
|
+
const res = await db.prepare(ftsSql).bind(...ftsBinds).all();
|
|
3127
|
+
rows = res.results || [];
|
|
3128
|
+
if (rows.length === 0 && words.length > 1) {
|
|
3129
|
+
const ftsOrQuery = buildFtsQuery(q, "OR");
|
|
3130
|
+
if (ftsOrQuery && ftsOrQuery !== ftsQuery) {
|
|
3131
|
+
const orBinds = [...ftsBinds];
|
|
3132
|
+
orBinds[0] = ftsOrQuery;
|
|
3133
|
+
const orRes = await db.prepare(ftsSql).bind(...orBinds).all();
|
|
3134
|
+
rows = orRes.results || [];
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
} catch {
|
|
3139
|
+
rows = [];
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
if (rows.length === 0) {
|
|
3143
|
+
modeUsed = "like";
|
|
3144
|
+
const searchPattern = `%${words.join("%")}%`;
|
|
3145
|
+
let sql = `
|
|
3146
|
+
SELECT id, collection_id, slug, path, title, description, content_md, metadata_json, locale
|
|
3147
|
+
FROM _qarpeo_cms_entries
|
|
3148
|
+
WHERE status = 'published'
|
|
3149
|
+
`;
|
|
3150
|
+
const binds = [];
|
|
3151
|
+
if (tenantId) {
|
|
3152
|
+
sql += " AND (tenant_id = ? OR tenant_id IS NULL)";
|
|
3153
|
+
binds.push(tenantId);
|
|
3154
|
+
}
|
|
3155
|
+
if (options.collection) {
|
|
3156
|
+
sql += " AND collection_id = ?";
|
|
3157
|
+
binds.push(options.collection);
|
|
3158
|
+
} else {
|
|
3159
|
+
sql += " AND (collection_id = 'docs' OR collection_id = 'content' OR path LIKE '/docs%' OR collection_id LIKE '%doc%')";
|
|
3160
|
+
}
|
|
3161
|
+
if (locale) {
|
|
3162
|
+
sql += " AND (locale = ? OR locale = 'en')";
|
|
3163
|
+
binds.push(locale);
|
|
3164
|
+
}
|
|
3165
|
+
if (options.version && options.version !== "all") {
|
|
3166
|
+
if (options.version === "latest") {
|
|
3167
|
+
sql += " AND (version = 'latest' OR version IS NULL OR path NOT LIKE '%/docs/v%')";
|
|
3168
|
+
} else {
|
|
3169
|
+
sql += " AND (version = ? OR path LIKE ?)";
|
|
3170
|
+
binds.push(options.version, `%/docs/${options.version}%`);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
sql += " AND (LOWER(title) LIKE ? OR LOWER(description) LIKE ? OR LOWER(content_md) LIKE ? OR LOWER(tags) LIKE ?)";
|
|
3174
|
+
binds.push(searchPattern, searchPattern, searchPattern, searchPattern);
|
|
3175
|
+
sql += " LIMIT 50";
|
|
3176
|
+
const res = await db.prepare(sql).bind(...binds).all();
|
|
3177
|
+
rows = res.results || [];
|
|
3178
|
+
}
|
|
3179
|
+
const sections = [];
|
|
3180
|
+
for (const row of rows) {
|
|
3181
|
+
const pageTitle = row.title || "Documentation";
|
|
3182
|
+
const docPath = row.path || `/${row.collection_id}/${row.slug}`;
|
|
3183
|
+
const md = row.content_md || "";
|
|
3184
|
+
const headingLines = md.split("\n");
|
|
3185
|
+
const rawSections = [];
|
|
3186
|
+
let currentSection = {
|
|
3187
|
+
heading: pageTitle,
|
|
3188
|
+
level: 1,
|
|
3189
|
+
anchor: "",
|
|
3190
|
+
lines: []
|
|
3191
|
+
};
|
|
3192
|
+
rawSections.push(currentSection);
|
|
3193
|
+
for (const line of headingLines) {
|
|
3194
|
+
const match = line.match(/^(#{1,4})\s+(.+)$/);
|
|
3195
|
+
if (match) {
|
|
3196
|
+
const level = match[1].length;
|
|
3197
|
+
const headingText = match[2].trim().replace(/\s*\{#.*\}\s*$/, "");
|
|
3198
|
+
const slugAnchor = headingText.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-");
|
|
3199
|
+
currentSection = {
|
|
3200
|
+
heading: headingText,
|
|
3201
|
+
level,
|
|
3202
|
+
anchor: `#${slugAnchor}`,
|
|
3203
|
+
lines: []
|
|
3204
|
+
};
|
|
3205
|
+
rawSections.push(currentSection);
|
|
3206
|
+
} else {
|
|
3207
|
+
currentSection.lines.push(line);
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
for (const sec of rawSections) {
|
|
3211
|
+
const rawText = sec.lines.join(" ");
|
|
3212
|
+
const cleanContent = rawText.replace(/```[\s\S]*?```/g, " ").replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[*_~#]/g, "").replace(/\s+/g, " ").trim();
|
|
3213
|
+
const textLower = cleanContent.toLowerCase();
|
|
3214
|
+
const headingLower = sec.heading.toLowerCase();
|
|
3215
|
+
const pageTitleLower = pageTitle.toLowerCase();
|
|
3216
|
+
let score = 0;
|
|
3217
|
+
let matchIdx = -1;
|
|
3218
|
+
for (const w of words) {
|
|
3219
|
+
if (headingLower.includes(w)) {
|
|
3220
|
+
score += 15;
|
|
3221
|
+
}
|
|
3222
|
+
if (pageTitleLower.includes(w)) {
|
|
3223
|
+
score += 8;
|
|
3224
|
+
}
|
|
3225
|
+
const idx = textLower.indexOf(w);
|
|
3226
|
+
if (idx !== -1) {
|
|
3227
|
+
score += 5;
|
|
3228
|
+
if (matchIdx === -1 || idx < matchIdx) {
|
|
3229
|
+
matchIdx = idx;
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
if (score === 0 && row.fts_rank !== void 0 && sec === rawSections[0]) {
|
|
3234
|
+
score = 10;
|
|
3235
|
+
}
|
|
3236
|
+
if (score > 0) {
|
|
3237
|
+
let excerpt = "";
|
|
3238
|
+
if (matchIdx !== -1) {
|
|
3239
|
+
const start = Math.max(0, matchIdx - 40);
|
|
3240
|
+
const end = Math.min(cleanContent.length, matchIdx + 110);
|
|
3241
|
+
excerpt = (start > 0 ? "... " : "") + cleanContent.slice(start, end).trim() + (end < cleanContent.length ? " ..." : "");
|
|
3242
|
+
} else {
|
|
3243
|
+
excerpt = cleanContent.slice(0, 140) + (cleanContent.length > 140 ? " ..." : "");
|
|
3244
|
+
}
|
|
3245
|
+
const titles = sec.heading !== pageTitle ? [pageTitle, sec.heading] : [pageTitle];
|
|
3246
|
+
sections.push({
|
|
3247
|
+
id: `${docPath}${sec.anchor}`,
|
|
3248
|
+
path: docPath,
|
|
3249
|
+
title: sec.heading,
|
|
3250
|
+
pageTitle,
|
|
3251
|
+
titles,
|
|
3252
|
+
content: excerpt || row.description || pageTitle,
|
|
3253
|
+
section: sec.anchor || void 0,
|
|
3254
|
+
locale: row.locale || "en",
|
|
3255
|
+
level: sec.level || 1,
|
|
3256
|
+
score
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
sections.sort((a, b) => b.score - a.score);
|
|
3262
|
+
return {
|
|
3263
|
+
results: sections.slice(0, limit),
|
|
3264
|
+
total: sections.length,
|
|
3265
|
+
query: q,
|
|
3266
|
+
mode: modeUsed
|
|
3267
|
+
};
|
|
3268
|
+
}
|