emdash 0.0.0-b → 0.0.2
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/README.md +87 -43
- package/dist/adapters-BLMa4JGD.d.mts +106 -0
- package/dist/adapters-BLMa4JGD.d.mts.map +1 -0
- package/dist/apply-Bjfq_b4-.mjs +1293 -0
- package/dist/apply-Bjfq_b4-.mjs.map +1 -0
- package/dist/astro/index.d.mts +51 -0
- package/dist/astro/index.d.mts.map +1 -0
- package/dist/astro/index.mjs +1336 -0
- package/dist/astro/index.mjs.map +1 -0
- package/dist/astro/middleware/auth.d.mts +31 -0
- package/dist/astro/middleware/auth.d.mts.map +1 -0
- package/dist/astro/middleware/auth.mjs +654 -0
- package/dist/astro/middleware/auth.mjs.map +1 -0
- package/dist/astro/middleware/redirect.d.mts +22 -0
- package/dist/astro/middleware/redirect.d.mts.map +1 -0
- package/dist/astro/middleware/redirect.mjs +63 -0
- package/dist/astro/middleware/redirect.mjs.map +1 -0
- package/dist/astro/middleware/request-context.d.mts +18 -0
- package/dist/astro/middleware/request-context.d.mts.map +1 -0
- package/dist/astro/middleware/request-context.mjs +1310 -0
- package/dist/astro/middleware/request-context.mjs.map +1 -0
- package/dist/astro/middleware/setup.d.mts +20 -0
- package/dist/astro/middleware/setup.d.mts.map +1 -0
- package/dist/astro/middleware/setup.mjs +47 -0
- package/dist/astro/middleware/setup.mjs.map +1 -0
- package/dist/astro/middleware.d.mts +13 -0
- package/dist/astro/middleware.d.mts.map +1 -0
- package/dist/astro/middleware.mjs +1613 -0
- package/dist/astro/middleware.mjs.map +1 -0
- package/dist/astro/types.d.mts +250 -0
- package/dist/astro/types.d.mts.map +1 -0
- package/dist/astro/types.mjs +1 -0
- package/dist/base64-MBPo9ozB.mjs +59 -0
- package/dist/base64-MBPo9ozB.mjs.map +1 -0
- package/dist/byline-CL847F26.mjs +213 -0
- package/dist/byline-CL847F26.mjs.map +1 -0
- package/dist/bylines-C2a-2TGt.mjs +136 -0
- package/dist/bylines-C2a-2TGt.mjs.map +1 -0
- package/dist/chunk-ClPoSABd.mjs +21 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +3909 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/client/cf-access.d.mts +60 -0
- package/dist/client/cf-access.d.mts.map +1 -0
- package/dist/client/cf-access.mjs +179 -0
- package/dist/client/cf-access.mjs.map +1 -0
- package/dist/client/index.d.mts +398 -0
- package/dist/client/index.d.mts.map +1 -0
- package/dist/client/index.mjs +346 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/config-CKE8p9xM.mjs +55 -0
- package/dist/config-CKE8p9xM.mjs.map +1 -0
- package/dist/connection-B4zVnQIa.mjs +40 -0
- package/dist/connection-B4zVnQIa.mjs.map +1 -0
- package/dist/content-D6C2WsZC.mjs +824 -0
- package/dist/content-D6C2WsZC.mjs.map +1 -0
- package/dist/db/index.d.mts +4 -0
- package/dist/db/index.mjs +62 -0
- package/dist/db/index.mjs.map +1 -0
- package/dist/db/libsql.d.mts +11 -0
- package/dist/db/libsql.d.mts.map +1 -0
- package/dist/db/libsql.mjs +17 -0
- package/dist/db/libsql.mjs.map +1 -0
- package/dist/db/postgres.d.mts +11 -0
- package/dist/db/postgres.d.mts.map +1 -0
- package/dist/db/postgres.mjs +30 -0
- package/dist/db/postgres.mjs.map +1 -0
- package/dist/db/sqlite.d.mts +11 -0
- package/dist/db/sqlite.d.mts.map +1 -0
- package/dist/db/sqlite.mjs +16 -0
- package/dist/db/sqlite.mjs.map +1 -0
- package/dist/default-Cyi4aAxu.mjs +81 -0
- package/dist/default-Cyi4aAxu.mjs.map +1 -0
- package/dist/dialect-helpers-B9uSp2GJ.mjs +90 -0
- package/dist/dialect-helpers-B9uSp2GJ.mjs.map +1 -0
- package/dist/error-Cxz0tQeO.mjs +27 -0
- package/dist/error-Cxz0tQeO.mjs.map +1 -0
- package/dist/index-C1xF3OGh.d.mts +4527 -0
- package/dist/index-C1xF3OGh.d.mts.map +1 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.mjs +30 -0
- package/dist/load-yOOlckBj.mjs +28 -0
- package/dist/load-yOOlckBj.mjs.map +1 -0
- package/dist/loader-fz8Q_3EO.mjs +447 -0
- package/dist/loader-fz8Q_3EO.mjs.map +1 -0
- package/dist/manifest-schema-Dcl0R6nM.mjs +184 -0
- package/dist/manifest-schema-Dcl0R6nM.mjs.map +1 -0
- package/dist/media/index.d.mts +26 -0
- package/dist/media/index.d.mts.map +1 -0
- package/dist/media/index.mjs +55 -0
- package/dist/media/index.mjs.map +1 -0
- package/dist/media/local-runtime.d.mts +39 -0
- package/dist/media/local-runtime.d.mts.map +1 -0
- package/dist/media/local-runtime.mjs +133 -0
- package/dist/media/local-runtime.mjs.map +1 -0
- package/dist/media-DqHVh136.mjs +200 -0
- package/dist/media-DqHVh136.mjs.map +1 -0
- package/dist/mode-C2EzN1uE.mjs +23 -0
- package/dist/mode-C2EzN1uE.mjs.map +1 -0
- package/dist/page/index.d.mts +140 -0
- package/dist/page/index.d.mts.map +1 -0
- package/dist/page/index.mjs +416 -0
- package/dist/page/index.mjs.map +1 -0
- package/dist/placeholder-CmGAmqeO.d.mts +276 -0
- package/dist/placeholder-CmGAmqeO.d.mts.map +1 -0
- package/dist/placeholder-SmpOx-_v.mjs +243 -0
- package/dist/placeholder-SmpOx-_v.mjs.map +1 -0
- package/dist/plugin-utils.d.mts +58 -0
- package/dist/plugin-utils.d.mts.map +1 -0
- package/dist/plugin-utils.mjs +78 -0
- package/dist/plugin-utils.mjs.map +1 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts +22 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -0
- package/dist/plugins/adapt-sandbox-entry.mjs +113 -0
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -0
- package/dist/query-CS_iSj34.mjs +460 -0
- package/dist/query-CS_iSj34.mjs.map +1 -0
- package/dist/redirect-DIfIni3r.mjs +329 -0
- package/dist/redirect-DIfIni3r.mjs.map +1 -0
- package/dist/registry-D_w5HW4G.mjs +863 -0
- package/dist/registry-D_w5HW4G.mjs.map +1 -0
- package/dist/request-context.d.mts +49 -0
- package/dist/request-context.d.mts.map +1 -0
- package/dist/request-context.mjs +43 -0
- package/dist/request-context.mjs.map +1 -0
- package/dist/runner-C0hCbYnD.mjs +1412 -0
- package/dist/runner-C0hCbYnD.mjs.map +1 -0
- package/dist/runner-EAtf0ZIe.d.mts +27 -0
- package/dist/runner-EAtf0ZIe.d.mts.map +1 -0
- package/dist/runtime.d.mts +26 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +42 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/search-DG603UrT.mjs +9211 -0
- package/dist/search-DG603UrT.mjs.map +1 -0
- package/dist/seed/index.d.mts +3 -0
- package/dist/seed/index.mjs +15 -0
- package/dist/seo/index.d.mts +70 -0
- package/dist/seo/index.d.mts.map +1 -0
- package/dist/seo/index.mjs +70 -0
- package/dist/seo/index.mjs.map +1 -0
- package/dist/storage/local.d.mts +39 -0
- package/dist/storage/local.d.mts.map +1 -0
- package/dist/storage/local.mjs +166 -0
- package/dist/storage/local.mjs.map +1 -0
- package/dist/storage/s3.d.mts +32 -0
- package/dist/storage/s3.d.mts.map +1 -0
- package/dist/storage/s3.mjs +175 -0
- package/dist/storage/s3.mjs.map +1 -0
- package/dist/tokens-DpgrkrXK.mjs +171 -0
- package/dist/tokens-DpgrkrXK.mjs.map +1 -0
- package/dist/transport-BFGblqwG.d.mts +42 -0
- package/dist/transport-BFGblqwG.d.mts.map +1 -0
- package/dist/transport-yxiQsi8I.mjs +418 -0
- package/dist/transport-yxiQsi8I.mjs.map +1 -0
- package/dist/types-BRuPJGdV.d.mts +102 -0
- package/dist/types-BRuPJGdV.d.mts.map +1 -0
- package/dist/types-C4-fAxN3.d.mts +182 -0
- package/dist/types-C4-fAxN3.d.mts.map +1 -0
- package/dist/types-CMMN0pNg.mjs +31 -0
- package/dist/types-CMMN0pNg.mjs.map +1 -0
- package/dist/types-CUBbjgmP.mjs +16 -0
- package/dist/types-CUBbjgmP.mjs.map +1 -0
- package/dist/types-DRjfYOEv.d.mts +426 -0
- package/dist/types-DRjfYOEv.d.mts.map +1 -0
- package/dist/types-DY5zk5HN.mjs +73 -0
- package/dist/types-DY5zk5HN.mjs.map +1 -0
- package/dist/types-DaNLHo_T.d.mts +184 -0
- package/dist/types-DaNLHo_T.d.mts.map +1 -0
- package/dist/types-DvhsUmSJ.d.mts +1111 -0
- package/dist/types-DvhsUmSJ.d.mts.map +1 -0
- package/dist/validate-CpBtVMsD.d.mts +378 -0
- package/dist/validate-CpBtVMsD.d.mts.map +1 -0
- package/dist/validate-CqRJb_xU.mjs +97 -0
- package/dist/validate-CqRJb_xU.mjs.map +1 -0
- package/dist/validate-O7PWmlnq.mjs +328 -0
- package/dist/validate-O7PWmlnq.mjs.map +1 -0
- package/locals.d.ts +46 -0
- package/package.json +233 -19
- package/src/api/authorize.ts +63 -0
- package/src/api/csrf.ts +48 -0
- package/src/api/error.ts +99 -0
- package/src/api/errors.ts +445 -0
- package/src/api/escape.ts +9 -0
- package/src/api/handlers/api-tokens.ts +240 -0
- package/src/api/handlers/comments.ts +314 -0
- package/src/api/handlers/content.ts +1315 -0
- package/src/api/handlers/dashboard.ts +205 -0
- package/src/api/handlers/device-flow.ts +684 -0
- package/src/api/handlers/index.ts +163 -0
- package/src/api/handlers/manifest.ts +158 -0
- package/src/api/handlers/marketplace.ts +930 -0
- package/src/api/handlers/media.ts +207 -0
- package/src/api/handlers/menus.ts +493 -0
- package/src/api/handlers/oauth-authorization.ts +429 -0
- package/src/api/handlers/oauth-clients.ts +349 -0
- package/src/api/handlers/oauth-user-lookup.ts +39 -0
- package/src/api/handlers/plugins.ts +254 -0
- package/src/api/handlers/redirects.ts +360 -0
- package/src/api/handlers/revision.ts +145 -0
- package/src/api/handlers/schema.ts +534 -0
- package/src/api/handlers/sections.ts +289 -0
- package/src/api/handlers/seo.ts +115 -0
- package/src/api/handlers/settings.ts +49 -0
- package/src/api/handlers/snapshot.ts +350 -0
- package/src/api/handlers/taxonomies.ts +523 -0
- package/src/api/index.ts +6 -0
- package/src/api/openapi/document.ts +2368 -0
- package/src/api/openapi/index.ts +1 -0
- package/src/api/parse.ts +139 -0
- package/src/api/redirect.ts +14 -0
- package/src/api/rev.ts +67 -0
- package/src/api/schemas/auth.ts +112 -0
- package/src/api/schemas/bylines.ts +85 -0
- package/src/api/schemas/comments.ts +117 -0
- package/src/api/schemas/common.ts +89 -0
- package/src/api/schemas/content.ts +191 -0
- package/src/api/schemas/import.ts +52 -0
- package/src/api/schemas/index.ts +17 -0
- package/src/api/schemas/media.ts +116 -0
- package/src/api/schemas/menus.ts +111 -0
- package/src/api/schemas/redirects.ts +155 -0
- package/src/api/schemas/schema.ts +203 -0
- package/src/api/schemas/search.ts +63 -0
- package/src/api/schemas/sections.ts +67 -0
- package/src/api/schemas/settings.ts +63 -0
- package/src/api/schemas/setup.ts +37 -0
- package/src/api/schemas/taxonomies.ts +113 -0
- package/src/api/schemas/users.ts +96 -0
- package/src/api/schemas/widgets.ts +80 -0
- package/src/api/site-url.ts +25 -0
- package/src/api/types.ts +82 -0
- package/src/astro/index.ts +27 -0
- package/src/astro/integration/index.ts +303 -0
- package/src/astro/integration/routes.ts +834 -0
- package/src/astro/integration/runtime.ts +338 -0
- package/src/astro/integration/virtual-modules.ts +469 -0
- package/src/astro/integration/vite-config.ts +335 -0
- package/src/astro/middleware/auth.ts +743 -0
- package/src/astro/middleware/redirect.ts +89 -0
- package/src/astro/middleware/request-context.ts +129 -0
- package/src/astro/middleware/setup.ts +89 -0
- package/src/astro/middleware.ts +398 -0
- package/src/astro/routes/PluginRegistry.tsx +15 -0
- package/src/astro/routes/admin.astro +81 -0
- package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
- package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
- package/src/astro/routes/api/admin/api-tokens/[id].ts +40 -0
- package/src/astro/routes/api/admin/api-tokens/index.ts +68 -0
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +87 -0
- package/src/astro/routes/api/admin/bylines/index.ts +72 -0
- package/src/astro/routes/api/admin/comments/[id]/status.ts +116 -0
- package/src/astro/routes/api/admin/comments/[id].ts +64 -0
- package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
- package/src/astro/routes/api/admin/comments/counts.ts +30 -0
- package/src/astro/routes/api/admin/comments/index.ts +46 -0
- package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +91 -0
- package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
- package/src/astro/routes/api/admin/oauth-clients/[id].ts +110 -0
- package/src/astro/routes/api/admin/oauth-clients/index.ts +71 -0
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +39 -0
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +39 -0
- package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +48 -0
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +59 -0
- package/src/astro/routes/api/admin/plugins/index.ts +32 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +61 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +62 -0
- package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +61 -0
- package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
- package/src/astro/routes/api/admin/users/[id]/disable.ts +69 -0
- package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
- package/src/astro/routes/api/admin/users/[id]/index.ts +146 -0
- package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
- package/src/astro/routes/api/admin/users/index.ts +66 -0
- package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
- package/src/astro/routes/api/auth/invite/accept.ts +52 -0
- package/src/astro/routes/api/auth/invite/complete.ts +84 -0
- package/src/astro/routes/api/auth/invite/index.ts +99 -0
- package/src/astro/routes/api/auth/logout.ts +40 -0
- package/src/astro/routes/api/auth/magic-link/send.ts +89 -0
- package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
- package/src/astro/routes/api/auth/me.ts +60 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +219 -0
- package/src/astro/routes/api/auth/oauth/[provider].ts +119 -0
- package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
- package/src/astro/routes/api/auth/passkey/index.ts +54 -0
- package/src/astro/routes/api/auth/passkey/options.ts +82 -0
- package/src/astro/routes/api/auth/passkey/register/options.ts +86 -0
- package/src/astro/routes/api/auth/passkey/register/verify.ts +115 -0
- package/src/astro/routes/api/auth/passkey/verify.ts +66 -0
- package/src/astro/routes/api/auth/signup/complete.ts +85 -0
- package/src/astro/routes/api/auth/signup/request.ts +77 -0
- package/src/astro/routes/api/auth/signup/verify.ts +53 -0
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +312 -0
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +54 -0
- package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +61 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +33 -0
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +56 -0
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +54 -0
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +101 -0
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +140 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +30 -0
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +56 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +137 -0
- package/src/astro/routes/api/content/[collection]/index.ts +59 -0
- package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
- package/src/astro/routes/api/dashboard.ts +32 -0
- package/src/astro/routes/api/dev/emails.ts +36 -0
- package/src/astro/routes/api/import/probe.ts +47 -0
- package/src/astro/routes/api/import/wordpress/analyze.ts +510 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +283 -0
- package/src/astro/routes/api/import/wordpress/media.ts +338 -0
- package/src/astro/routes/api/import/wordpress/prepare.ts +181 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +393 -0
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
- package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +347 -0
- package/src/astro/routes/api/manifest.ts +62 -0
- package/src/astro/routes/api/mcp.ts +124 -0
- package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
- package/src/astro/routes/api/media/[id].ts +145 -0
- package/src/astro/routes/api/media/file/[key].ts +79 -0
- package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +86 -0
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
- package/src/astro/routes/api/media/providers/index.ts +30 -0
- package/src/astro/routes/api/media/upload-url.ts +137 -0
- package/src/astro/routes/api/media.ts +190 -0
- package/src/astro/routes/api/menus/[name]/items.ts +87 -0
- package/src/astro/routes/api/menus/[name]/reorder.ts +33 -0
- package/src/astro/routes/api/menus/[name].ts +65 -0
- package/src/astro/routes/api/menus/index.ts +47 -0
- package/src/astro/routes/api/oauth/authorize.ts +412 -0
- package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
- package/src/astro/routes/api/oauth/device/code.ts +51 -0
- package/src/astro/routes/api/oauth/device/token.ts +69 -0
- package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
- package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
- package/src/astro/routes/api/oauth/token.ts +184 -0
- package/src/astro/routes/api/openapi.json.ts +32 -0
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +92 -0
- package/src/astro/routes/api/redirects/404s/index.ts +72 -0
- package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
- package/src/astro/routes/api/redirects/[id].ts +84 -0
- package/src/astro/routes/api/redirects/index.ts +52 -0
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
- package/src/astro/routes/api/revisions/[revisionId]/restore.ts +58 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +76 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +52 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +32 -0
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +80 -0
- package/src/astro/routes/api/schema/collections/index.ts +47 -0
- package/src/astro/routes/api/schema/index.ts +109 -0
- package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
- package/src/astro/routes/api/schema/orphans/index.ts +26 -0
- package/src/astro/routes/api/search/enable.ts +64 -0
- package/src/astro/routes/api/search/index.ts +55 -0
- package/src/astro/routes/api/search/rebuild.ts +72 -0
- package/src/astro/routes/api/search/stats.ts +35 -0
- package/src/astro/routes/api/search/suggest.ts +53 -0
- package/src/astro/routes/api/sections/[slug].ts +84 -0
- package/src/astro/routes/api/sections/index.ts +52 -0
- package/src/astro/routes/api/settings/email.ts +150 -0
- package/src/astro/routes/api/settings.ts +67 -0
- package/src/astro/routes/api/setup/admin-verify.ts +100 -0
- package/src/astro/routes/api/setup/admin.ts +94 -0
- package/src/astro/routes/api/setup/dev-bypass.ts +199 -0
- package/src/astro/routes/api/setup/dev-reset.ts +40 -0
- package/src/astro/routes/api/setup/index.ts +126 -0
- package/src/astro/routes/api/setup/status.ts +122 -0
- package/src/astro/routes/api/snapshot.ts +75 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +95 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +69 -0
- package/src/astro/routes/api/taxonomies/index.ts +59 -0
- package/src/astro/routes/api/themes/preview.ts +77 -0
- package/src/astro/routes/api/typegen.ts +114 -0
- package/src/astro/routes/api/well-known/auth.ts +68 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +44 -0
- package/src/astro/routes/api/well-known/oauth-protected-resource.ts +37 -0
- package/src/astro/routes/api/widget-areas/[name]/reorder.ts +68 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +127 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +80 -0
- package/src/astro/routes/api/widget-areas/[name].ts +87 -0
- package/src/astro/routes/api/widget-areas/index.ts +99 -0
- package/src/astro/routes/api/widget-components.ts +22 -0
- package/src/astro/routes/robots.txt.ts +77 -0
- package/src/astro/routes/sitemap.xml.ts +97 -0
- package/src/astro/storage/adapters.ts +74 -0
- package/src/astro/storage/index.ts +19 -0
- package/src/astro/storage/types.ts +60 -0
- package/src/astro/types.ts +346 -0
- package/src/auth/api-tokens.ts +25 -0
- package/src/auth/challenge-store.ts +80 -0
- package/src/auth/mode.ts +96 -0
- package/src/auth/oauth-state-store.ts +96 -0
- package/src/auth/passkey-config.ts +27 -0
- package/src/auth/rate-limit.ts +158 -0
- package/src/auth/scopes.ts +33 -0
- package/src/auth/types.ts +104 -0
- package/src/aws-sdk.d.ts +100 -0
- package/src/bylines/index.ts +237 -0
- package/src/cleanup.ts +153 -0
- package/src/cli/client-factory.ts +100 -0
- package/src/cli/commands/auth.ts +46 -0
- package/src/cli/commands/bundle-utils.ts +247 -0
- package/src/cli/commands/bundle.ts +609 -0
- package/src/cli/commands/content.ts +442 -0
- package/src/cli/commands/dev.ts +191 -0
- package/src/cli/commands/doctor.ts +211 -0
- package/src/cli/commands/export-seed.ts +630 -0
- package/src/cli/commands/import/wordpress.ts +1056 -0
- package/src/cli/commands/init.ts +192 -0
- package/src/cli/commands/login.ts +547 -0
- package/src/cli/commands/media.ts +165 -0
- package/src/cli/commands/menu.ts +67 -0
- package/src/cli/commands/plugin-init.ts +291 -0
- package/src/cli/commands/plugin-validate.ts +31 -0
- package/src/cli/commands/plugin.ts +33 -0
- package/src/cli/commands/publish.ts +697 -0
- package/src/cli/commands/schema.ts +233 -0
- package/src/cli/commands/search-cmd.ts +54 -0
- package/src/cli/commands/seed.ts +286 -0
- package/src/cli/commands/taxonomy.ts +128 -0
- package/src/cli/commands/types.ts +68 -0
- package/src/cli/credentials.ts +236 -0
- package/src/cli/index.ts +70 -0
- package/src/cli/output.ts +75 -0
- package/src/cli/wxr/parser.ts +969 -0
- package/src/client/cf-access.ts +193 -0
- package/src/client/index.ts +854 -0
- package/src/client/portable-text.ts +413 -0
- package/src/client/transport.ts +200 -0
- package/src/comments/moderator.ts +46 -0
- package/src/comments/notifications.ts +144 -0
- package/src/comments/query.ts +105 -0
- package/src/comments/service.ts +213 -0
- package/src/components/Break.astro +45 -0
- package/src/components/Button.astro +71 -0
- package/src/components/Buttons.astro +49 -0
- package/src/components/Code.astro +59 -0
- package/src/components/Columns.astro +59 -0
- package/src/components/CommentForm.astro +315 -0
- package/src/components/Comments.astro +232 -0
- package/src/components/Cover.astro +128 -0
- package/src/components/EmDashBodyEnd.astro +32 -0
- package/src/components/EmDashBodyStart.astro +32 -0
- package/src/components/EmDashHead.astro +53 -0
- package/src/components/EmDashImage.astro +178 -0
- package/src/components/EmDashMedia.astro +167 -0
- package/src/components/Embed.astro +128 -0
- package/src/components/File.astro +122 -0
- package/src/components/Gallery.astro +93 -0
- package/src/components/HtmlBlock.astro +33 -0
- package/src/components/Image.astro +178 -0
- package/src/components/InlineEditor.astro +27 -0
- package/src/components/InlinePortableTextEditor.tsx +1905 -0
- package/src/components/LiveSearch.astro +614 -0
- package/src/components/PortableText.astro +51 -0
- package/src/components/Pullquote.astro +51 -0
- package/src/components/Table.astro +108 -0
- package/src/components/WidgetArea.astro +22 -0
- package/src/components/WidgetRenderer.astro +72 -0
- package/src/components/index.ts +116 -0
- package/src/components/marks/Link.astro +31 -0
- package/src/components/marks/StrikeThrough.astro +7 -0
- package/src/components/marks/Subscript.astro +7 -0
- package/src/components/marks/Superscript.astro +7 -0
- package/src/components/marks/Underline.astro +7 -0
- package/src/components/widgets/Archives.astro +65 -0
- package/src/components/widgets/Categories.astro +35 -0
- package/src/components/widgets/RecentPosts.astro +51 -0
- package/src/components/widgets/Search.astro +18 -0
- package/src/components/widgets/Tags.astro +38 -0
- package/src/content/converters/index.ts +9 -0
- package/src/content/converters/portable-text-to-prosemirror.ts +385 -0
- package/src/content/converters/prosemirror-to-portable-text.ts +413 -0
- package/src/content/converters/types.ts +120 -0
- package/src/content/index.ts +5 -0
- package/src/database/connection.ts +67 -0
- package/src/database/dialect-helpers.ts +138 -0
- package/src/database/index.ts +5 -0
- package/src/database/migrations/001_initial.ts +170 -0
- package/src/database/migrations/002_media_status.ts +26 -0
- package/src/database/migrations/003_schema_registry.ts +79 -0
- package/src/database/migrations/004_plugins.ts +62 -0
- package/src/database/migrations/005_menus.ts +67 -0
- package/src/database/migrations/006_taxonomy_defs.ts +51 -0
- package/src/database/migrations/007_widgets.ts +42 -0
- package/src/database/migrations/008_auth.ts +194 -0
- package/src/database/migrations/009_user_disabled.ts +27 -0
- package/src/database/migrations/011_sections.ts +65 -0
- package/src/database/migrations/012_search.ts +25 -0
- package/src/database/migrations/013_scheduled_publishing.ts +51 -0
- package/src/database/migrations/014_draft_revisions.ts +72 -0
- package/src/database/migrations/015_indexes.ts +82 -0
- package/src/database/migrations/016_api_tokens.ts +89 -0
- package/src/database/migrations/017_authorization_codes.ts +45 -0
- package/src/database/migrations/018_seo.ts +56 -0
- package/src/database/migrations/019_i18n.ts +618 -0
- package/src/database/migrations/020_collection_url_pattern.ts +23 -0
- package/src/database/migrations/021_remove_section_categories.ts +43 -0
- package/src/database/migrations/022_marketplace_plugin_state.ts +46 -0
- package/src/database/migrations/023_plugin_metadata.ts +33 -0
- package/src/database/migrations/024_media_placeholders.ts +32 -0
- package/src/database/migrations/025_oauth_clients.ts +28 -0
- package/src/database/migrations/026_cron_tasks.ts +49 -0
- package/src/database/migrations/027_comments.ts +87 -0
- package/src/database/migrations/028_drop_author_url.ts +9 -0
- package/src/database/migrations/029_redirects.ts +67 -0
- package/src/database/migrations/030_widen_scheduled_index.ts +48 -0
- package/src/database/migrations/031_bylines.ts +90 -0
- package/src/database/migrations/032_rate_limits.ts +39 -0
- package/src/database/migrations/runner.ts +170 -0
- package/src/database/repositories/audit.ts +294 -0
- package/src/database/repositories/byline.ts +387 -0
- package/src/database/repositories/comment.ts +458 -0
- package/src/database/repositories/content.ts +1144 -0
- package/src/database/repositories/index.ts +30 -0
- package/src/database/repositories/media.ts +347 -0
- package/src/database/repositories/options.ts +150 -0
- package/src/database/repositories/plugin-storage.ts +373 -0
- package/src/database/repositories/redirect.ts +480 -0
- package/src/database/repositories/revision.ts +200 -0
- package/src/database/repositories/seo.ts +176 -0
- package/src/database/repositories/taxonomy.ts +294 -0
- package/src/database/repositories/types.ts +132 -0
- package/src/database/repositories/user.ts +258 -0
- package/src/database/transaction.ts +54 -0
- package/src/database/types.ts +501 -0
- package/src/database/validate.ts +138 -0
- package/src/db/adapters.ts +125 -0
- package/src/db/index.ts +37 -0
- package/src/db/libsql.ts +23 -0
- package/src/db/postgres.ts +30 -0
- package/src/db/sqlite.ts +27 -0
- package/src/emdash-runtime.ts +2096 -0
- package/src/fields/boolean.ts +34 -0
- package/src/fields/datetime.ts +44 -0
- package/src/fields/file.ts +41 -0
- package/src/fields/image.ts +34 -0
- package/src/fields/index.ts +42 -0
- package/src/fields/integer.ts +50 -0
- package/src/fields/json.ts +37 -0
- package/src/fields/multiselect.ts +48 -0
- package/src/fields/number.ts +52 -0
- package/src/fields/portable-text.ts +33 -0
- package/src/fields/reference.ts +29 -0
- package/src/fields/richtext.ts +31 -0
- package/src/fields/select.ts +46 -0
- package/src/fields/slug.ts +38 -0
- package/src/fields/text.ts +55 -0
- package/src/fields/textarea.ts +52 -0
- package/src/fields/types.ts +64 -0
- package/src/i18n/config.ts +68 -0
- package/src/import/index.ts +90 -0
- package/src/import/menus.ts +436 -0
- package/src/import/registry.ts +111 -0
- package/src/import/sections.ts +103 -0
- package/src/import/settings.ts +281 -0
- package/src/import/sources/wordpress-plugin.ts +641 -0
- package/src/import/sources/wordpress-rest.ts +191 -0
- package/src/import/sources/wxr.ts +330 -0
- package/src/import/ssrf.ts +260 -0
- package/src/import/types.ts +418 -0
- package/src/import/utils.ts +412 -0
- package/src/index.ts +481 -0
- package/src/loader.ts +770 -0
- package/src/mcp/server.ts +1463 -0
- package/src/media/index.ts +32 -0
- package/src/media/local-runtime.ts +213 -0
- package/src/media/local.ts +46 -0
- package/src/media/normalize.ts +190 -0
- package/src/media/placeholder.ts +150 -0
- package/src/media/provider-loader.ts +78 -0
- package/src/media/types.ts +279 -0
- package/src/menus/index.ts +324 -0
- package/src/menus/types.ts +112 -0
- package/src/page/context.ts +93 -0
- package/src/page/fragments.ts +89 -0
- package/src/page/index.ts +58 -0
- package/src/page/jsonld.ts +94 -0
- package/src/page/metadata.ts +185 -0
- package/src/page/seo-contributions.ts +136 -0
- package/src/plugin-utils.ts +80 -0
- package/src/plugins/adapt-sandbox-entry.ts +207 -0
- package/src/plugins/context.ts +833 -0
- package/src/plugins/cron.ts +361 -0
- package/src/plugins/define-plugin.ts +259 -0
- package/src/plugins/email-console.ts +73 -0
- package/src/plugins/email.ts +209 -0
- package/src/plugins/hooks.ts +1273 -0
- package/src/plugins/index.ts +193 -0
- package/src/plugins/manager.ts +595 -0
- package/src/plugins/manifest-schema.ts +230 -0
- package/src/plugins/marketplace.ts +460 -0
- package/src/plugins/request-meta.ts +139 -0
- package/src/plugins/routes.ts +302 -0
- package/src/plugins/sandbox/index.ts +18 -0
- package/src/plugins/sandbox/noop.ts +76 -0
- package/src/plugins/sandbox/types.ts +173 -0
- package/src/plugins/scheduler/node.ts +122 -0
- package/src/plugins/scheduler/piggyback.ts +71 -0
- package/src/plugins/scheduler/types.ts +27 -0
- package/src/plugins/state.ts +208 -0
- package/src/plugins/storage-indexes.ts +326 -0
- package/src/plugins/storage-query.ts +240 -0
- package/src/plugins/types.ts +1284 -0
- package/src/preview/helpers.ts +27 -0
- package/src/preview/index.ts +40 -0
- package/src/preview/tokens.ts +279 -0
- package/src/preview/urls.ts +118 -0
- package/src/query.ts +674 -0
- package/src/redirects/patterns.ts +224 -0
- package/src/request-context.ts +67 -0
- package/src/runtime.ts +21 -0
- package/src/schema/index.ts +29 -0
- package/src/schema/query.ts +44 -0
- package/src/schema/registry.ts +965 -0
- package/src/schema/types.ts +276 -0
- package/src/schema/zod-generator.ts +413 -0
- package/src/search/fts-manager.ts +452 -0
- package/src/search/index.ts +26 -0
- package/src/search/query.ts +396 -0
- package/src/search/text-extraction.ts +162 -0
- package/src/search/types.ts +114 -0
- package/src/sections/index.ts +226 -0
- package/src/sections/types.ts +86 -0
- package/src/seed/apply.ts +1141 -0
- package/src/seed/default.ts +86 -0
- package/src/seed/index.ts +28 -0
- package/src/seed/load.ts +35 -0
- package/src/seed/types.ts +341 -0
- package/src/seed/validate.ts +642 -0
- package/src/seo/index.ts +179 -0
- package/src/settings/index.ts +203 -0
- package/src/settings/types.ts +58 -0
- package/src/storage/index.ts +28 -0
- package/src/storage/local.ts +249 -0
- package/src/storage/s3.ts +263 -0
- package/src/storage/types.ts +204 -0
- package/src/taxonomies/index.ts +309 -0
- package/src/taxonomies/types.ts +61 -0
- package/src/ui.ts +75 -0
- package/src/utils/base64.ts +73 -0
- package/src/utils/hash.ts +36 -0
- package/src/utils/sanitize.ts +20 -0
- package/src/utils/slugify.ts +29 -0
- package/src/utils/url.ts +48 -0
- package/src/virtual-modules.d.ts +111 -0
- package/src/visual-editing/editable.ts +108 -0
- package/src/visual-editing/toolbar.ts +1229 -0
- package/src/widgets/components.ts +105 -0
- package/src/widgets/index.ts +131 -0
- package/src/widgets/types.ts +81 -0
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
|
+
import type { Selectable } from "kysely";
|
|
3
|
+
import { sql } from "kysely";
|
|
4
|
+
import { ulid } from "ulidx";
|
|
5
|
+
|
|
6
|
+
import { currentTimestamp, listTablesLike, tableExists } from "../database/dialect-helpers.js";
|
|
7
|
+
import { withTransaction } from "../database/transaction.js";
|
|
8
|
+
import type { CollectionTable, Database, FieldTable } from "../database/types.js";
|
|
9
|
+
import { FTSManager } from "../search/fts-manager.js";
|
|
10
|
+
import {
|
|
11
|
+
type Collection,
|
|
12
|
+
type CollectionSource,
|
|
13
|
+
type ColumnType,
|
|
14
|
+
type Field,
|
|
15
|
+
type CreateCollectionInput,
|
|
16
|
+
type UpdateCollectionInput,
|
|
17
|
+
type CreateFieldInput,
|
|
18
|
+
type UpdateFieldInput,
|
|
19
|
+
type CollectionWithFields,
|
|
20
|
+
type FieldType,
|
|
21
|
+
FIELD_TYPE_TO_COLUMN,
|
|
22
|
+
RESERVED_FIELD_SLUGS,
|
|
23
|
+
RESERVED_COLLECTION_SLUGS,
|
|
24
|
+
} from "./types.js";
|
|
25
|
+
|
|
26
|
+
// Regex patterns for schema registry
|
|
27
|
+
const SLUG_VALIDATION_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
28
|
+
const EC_PREFIX_PATTERN = /^ec_/;
|
|
29
|
+
const SINGLE_QUOTE_PATTERN = /'/g;
|
|
30
|
+
const UNDERSCORE_PATTERN = /_/g;
|
|
31
|
+
const WORD_BOUNDARY_PATTERN = /\b\w/g;
|
|
32
|
+
|
|
33
|
+
/** Valid column types for runtime validation */
|
|
34
|
+
const COLUMN_TYPES: ReadonlySet<string> = new Set(["TEXT", "REAL", "INTEGER", "JSON"]);
|
|
35
|
+
|
|
36
|
+
/** Valid collection source prefixes/values */
|
|
37
|
+
const VALID_SOURCES: ReadonlySet<string> = new Set(["manual", "discovered", "seed"]);
|
|
38
|
+
|
|
39
|
+
function isCollectionSource(value: string): value is CollectionSource {
|
|
40
|
+
return VALID_SOURCES.has(value) || value.startsWith("template:") || value.startsWith("import:");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isFieldType(value: string): value is FieldType {
|
|
44
|
+
return value in FIELD_TYPE_TO_COLUMN;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isColumnType(value: string): value is ColumnType {
|
|
48
|
+
return COLUMN_TYPES.has(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Error thrown when a schema operation fails
|
|
53
|
+
*/
|
|
54
|
+
export class SchemaError extends Error {
|
|
55
|
+
constructor(
|
|
56
|
+
message: string,
|
|
57
|
+
public code: string,
|
|
58
|
+
public details?: Record<string, unknown>,
|
|
59
|
+
) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.name = "SchemaError";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Schema Registry
|
|
67
|
+
*
|
|
68
|
+
* Manages collection and field definitions stored in D1.
|
|
69
|
+
* Handles runtime DDL operations (CREATE TABLE, ALTER TABLE).
|
|
70
|
+
*/
|
|
71
|
+
export class SchemaRegistry {
|
|
72
|
+
constructor(private db: Kysely<Database>) {}
|
|
73
|
+
|
|
74
|
+
// ============================================
|
|
75
|
+
// Collection Operations
|
|
76
|
+
// ============================================
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* List all collections
|
|
80
|
+
*/
|
|
81
|
+
async listCollections(): Promise<Collection[]> {
|
|
82
|
+
const rows = await this.db
|
|
83
|
+
.selectFrom("_emdash_collections")
|
|
84
|
+
.selectAll()
|
|
85
|
+
.orderBy("slug", "asc")
|
|
86
|
+
.execute();
|
|
87
|
+
|
|
88
|
+
return rows.map(this.mapCollectionRow);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get a collection by slug
|
|
93
|
+
*/
|
|
94
|
+
async getCollection(slug: string): Promise<Collection | null> {
|
|
95
|
+
const row = await this.db
|
|
96
|
+
.selectFrom("_emdash_collections")
|
|
97
|
+
.where("slug", "=", slug)
|
|
98
|
+
.selectAll()
|
|
99
|
+
.executeTakeFirst();
|
|
100
|
+
|
|
101
|
+
return row ? this.mapCollectionRow(row) : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get a collection with all its fields
|
|
106
|
+
*/
|
|
107
|
+
async getCollectionWithFields(slug: string): Promise<CollectionWithFields | null> {
|
|
108
|
+
const collection = await this.getCollection(slug);
|
|
109
|
+
if (!collection) return null;
|
|
110
|
+
|
|
111
|
+
const fields = await this.listFields(collection.id);
|
|
112
|
+
|
|
113
|
+
return { ...collection, fields };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create a new collection
|
|
118
|
+
*/
|
|
119
|
+
async createCollection(input: CreateCollectionInput): Promise<Collection> {
|
|
120
|
+
// Validate slug
|
|
121
|
+
this.validateSlug(input.slug, "collection");
|
|
122
|
+
if (RESERVED_COLLECTION_SLUGS.includes(input.slug)) {
|
|
123
|
+
throw new SchemaError(`Collection slug "${input.slug}" is reserved`, "RESERVED_SLUG");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check if collection already exists
|
|
127
|
+
const existing = await this.getCollection(input.slug);
|
|
128
|
+
if (existing) {
|
|
129
|
+
throw new SchemaError(`Collection "${input.slug}" already exists`, "COLLECTION_EXISTS");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const id = ulid();
|
|
133
|
+
|
|
134
|
+
// Insert collection record and create content table in a transaction
|
|
135
|
+
// so a failure in table creation doesn't leave an orphaned row.
|
|
136
|
+
// Uses withTransaction for D1 compatibility (no transaction support).
|
|
137
|
+
// Derive hasSeo from supports array if not explicitly set
|
|
138
|
+
const hasSeo = input.hasSeo ?? input.supports?.includes("seo") ?? false;
|
|
139
|
+
|
|
140
|
+
await withTransaction(this.db, async (trx) => {
|
|
141
|
+
await trx
|
|
142
|
+
.insertInto("_emdash_collections")
|
|
143
|
+
.values({
|
|
144
|
+
id,
|
|
145
|
+
slug: input.slug,
|
|
146
|
+
label: input.label,
|
|
147
|
+
label_singular: input.labelSingular ?? null,
|
|
148
|
+
description: input.description ?? null,
|
|
149
|
+
icon: input.icon ?? null,
|
|
150
|
+
supports: input.supports ? JSON.stringify(input.supports) : null,
|
|
151
|
+
source: input.source ?? "manual",
|
|
152
|
+
has_seo: hasSeo ? 1 : 0,
|
|
153
|
+
comments_enabled: input.commentsEnabled ? 1 : 0,
|
|
154
|
+
url_pattern: input.urlPattern ?? null,
|
|
155
|
+
})
|
|
156
|
+
.execute();
|
|
157
|
+
|
|
158
|
+
// Create the content table for this collection
|
|
159
|
+
await this.createContentTable(input.slug, trx);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const collection = await this.getCollection(input.slug);
|
|
163
|
+
if (!collection) {
|
|
164
|
+
throw new SchemaError("Failed to create collection", "CREATE_FAILED");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return collection;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Update a collection
|
|
172
|
+
*/
|
|
173
|
+
async updateCollection(slug: string, input: UpdateCollectionInput): Promise<Collection> {
|
|
174
|
+
const existing = await this.getCollection(slug);
|
|
175
|
+
if (!existing) {
|
|
176
|
+
throw new SchemaError(`Collection "${slug}" not found`, "COLLECTION_NOT_FOUND");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const now = new Date().toISOString();
|
|
180
|
+
|
|
181
|
+
// Derive hasSeo from supports array if supports is being updated and hasSeo not explicitly set
|
|
182
|
+
const supportsArray = input.supports ?? existing.supports;
|
|
183
|
+
const hasSeo =
|
|
184
|
+
input.hasSeo !== undefined
|
|
185
|
+
? input.hasSeo
|
|
186
|
+
: input.supports !== undefined
|
|
187
|
+
? supportsArray.includes("seo")
|
|
188
|
+
: existing.hasSeo;
|
|
189
|
+
|
|
190
|
+
await this.db
|
|
191
|
+
.updateTable("_emdash_collections")
|
|
192
|
+
.set({
|
|
193
|
+
label: input.label ?? existing.label,
|
|
194
|
+
label_singular: input.labelSingular ?? existing.labelSingular ?? null,
|
|
195
|
+
description: input.description ?? existing.description ?? null,
|
|
196
|
+
icon: input.icon ?? existing.icon ?? null,
|
|
197
|
+
supports: input.supports
|
|
198
|
+
? JSON.stringify(input.supports)
|
|
199
|
+
: JSON.stringify(existing.supports),
|
|
200
|
+
url_pattern:
|
|
201
|
+
input.urlPattern !== undefined
|
|
202
|
+
? (input.urlPattern ?? null)
|
|
203
|
+
: (existing.urlPattern ?? null),
|
|
204
|
+
has_seo: hasSeo ? 1 : 0,
|
|
205
|
+
comments_enabled:
|
|
206
|
+
input.commentsEnabled !== undefined
|
|
207
|
+
? input.commentsEnabled
|
|
208
|
+
? 1
|
|
209
|
+
: 0
|
|
210
|
+
: existing.commentsEnabled
|
|
211
|
+
? 1
|
|
212
|
+
: 0,
|
|
213
|
+
comments_moderation: input.commentsModeration ?? existing.commentsModeration,
|
|
214
|
+
comments_closed_after_days:
|
|
215
|
+
input.commentsClosedAfterDays !== undefined
|
|
216
|
+
? input.commentsClosedAfterDays
|
|
217
|
+
: existing.commentsClosedAfterDays,
|
|
218
|
+
comments_auto_approve_users:
|
|
219
|
+
input.commentsAutoApproveUsers !== undefined
|
|
220
|
+
? input.commentsAutoApproveUsers
|
|
221
|
+
? 1
|
|
222
|
+
: 0
|
|
223
|
+
: existing.commentsAutoApproveUsers
|
|
224
|
+
? 1
|
|
225
|
+
: 0,
|
|
226
|
+
updated_at: now,
|
|
227
|
+
})
|
|
228
|
+
.where("slug", "=", slug)
|
|
229
|
+
.execute();
|
|
230
|
+
|
|
231
|
+
const updated = await this.getCollection(slug);
|
|
232
|
+
if (!updated) {
|
|
233
|
+
throw new SchemaError("Failed to update collection", "UPDATE_FAILED");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return updated;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Delete a collection
|
|
241
|
+
*/
|
|
242
|
+
async deleteCollection(slug: string, options?: { force?: boolean }): Promise<void> {
|
|
243
|
+
const existing = await this.getCollection(slug);
|
|
244
|
+
if (!existing) {
|
|
245
|
+
throw new SchemaError(`Collection "${slug}" not found`, "COLLECTION_NOT_FOUND");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Check if collection has content
|
|
249
|
+
if (!options?.force) {
|
|
250
|
+
const hasContent = await this.collectionHasContent(slug);
|
|
251
|
+
if (hasContent) {
|
|
252
|
+
throw new SchemaError(
|
|
253
|
+
`Collection "${slug}" has content. Use force: true to delete.`,
|
|
254
|
+
"COLLECTION_HAS_CONTENT",
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Drop the content table
|
|
260
|
+
await this.dropContentTable(slug);
|
|
261
|
+
|
|
262
|
+
// Delete the collection record (fields will cascade)
|
|
263
|
+
await this.db.deleteFrom("_emdash_collections").where("id", "=", existing.id).execute();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ============================================
|
|
267
|
+
// Field Operations
|
|
268
|
+
// ============================================
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* List fields for a collection
|
|
272
|
+
*/
|
|
273
|
+
async listFields(collectionId: string): Promise<Field[]> {
|
|
274
|
+
const rows = await this.db
|
|
275
|
+
.selectFrom("_emdash_fields")
|
|
276
|
+
.where("collection_id", "=", collectionId)
|
|
277
|
+
.selectAll()
|
|
278
|
+
.orderBy("sort_order", "asc")
|
|
279
|
+
.orderBy("created_at", "asc")
|
|
280
|
+
.execute();
|
|
281
|
+
|
|
282
|
+
return rows.map(this.mapFieldRow);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get a field by slug within a collection
|
|
287
|
+
*/
|
|
288
|
+
async getField(collectionSlug: string, fieldSlug: string): Promise<Field | null> {
|
|
289
|
+
const collection = await this.getCollection(collectionSlug);
|
|
290
|
+
if (!collection) return null;
|
|
291
|
+
|
|
292
|
+
const row = await this.db
|
|
293
|
+
.selectFrom("_emdash_fields")
|
|
294
|
+
.where("collection_id", "=", collection.id)
|
|
295
|
+
.where("slug", "=", fieldSlug)
|
|
296
|
+
.selectAll()
|
|
297
|
+
.executeTakeFirst();
|
|
298
|
+
|
|
299
|
+
return row ? this.mapFieldRow(row) : null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Create a new field
|
|
304
|
+
*/
|
|
305
|
+
async createField(collectionSlug: string, input: CreateFieldInput): Promise<Field> {
|
|
306
|
+
const collection = await this.getCollection(collectionSlug);
|
|
307
|
+
if (!collection) {
|
|
308
|
+
throw new SchemaError(`Collection "${collectionSlug}" not found`, "COLLECTION_NOT_FOUND");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Validate slug
|
|
312
|
+
this.validateSlug(input.slug, "field");
|
|
313
|
+
if (RESERVED_FIELD_SLUGS.includes(input.slug)) {
|
|
314
|
+
throw new SchemaError(`Field slug "${input.slug}" is reserved`, "RESERVED_SLUG");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Check if field already exists
|
|
318
|
+
const existing = await this.getField(collectionSlug, input.slug);
|
|
319
|
+
if (existing) {
|
|
320
|
+
throw new SchemaError(
|
|
321
|
+
`Field "${input.slug}" already exists in collection "${collectionSlug}"`,
|
|
322
|
+
"FIELD_EXISTS",
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const id = ulid();
|
|
327
|
+
const columnType = FIELD_TYPE_TO_COLUMN[input.type];
|
|
328
|
+
|
|
329
|
+
// Get max sort order
|
|
330
|
+
const maxSort = await this.db
|
|
331
|
+
.selectFrom("_emdash_fields")
|
|
332
|
+
.where("collection_id", "=", collection.id)
|
|
333
|
+
.select((eb) => eb.fn.max<number>("sort_order").as("max"))
|
|
334
|
+
.executeTakeFirst();
|
|
335
|
+
|
|
336
|
+
const sortOrder = input.sortOrder ?? (maxSort?.max ?? -1) + 1;
|
|
337
|
+
|
|
338
|
+
// Insert field record
|
|
339
|
+
await this.db
|
|
340
|
+
.insertInto("_emdash_fields")
|
|
341
|
+
.values({
|
|
342
|
+
id,
|
|
343
|
+
collection_id: collection.id,
|
|
344
|
+
slug: input.slug,
|
|
345
|
+
label: input.label,
|
|
346
|
+
type: input.type,
|
|
347
|
+
column_type: columnType,
|
|
348
|
+
required: input.required ? 1 : 0,
|
|
349
|
+
unique: input.unique ? 1 : 0,
|
|
350
|
+
default_value: input.defaultValue !== undefined ? JSON.stringify(input.defaultValue) : null,
|
|
351
|
+
validation: input.validation ? JSON.stringify(input.validation) : null,
|
|
352
|
+
widget: input.widget ?? null,
|
|
353
|
+
options: input.options ? JSON.stringify(input.options) : null,
|
|
354
|
+
sort_order: sortOrder,
|
|
355
|
+
searchable: input.searchable ? 1 : 0,
|
|
356
|
+
translatable: input.translatable === false ? 0 : 1,
|
|
357
|
+
})
|
|
358
|
+
.execute();
|
|
359
|
+
|
|
360
|
+
// Add column to content table
|
|
361
|
+
await this.addColumn(collectionSlug, input.slug, input.type, {
|
|
362
|
+
required: input.required,
|
|
363
|
+
defaultValue: input.defaultValue,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const field = await this.getField(collectionSlug, input.slug);
|
|
367
|
+
if (!field) {
|
|
368
|
+
throw new SchemaError("Failed to create field", "CREATE_FAILED");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return field;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Update a field
|
|
376
|
+
*/
|
|
377
|
+
async updateField(
|
|
378
|
+
collectionSlug: string,
|
|
379
|
+
fieldSlug: string,
|
|
380
|
+
input: UpdateFieldInput,
|
|
381
|
+
): Promise<Field> {
|
|
382
|
+
const field = await this.getField(collectionSlug, fieldSlug);
|
|
383
|
+
if (!field) {
|
|
384
|
+
throw new SchemaError(
|
|
385
|
+
`Field "${fieldSlug}" not found in collection "${collectionSlug}"`,
|
|
386
|
+
"FIELD_NOT_FOUND",
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
await this.db
|
|
391
|
+
.updateTable("_emdash_fields")
|
|
392
|
+
.set({
|
|
393
|
+
label: input.label ?? field.label,
|
|
394
|
+
required: input.required !== undefined ? (input.required ? 1 : 0) : field.required ? 1 : 0,
|
|
395
|
+
unique: input.unique !== undefined ? (input.unique ? 1 : 0) : field.unique ? 1 : 0,
|
|
396
|
+
searchable:
|
|
397
|
+
input.searchable !== undefined ? (input.searchable ? 1 : 0) : field.searchable ? 1 : 0,
|
|
398
|
+
translatable:
|
|
399
|
+
input.translatable !== undefined
|
|
400
|
+
? input.translatable
|
|
401
|
+
? 1
|
|
402
|
+
: 0
|
|
403
|
+
: field.translatable
|
|
404
|
+
? 1
|
|
405
|
+
: 0,
|
|
406
|
+
default_value:
|
|
407
|
+
input.defaultValue !== undefined
|
|
408
|
+
? JSON.stringify(input.defaultValue)
|
|
409
|
+
: field.defaultValue !== undefined
|
|
410
|
+
? JSON.stringify(field.defaultValue)
|
|
411
|
+
: null,
|
|
412
|
+
validation: input.validation
|
|
413
|
+
? JSON.stringify(input.validation)
|
|
414
|
+
: field.validation
|
|
415
|
+
? JSON.stringify(field.validation)
|
|
416
|
+
: null,
|
|
417
|
+
widget: input.widget ?? field.widget ?? null,
|
|
418
|
+
options: input.options
|
|
419
|
+
? JSON.stringify(input.options)
|
|
420
|
+
: field.options
|
|
421
|
+
? JSON.stringify(field.options)
|
|
422
|
+
: null,
|
|
423
|
+
sort_order: input.sortOrder ?? field.sortOrder,
|
|
424
|
+
})
|
|
425
|
+
.where("id", "=", field.id)
|
|
426
|
+
.execute();
|
|
427
|
+
|
|
428
|
+
const updated = await this.getField(collectionSlug, fieldSlug);
|
|
429
|
+
if (!updated) {
|
|
430
|
+
throw new SchemaError("Failed to update field", "UPDATE_FAILED");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// If searchable changed, rebuild the FTS index for this collection
|
|
434
|
+
const searchableChanged =
|
|
435
|
+
input.searchable !== undefined && input.searchable !== field.searchable;
|
|
436
|
+
if (searchableChanged) {
|
|
437
|
+
await this.rebuildSearchIndex(collectionSlug);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return updated;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Rebuild the search index for a collection
|
|
445
|
+
*
|
|
446
|
+
* Called when searchable fields change. If search is enabled for the collection,
|
|
447
|
+
* this will rebuild the FTS table with the updated field list.
|
|
448
|
+
*/
|
|
449
|
+
private async rebuildSearchIndex(collectionSlug: string): Promise<void> {
|
|
450
|
+
const ftsManager = new FTSManager(this.db);
|
|
451
|
+
|
|
452
|
+
// Check if search is enabled for this collection
|
|
453
|
+
const config = await ftsManager.getSearchConfig(collectionSlug);
|
|
454
|
+
if (!config?.enabled) {
|
|
455
|
+
// Search not enabled, nothing to do
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Get current searchable fields
|
|
460
|
+
const searchableFields = await ftsManager.getSearchableFields(collectionSlug);
|
|
461
|
+
|
|
462
|
+
if (searchableFields.length === 0) {
|
|
463
|
+
// No searchable fields left, disable search
|
|
464
|
+
await ftsManager.disableSearch(collectionSlug);
|
|
465
|
+
} else {
|
|
466
|
+
// Rebuild the index with updated fields
|
|
467
|
+
await ftsManager.rebuildIndex(collectionSlug, searchableFields, config.weights);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Delete a field
|
|
473
|
+
*/
|
|
474
|
+
async deleteField(collectionSlug: string, fieldSlug: string): Promise<void> {
|
|
475
|
+
const field = await this.getField(collectionSlug, fieldSlug);
|
|
476
|
+
if (!field) {
|
|
477
|
+
throw new SchemaError(
|
|
478
|
+
`Field "${fieldSlug}" not found in collection "${collectionSlug}"`,
|
|
479
|
+
"FIELD_NOT_FOUND",
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Drop column from content table
|
|
484
|
+
await this.dropColumn(collectionSlug, fieldSlug);
|
|
485
|
+
|
|
486
|
+
// Delete field record
|
|
487
|
+
await this.db.deleteFrom("_emdash_fields").where("id", "=", field.id).execute();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Reorder fields
|
|
492
|
+
*/
|
|
493
|
+
async reorderFields(collectionSlug: string, fieldSlugs: string[]): Promise<void> {
|
|
494
|
+
const collection = await this.getCollection(collectionSlug);
|
|
495
|
+
if (!collection) {
|
|
496
|
+
throw new SchemaError(`Collection "${collectionSlug}" not found`, "COLLECTION_NOT_FOUND");
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Update sort_order for each field
|
|
500
|
+
for (let i = 0; i < fieldSlugs.length; i++) {
|
|
501
|
+
await this.db
|
|
502
|
+
.updateTable("_emdash_fields")
|
|
503
|
+
.set({ sort_order: i })
|
|
504
|
+
.where("collection_id", "=", collection.id)
|
|
505
|
+
.where("slug", "=", fieldSlugs[i])
|
|
506
|
+
.execute();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ============================================
|
|
511
|
+
// DDL Operations
|
|
512
|
+
// ============================================
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Create a content table for a collection
|
|
516
|
+
*/
|
|
517
|
+
private async createContentTable(slug: string, db?: Kysely<Database>): Promise<void> {
|
|
518
|
+
const conn = db ?? this.db;
|
|
519
|
+
const tableName = this.getTableName(slug);
|
|
520
|
+
|
|
521
|
+
await conn.schema
|
|
522
|
+
.createTable(tableName)
|
|
523
|
+
.addColumn("id", "text", (col) => col.primaryKey())
|
|
524
|
+
.addColumn("slug", "text")
|
|
525
|
+
.addColumn("status", "text", (col) => col.defaultTo("draft"))
|
|
526
|
+
.addColumn("author_id", "text")
|
|
527
|
+
.addColumn("primary_byline_id", "text")
|
|
528
|
+
.addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(conn)))
|
|
529
|
+
.addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(conn)))
|
|
530
|
+
.addColumn("published_at", "text")
|
|
531
|
+
.addColumn("scheduled_at", "text")
|
|
532
|
+
.addColumn("deleted_at", "text")
|
|
533
|
+
.addColumn("version", "integer", (col) => col.defaultTo(1))
|
|
534
|
+
.addColumn("live_revision_id", "text", (col) => col.references("revisions.id"))
|
|
535
|
+
.addColumn("draft_revision_id", "text", (col) => col.references("revisions.id"))
|
|
536
|
+
.addColumn("locale", "text", (col) => col.notNull().defaultTo("en"))
|
|
537
|
+
.addColumn("translation_group", "text")
|
|
538
|
+
.addUniqueConstraint(`${tableName}_slug_locale_unique`, ["slug", "locale"])
|
|
539
|
+
.execute();
|
|
540
|
+
|
|
541
|
+
// Create standard indexes
|
|
542
|
+
await sql`
|
|
543
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_status`)}
|
|
544
|
+
ON ${sql.ref(tableName)} (status)
|
|
545
|
+
`.execute(conn);
|
|
546
|
+
|
|
547
|
+
await sql`
|
|
548
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_slug`)}
|
|
549
|
+
ON ${sql.ref(tableName)} (slug)
|
|
550
|
+
`.execute(conn);
|
|
551
|
+
|
|
552
|
+
await sql`
|
|
553
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_created`)}
|
|
554
|
+
ON ${sql.ref(tableName)} (created_at)
|
|
555
|
+
`.execute(conn);
|
|
556
|
+
|
|
557
|
+
await sql`
|
|
558
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_deleted`)}
|
|
559
|
+
ON ${sql.ref(tableName)} (deleted_at)
|
|
560
|
+
`.execute(conn);
|
|
561
|
+
|
|
562
|
+
await sql`
|
|
563
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_scheduled`)}
|
|
564
|
+
ON ${sql.ref(tableName)} (scheduled_at)
|
|
565
|
+
WHERE scheduled_at IS NOT NULL
|
|
566
|
+
`.execute(conn);
|
|
567
|
+
|
|
568
|
+
await sql`
|
|
569
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_live_revision`)}
|
|
570
|
+
ON ${sql.ref(tableName)} (live_revision_id)
|
|
571
|
+
`.execute(conn);
|
|
572
|
+
|
|
573
|
+
await sql`
|
|
574
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_draft_revision`)}
|
|
575
|
+
ON ${sql.ref(tableName)} (draft_revision_id)
|
|
576
|
+
`.execute(conn);
|
|
577
|
+
|
|
578
|
+
await sql`
|
|
579
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_author`)}
|
|
580
|
+
ON ${sql.ref(tableName)} (author_id)
|
|
581
|
+
`.execute(conn);
|
|
582
|
+
|
|
583
|
+
await sql`
|
|
584
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_primary_byline`)}
|
|
585
|
+
ON ${sql.ref(tableName)} (primary_byline_id)
|
|
586
|
+
`.execute(conn);
|
|
587
|
+
|
|
588
|
+
await sql`
|
|
589
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_updated`)}
|
|
590
|
+
ON ${sql.ref(tableName)} (updated_at)
|
|
591
|
+
`.execute(conn);
|
|
592
|
+
|
|
593
|
+
await sql`
|
|
594
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_locale`)}
|
|
595
|
+
ON ${sql.ref(tableName)} (locale)
|
|
596
|
+
`.execute(conn);
|
|
597
|
+
|
|
598
|
+
await sql`
|
|
599
|
+
CREATE INDEX ${sql.ref(`idx_${tableName}_translation_group`)}
|
|
600
|
+
ON ${sql.ref(tableName)} (translation_group)
|
|
601
|
+
`.execute(conn);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Drop a content table
|
|
606
|
+
*/
|
|
607
|
+
private async dropContentTable(slug: string): Promise<void> {
|
|
608
|
+
const tableName = this.getTableName(slug);
|
|
609
|
+
await sql`DROP TABLE IF EXISTS ${sql.ref(tableName)}`.execute(this.db);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Add a column to a content table
|
|
614
|
+
*/
|
|
615
|
+
private async addColumn(
|
|
616
|
+
collectionSlug: string,
|
|
617
|
+
fieldSlug: string,
|
|
618
|
+
fieldType: FieldType,
|
|
619
|
+
options?: { required?: boolean; defaultValue?: unknown },
|
|
620
|
+
): Promise<void> {
|
|
621
|
+
const tableName = this.getTableName(collectionSlug);
|
|
622
|
+
const columnType = FIELD_TYPE_TO_COLUMN[fieldType];
|
|
623
|
+
const columnName = this.getColumnName(fieldSlug);
|
|
624
|
+
|
|
625
|
+
// Build ALTER TABLE statement
|
|
626
|
+
// Note: SQLite requires DEFAULT for NOT NULL columns in ALTER TABLE
|
|
627
|
+
if (options?.required && options?.defaultValue !== undefined) {
|
|
628
|
+
const defaultVal = this.formatDefaultValue(options.defaultValue, fieldType);
|
|
629
|
+
await sql`
|
|
630
|
+
ALTER TABLE ${sql.ref(tableName)}
|
|
631
|
+
ADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)} NOT NULL DEFAULT ${sql.raw(defaultVal)}
|
|
632
|
+
`.execute(this.db);
|
|
633
|
+
} else if (options?.required) {
|
|
634
|
+
// For required fields without default, use empty string/0 as default
|
|
635
|
+
const defaultVal = this.getEmptyDefault(fieldType);
|
|
636
|
+
await sql`
|
|
637
|
+
ALTER TABLE ${sql.ref(tableName)}
|
|
638
|
+
ADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)} NOT NULL DEFAULT ${sql.raw(defaultVal)}
|
|
639
|
+
`.execute(this.db);
|
|
640
|
+
} else {
|
|
641
|
+
await sql`
|
|
642
|
+
ALTER TABLE ${sql.ref(tableName)}
|
|
643
|
+
ADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)}
|
|
644
|
+
`.execute(this.db);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Drop a column from a content table
|
|
650
|
+
*/
|
|
651
|
+
private async dropColumn(collectionSlug: string, fieldSlug: string): Promise<void> {
|
|
652
|
+
const tableName = this.getTableName(collectionSlug);
|
|
653
|
+
const columnName = this.getColumnName(fieldSlug);
|
|
654
|
+
|
|
655
|
+
await sql`
|
|
656
|
+
ALTER TABLE ${sql.ref(tableName)}
|
|
657
|
+
DROP COLUMN ${sql.ref(columnName)}
|
|
658
|
+
`.execute(this.db);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ============================================
|
|
662
|
+
// Helpers
|
|
663
|
+
// ============================================
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Check if a collection has any content
|
|
667
|
+
*/
|
|
668
|
+
private async collectionHasContent(slug: string): Promise<boolean> {
|
|
669
|
+
const tableName = this.getTableName(slug);
|
|
670
|
+
try {
|
|
671
|
+
const result = await sql<{ count: number }>`
|
|
672
|
+
SELECT COUNT(*) as count FROM ${sql.ref(tableName)}
|
|
673
|
+
WHERE deleted_at IS NULL
|
|
674
|
+
`.execute(this.db);
|
|
675
|
+
return (result.rows[0]?.count ?? 0) > 0;
|
|
676
|
+
} catch {
|
|
677
|
+
// Table might not exist
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Get table name for a collection
|
|
684
|
+
*/
|
|
685
|
+
private getTableName(slug: string): string {
|
|
686
|
+
return `ec_${slug}`;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Get column name for a field
|
|
691
|
+
*/
|
|
692
|
+
private getColumnName(slug: string): string {
|
|
693
|
+
return slug;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Validate a slug
|
|
698
|
+
*/
|
|
699
|
+
private validateSlug(slug: string, type: "collection" | "field"): void {
|
|
700
|
+
if (!slug || typeof slug !== "string") {
|
|
701
|
+
throw new SchemaError(`${type} slug is required`, "INVALID_SLUG");
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (!SLUG_VALIDATION_PATTERN.test(slug)) {
|
|
705
|
+
throw new SchemaError(
|
|
706
|
+
`${type} slug must start with a letter and contain only lowercase letters, numbers, and underscores`,
|
|
707
|
+
"INVALID_SLUG",
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (slug.length > 63) {
|
|
712
|
+
throw new SchemaError(`${type} slug must be 63 characters or less`, "INVALID_SLUG");
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Format a default value for SQL.
|
|
718
|
+
*
|
|
719
|
+
* SQLite `ALTER TABLE ADD COLUMN ... DEFAULT` requires a literal constant
|
|
720
|
+
* expression — parameterized values cannot be used here. We manually escape
|
|
721
|
+
* single quotes and coerce types to ensure the output is safe.
|
|
722
|
+
*
|
|
723
|
+
* INTEGER/REAL values are coerced through `Number()` which can only produce
|
|
724
|
+
* digits, `.`, `-`, `e`, `Infinity`, or `NaN` — all safe in SQL.
|
|
725
|
+
* TEXT/JSON values have single quotes escaped via SQL standard doubling (`''`).
|
|
726
|
+
*/
|
|
727
|
+
private formatDefaultValue(value: unknown, fieldType: FieldType): string {
|
|
728
|
+
if (value === null || value === undefined) {
|
|
729
|
+
return "NULL";
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const columnType = FIELD_TYPE_TO_COLUMN[fieldType];
|
|
733
|
+
|
|
734
|
+
if (columnType === "JSON") {
|
|
735
|
+
// JSON.stringify produces valid JSON; escape single quotes for SQL literal
|
|
736
|
+
const json = JSON.stringify(value);
|
|
737
|
+
return `'${json.replace(SINGLE_QUOTE_PATTERN, "''")}'`;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (columnType === "INTEGER") {
|
|
741
|
+
if (typeof value === "boolean") {
|
|
742
|
+
return value ? "1" : "0";
|
|
743
|
+
}
|
|
744
|
+
const num = Number(value);
|
|
745
|
+
if (!Number.isFinite(num)) {
|
|
746
|
+
return "0";
|
|
747
|
+
}
|
|
748
|
+
return String(Math.trunc(num));
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (columnType === "REAL") {
|
|
752
|
+
const num = Number(value);
|
|
753
|
+
if (!Number.isFinite(num)) {
|
|
754
|
+
return "0";
|
|
755
|
+
}
|
|
756
|
+
return String(num);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// TEXT — escape single quotes via SQL standard doubling
|
|
760
|
+
let text: string;
|
|
761
|
+
if (typeof value === "string") {
|
|
762
|
+
text = value;
|
|
763
|
+
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
764
|
+
text = String(value);
|
|
765
|
+
} else if (typeof value === "object" && value !== null) {
|
|
766
|
+
text = JSON.stringify(value);
|
|
767
|
+
} else {
|
|
768
|
+
text = "";
|
|
769
|
+
}
|
|
770
|
+
return `'${text.replace(SINGLE_QUOTE_PATTERN, "''")}'`;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Get empty default for a field type
|
|
775
|
+
*/
|
|
776
|
+
private getEmptyDefault(fieldType: FieldType): string {
|
|
777
|
+
const columnType = FIELD_TYPE_TO_COLUMN[fieldType];
|
|
778
|
+
|
|
779
|
+
switch (columnType) {
|
|
780
|
+
case "INTEGER":
|
|
781
|
+
return "0";
|
|
782
|
+
case "REAL":
|
|
783
|
+
return "0.0";
|
|
784
|
+
case "JSON":
|
|
785
|
+
return "'null'";
|
|
786
|
+
default:
|
|
787
|
+
return "''";
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Map a collection row to a Collection object
|
|
793
|
+
*/
|
|
794
|
+
private mapCollectionRow = (row: Selectable<CollectionTable>): Collection => {
|
|
795
|
+
const moderation = row.comments_moderation;
|
|
796
|
+
return {
|
|
797
|
+
id: row.id,
|
|
798
|
+
slug: row.slug,
|
|
799
|
+
label: row.label,
|
|
800
|
+
labelSingular: row.label_singular ?? undefined,
|
|
801
|
+
description: row.description ?? undefined,
|
|
802
|
+
icon: row.icon ?? undefined,
|
|
803
|
+
supports: row.supports ? JSON.parse(row.supports) : [],
|
|
804
|
+
source: row.source && isCollectionSource(row.source) ? row.source : undefined,
|
|
805
|
+
hasSeo: row.has_seo === 1,
|
|
806
|
+
urlPattern: row.url_pattern ?? undefined,
|
|
807
|
+
commentsEnabled: row.comments_enabled === 1,
|
|
808
|
+
commentsModeration:
|
|
809
|
+
moderation === "all" || moderation === "first_time" || moderation === "none"
|
|
810
|
+
? moderation
|
|
811
|
+
: "first_time",
|
|
812
|
+
commentsClosedAfterDays: row.comments_closed_after_days ?? 90,
|
|
813
|
+
commentsAutoApproveUsers: row.comments_auto_approve_users === 1,
|
|
814
|
+
createdAt: row.created_at,
|
|
815
|
+
updatedAt: row.updated_at,
|
|
816
|
+
};
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Map a field row to a Field object
|
|
821
|
+
*/
|
|
822
|
+
private mapFieldRow = (row: Selectable<FieldTable>): Field => {
|
|
823
|
+
return {
|
|
824
|
+
id: row.id,
|
|
825
|
+
collectionId: row.collection_id,
|
|
826
|
+
slug: row.slug,
|
|
827
|
+
label: row.label,
|
|
828
|
+
type: isFieldType(row.type) ? row.type : "string",
|
|
829
|
+
columnType: isColumnType(row.column_type) ? row.column_type : "TEXT",
|
|
830
|
+
required: row.required === 1,
|
|
831
|
+
unique: row.unique === 1,
|
|
832
|
+
defaultValue: row.default_value ? JSON.parse(row.default_value) : undefined,
|
|
833
|
+
validation: row.validation ? JSON.parse(row.validation) : undefined,
|
|
834
|
+
widget: row.widget ?? undefined,
|
|
835
|
+
options: row.options ? JSON.parse(row.options) : undefined,
|
|
836
|
+
sortOrder: row.sort_order,
|
|
837
|
+
searchable: row.searchable === 1,
|
|
838
|
+
translatable: row.translatable !== 0,
|
|
839
|
+
createdAt: row.created_at,
|
|
840
|
+
};
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
// ============================================
|
|
844
|
+
// Discovery
|
|
845
|
+
// ============================================
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Discover orphaned content tables
|
|
849
|
+
*
|
|
850
|
+
* Finds ec_* tables that exist in the database but don't have a
|
|
851
|
+
* corresponding entry in _emdash_collections.
|
|
852
|
+
*/
|
|
853
|
+
async discoverOrphanedTables(): Promise<
|
|
854
|
+
Array<{ slug: string; tableName: string; rowCount: number }>
|
|
855
|
+
> {
|
|
856
|
+
// Get all ec_* tables
|
|
857
|
+
// Content tables are ec_* (e.g., ec_posts, ec_pages)
|
|
858
|
+
// Internal tables are _emdash_* (e.g., _emdash_collections, _emdash_fts_posts)
|
|
859
|
+
const allTables = await listTablesLike(this.db, "ec_%");
|
|
860
|
+
|
|
861
|
+
// Get registered collections
|
|
862
|
+
const registered = await this.listCollections();
|
|
863
|
+
const registeredSlugs = new Set(registered.map((c) => c.slug));
|
|
864
|
+
|
|
865
|
+
// Find orphans
|
|
866
|
+
const orphans: Array<{
|
|
867
|
+
slug: string;
|
|
868
|
+
tableName: string;
|
|
869
|
+
rowCount: number;
|
|
870
|
+
}> = [];
|
|
871
|
+
|
|
872
|
+
for (const tableName of allTables) {
|
|
873
|
+
const slug = tableName.replace(EC_PREFIX_PATTERN, "");
|
|
874
|
+
|
|
875
|
+
if (!registeredSlugs.has(slug)) {
|
|
876
|
+
// Count rows in the orphaned table
|
|
877
|
+
try {
|
|
878
|
+
const countResult = await sql<{ count: number }>`
|
|
879
|
+
SELECT COUNT(*) as count FROM ${sql.ref(tableName)}
|
|
880
|
+
WHERE deleted_at IS NULL
|
|
881
|
+
`.execute(this.db);
|
|
882
|
+
|
|
883
|
+
orphans.push({
|
|
884
|
+
slug,
|
|
885
|
+
tableName,
|
|
886
|
+
rowCount: countResult.rows[0]?.count ?? 0,
|
|
887
|
+
});
|
|
888
|
+
} catch {
|
|
889
|
+
// Table might have unexpected schema, still report it
|
|
890
|
+
orphans.push({
|
|
891
|
+
slug,
|
|
892
|
+
tableName,
|
|
893
|
+
rowCount: 0,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return orphans;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Register an orphaned table as a collection
|
|
904
|
+
*
|
|
905
|
+
* Creates a _emdash_collections entry for an existing ec_* table.
|
|
906
|
+
*/
|
|
907
|
+
async registerOrphanedTable(
|
|
908
|
+
slug: string,
|
|
909
|
+
options?: {
|
|
910
|
+
label?: string;
|
|
911
|
+
labelSingular?: string;
|
|
912
|
+
description?: string;
|
|
913
|
+
},
|
|
914
|
+
): Promise<Collection> {
|
|
915
|
+
// Verify table exists
|
|
916
|
+
const tableName = this.getTableName(slug);
|
|
917
|
+
const exists = await tableExists(this.db, tableName);
|
|
918
|
+
|
|
919
|
+
if (!exists) {
|
|
920
|
+
throw new SchemaError(`Table "${tableName}" does not exist`, "TABLE_NOT_FOUND");
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Check if already registered
|
|
924
|
+
const existing = await this.getCollection(slug);
|
|
925
|
+
if (existing) {
|
|
926
|
+
throw new SchemaError(`Collection "${slug}" is already registered`, "COLLECTION_EXISTS");
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Create collection entry
|
|
930
|
+
const id = ulid();
|
|
931
|
+
const label = options?.label || this.slugToLabel(slug);
|
|
932
|
+
|
|
933
|
+
await this.db
|
|
934
|
+
.insertInto("_emdash_collections")
|
|
935
|
+
.values({
|
|
936
|
+
id,
|
|
937
|
+
slug,
|
|
938
|
+
label,
|
|
939
|
+
label_singular: options?.labelSingular ?? null,
|
|
940
|
+
description: options?.description ?? null,
|
|
941
|
+
icon: null,
|
|
942
|
+
supports: JSON.stringify([]),
|
|
943
|
+
source: "discovered",
|
|
944
|
+
has_seo: 0,
|
|
945
|
+
url_pattern: null,
|
|
946
|
+
})
|
|
947
|
+
.execute();
|
|
948
|
+
|
|
949
|
+
const collection = await this.getCollection(slug);
|
|
950
|
+
if (!collection) {
|
|
951
|
+
throw new SchemaError("Failed to register orphaned table", "REGISTER_FAILED");
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return collection;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Convert slug to human-readable label
|
|
959
|
+
*/
|
|
960
|
+
private slugToLabel(slug: string): string {
|
|
961
|
+
return slug
|
|
962
|
+
.replace(UNDERSCORE_PATTERN, " ")
|
|
963
|
+
.replace(WORD_BOUNDARY_PATTERN, (c) => c.toUpperCase());
|
|
964
|
+
}
|
|
965
|
+
}
|