emdash 0.0.0-a → 0.0.1
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 -1
- 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 +1333 -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-B-u2F2b6.mjs +1412 -0
- package/dist/runner-B-u2F2b6.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 +687 -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 +353 -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 +328 -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 +120 -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 +117 -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 +105 -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 +62 -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 +72 -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 +699 -0
- package/src/cli/commands/schema.ts +233 -0
- package/src/cli/commands/search-cmd.ts +54 -0
- package/src/cli/commands/seed.ts +288 -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 +136 -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 +42 -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 +253 -0
- package/src/storage/s3.ts +271 -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,56 @@
|
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
|
+
import { sql } from "kysely";
|
|
3
|
+
|
|
4
|
+
import { currentTimestamp } from "../dialect-helpers.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Migration: SEO support
|
|
8
|
+
*
|
|
9
|
+
* Creates:
|
|
10
|
+
* - `_emdash_seo` table: per-content SEO metadata (separate from content tables)
|
|
11
|
+
* - `has_seo` column on `_emdash_collections`: opt-in flag per collection
|
|
12
|
+
*
|
|
13
|
+
* SEO is not a universal concern — only collections representing web pages
|
|
14
|
+
* need it. The `has_seo` flag controls whether the admin shows SEO fields
|
|
15
|
+
* and whether the collection's content appears in sitemaps.
|
|
16
|
+
*/
|
|
17
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
18
|
+
// Create the SEO table
|
|
19
|
+
await db.schema
|
|
20
|
+
.createTable("_emdash_seo")
|
|
21
|
+
.addColumn("collection", "text", (col) => col.notNull())
|
|
22
|
+
.addColumn("content_id", "text", (col) => col.notNull())
|
|
23
|
+
.addColumn("seo_title", "text")
|
|
24
|
+
.addColumn("seo_description", "text")
|
|
25
|
+
.addColumn("seo_image", "text")
|
|
26
|
+
.addColumn("seo_canonical", "text")
|
|
27
|
+
.addColumn("seo_no_index", "integer", (col) => col.notNull().defaultTo(0))
|
|
28
|
+
.addColumn("created_at", "text", (col) => col.notNull().defaultTo(currentTimestamp(db)))
|
|
29
|
+
.addColumn("updated_at", "text", (col) => col.notNull().defaultTo(currentTimestamp(db)))
|
|
30
|
+
.addPrimaryKeyConstraint("_emdash_seo_pk", ["collection", "content_id"])
|
|
31
|
+
.execute();
|
|
32
|
+
|
|
33
|
+
// Index for batch lookups by collection (PK covers point lookups).
|
|
34
|
+
// Sitemap queries join on (collection, content_id) which the PK covers,
|
|
35
|
+
// and filter seo_no_index. This index supports getMany() batch queries.
|
|
36
|
+
await sql`
|
|
37
|
+
CREATE INDEX idx_emdash_seo_collection
|
|
38
|
+
ON _emdash_seo (collection)
|
|
39
|
+
`.execute(db);
|
|
40
|
+
|
|
41
|
+
// Add has_seo flag to collections
|
|
42
|
+
await sql`
|
|
43
|
+
ALTER TABLE _emdash_collections
|
|
44
|
+
ADD COLUMN has_seo INTEGER NOT NULL DEFAULT 0
|
|
45
|
+
`.execute(db);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
49
|
+
await sql`DROP TABLE IF EXISTS _emdash_seo`.execute(db);
|
|
50
|
+
|
|
51
|
+
// SQLite doesn't support DROP COLUMN before 3.35.0, but D1 does
|
|
52
|
+
await sql`
|
|
53
|
+
ALTER TABLE _emdash_collections
|
|
54
|
+
DROP COLUMN has_seo
|
|
55
|
+
`.execute(db);
|
|
56
|
+
}
|
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
|
+
import { sql } from "kysely";
|
|
3
|
+
|
|
4
|
+
import { isSqlite, listTablesLike } from "../dialect-helpers.js";
|
|
5
|
+
import { validateIdentifier } from "../validate.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Migration: i18n support (row-per-locale model)
|
|
9
|
+
*
|
|
10
|
+
* Each piece of content can exist in multiple locales. Translations of the
|
|
11
|
+
* same content share a `translation_group` ULID while each row carries its
|
|
12
|
+
* own `locale` code. Slugs are unique per-locale, not globally.
|
|
13
|
+
*
|
|
14
|
+
* Changes:
|
|
15
|
+
* 1. For every ec_* content table:
|
|
16
|
+
* - Rebuild the table to replace inline `slug TEXT UNIQUE` with
|
|
17
|
+
* `slug TEXT` + a compound `UNIQUE(slug, locale)` constraint.
|
|
18
|
+
* - Add `locale TEXT NOT NULL DEFAULT 'en'`
|
|
19
|
+
* - Add `translation_group TEXT`
|
|
20
|
+
* - Backfill `translation_group = id` for existing rows
|
|
21
|
+
* - Recreate all standard indexes plus new locale/translation_group indexes
|
|
22
|
+
*
|
|
23
|
+
* 2. Add `translatable` column to `_emdash_fields`
|
|
24
|
+
*
|
|
25
|
+
* The table-rebuild approach is required because SQLite cannot drop an inline
|
|
26
|
+
* UNIQUE constraint via ALTER TABLE. We use PRAGMA table_info to discover all
|
|
27
|
+
* columns (including dynamic user-defined fields) and rebuild dynamically.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// Column info returned by PRAGMA table_info
|
|
31
|
+
interface ColumnInfo {
|
|
32
|
+
cid: number;
|
|
33
|
+
name: string;
|
|
34
|
+
type: string;
|
|
35
|
+
notnull: number;
|
|
36
|
+
dflt_value: string | null;
|
|
37
|
+
pk: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Index info returned by PRAGMA index_list
|
|
41
|
+
interface IndexInfo {
|
|
42
|
+
seq: number;
|
|
43
|
+
name: string;
|
|
44
|
+
unique: number;
|
|
45
|
+
origin: string;
|
|
46
|
+
partial: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Index column info returned by PRAGMA index_info
|
|
50
|
+
interface IndexColumnInfo {
|
|
51
|
+
seqno: number;
|
|
52
|
+
cid: number;
|
|
53
|
+
name: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Quote an identifier for use in raw SQL. Escapes embedded double-quotes
|
|
58
|
+
* per SQL standard (double them). The name should first pass
|
|
59
|
+
* validateIdentifier() or validateTableName() for defense-in-depth.
|
|
60
|
+
*/
|
|
61
|
+
const DOUBLE_QUOTE_RE = /"/g;
|
|
62
|
+
function quoteIdent(name: string): string {
|
|
63
|
+
return `"${name.replace(DOUBLE_QUOTE_RE, '""')}"`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Suffix added to tmp tables during i18n migration rebuild. */
|
|
67
|
+
const I18N_TMP_SUFFIX = /_i18n_tmp$/;
|
|
68
|
+
|
|
69
|
+
/** Table names from sqlite_master are ec_{slug} — validate the pattern. */
|
|
70
|
+
const TABLE_NAME_PATTERN = /^ec_[a-z][a-z0-9_]*$/;
|
|
71
|
+
function validateTableName(name: string): void {
|
|
72
|
+
if (!TABLE_NAME_PATTERN.test(name)) {
|
|
73
|
+
throw new Error(`Invalid content table name: "${name}"`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** SQLite column types produced by EmDash's schema registry. */
|
|
78
|
+
const ALLOWED_COLUMN_TYPES = new Set(["TEXT", "INTEGER", "REAL", "BLOB", "JSON", "NUMERIC", ""]);
|
|
79
|
+
function validateColumnType(type: string, colName: string): void {
|
|
80
|
+
if (!ALLOWED_COLUMN_TYPES.has(type.toUpperCase())) {
|
|
81
|
+
throw new Error(`Unexpected column type "${type}" for column "${colName}"`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validate that a default value expression from PRAGMA table_info is safe
|
|
87
|
+
* to interpolate into DDL. Allows: string literals, numeric literals,
|
|
88
|
+
* NULL, and known function calls like datetime('now').
|
|
89
|
+
*
|
|
90
|
+
* Note: PRAGMA table_info strips the outer parens from expression defaults,
|
|
91
|
+
* so `DEFAULT (datetime('now'))` is reported as `datetime('now')`.
|
|
92
|
+
* We accept both forms and re-wrap in parens via normalizeDdlDefault().
|
|
93
|
+
*/
|
|
94
|
+
const SAFE_DEFAULT_PATTERN =
|
|
95
|
+
/^(?:'[^']*'|NULL|-?\d+(?:\.\d+)?|\(?datetime\('now'\)\)?|\(?json\('[^']*'\)\)?|0|1)$/i;
|
|
96
|
+
function validateDefaultValue(value: string, colName: string): void {
|
|
97
|
+
if (!SAFE_DEFAULT_PATTERN.test(value)) {
|
|
98
|
+
throw new Error(`Unexpected default value "${value}" for column "${colName}"`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Normalize a PRAGMA table_info default value for use in DDL.
|
|
104
|
+
* Function-call defaults (e.g. `datetime('now')`) must be wrapped in parens
|
|
105
|
+
* to form valid expression defaults: `DEFAULT (datetime('now'))`.
|
|
106
|
+
* PRAGMA strips the outer parens, so we re-add them here.
|
|
107
|
+
*/
|
|
108
|
+
const FUNCTION_DEFAULT_PATTERN = /^(?:datetime|json)\(/i;
|
|
109
|
+
function normalizeDdlDefault(value: string): string {
|
|
110
|
+
// Already wrapped in parens — return as-is
|
|
111
|
+
if (value.startsWith("(")) return value;
|
|
112
|
+
if (FUNCTION_DEFAULT_PATTERN.test(value)) return `(${value})`;
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Validate that a CREATE INDEX statement from sqlite_master is safe to replay.
|
|
118
|
+
* Must start with CREATE [UNIQUE] INDEX and not contain suspicious patterns.
|
|
119
|
+
*/
|
|
120
|
+
const CREATE_INDEX_PATTERN = /^CREATE\s+(UNIQUE\s+)?INDEX\s+/i;
|
|
121
|
+
function validateCreateIndexSql(sqlStr: string, idxName: string): void {
|
|
122
|
+
if (!CREATE_INDEX_PATTERN.test(sqlStr)) {
|
|
123
|
+
throw new Error(`Unexpected index SQL for "${idxName}": does not match CREATE INDEX pattern`);
|
|
124
|
+
}
|
|
125
|
+
// Reject semicolons which could allow statement injection
|
|
126
|
+
if (sqlStr.includes(";")) {
|
|
127
|
+
throw new Error(`Unexpected index SQL for "${idxName}": contains semicolon`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* PostgreSQL path: ALTER TABLE supports ADD COLUMN and DROP CONSTRAINT directly.
|
|
133
|
+
* No table rebuild needed.
|
|
134
|
+
*/
|
|
135
|
+
async function upPostgres(db: Kysely<unknown>): Promise<void> {
|
|
136
|
+
const tableNames = await listTablesLike(db, "ec_%");
|
|
137
|
+
|
|
138
|
+
for (const t of tableNames) {
|
|
139
|
+
validateTableName(t);
|
|
140
|
+
|
|
141
|
+
// Check if already migrated (idempotency)
|
|
142
|
+
const hasLocale = await sql<{ exists: boolean }>`
|
|
143
|
+
SELECT EXISTS(
|
|
144
|
+
SELECT 1 FROM information_schema.columns
|
|
145
|
+
WHERE table_schema = 'public' AND table_name = ${t} AND column_name = 'locale'
|
|
146
|
+
) as exists
|
|
147
|
+
`.execute(db);
|
|
148
|
+
if (hasLocale.rows[0]?.exists === true) continue;
|
|
149
|
+
|
|
150
|
+
// Add i18n columns
|
|
151
|
+
await sql`ALTER TABLE ${sql.ref(t)} ADD COLUMN locale TEXT NOT NULL DEFAULT 'en'`.execute(db);
|
|
152
|
+
await sql`ALTER TABLE ${sql.ref(t)} ADD COLUMN translation_group TEXT`.execute(db);
|
|
153
|
+
|
|
154
|
+
// Drop existing unique constraint on slug (Postgres auto-names these)
|
|
155
|
+
// Find the constraint name first
|
|
156
|
+
const constraints = await sql<{ conname: string }>`
|
|
157
|
+
SELECT conname FROM pg_constraint
|
|
158
|
+
WHERE conrelid = ${t}::regclass
|
|
159
|
+
AND contype = 'u'
|
|
160
|
+
AND array_length(conkey, 1) = 1
|
|
161
|
+
AND conkey[1] = (
|
|
162
|
+
SELECT attnum FROM pg_attribute
|
|
163
|
+
WHERE attrelid = ${t}::regclass AND attname = 'slug'
|
|
164
|
+
)
|
|
165
|
+
`.execute(db);
|
|
166
|
+
|
|
167
|
+
for (const c of constraints.rows) {
|
|
168
|
+
await sql`ALTER TABLE ${sql.ref(t)} DROP CONSTRAINT ${sql.ref(c.conname)}`.execute(db);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Add compound unique constraint
|
|
172
|
+
await sql`
|
|
173
|
+
ALTER TABLE ${sql.ref(t)}
|
|
174
|
+
ADD CONSTRAINT ${sql.ref(`${t}_slug_locale_unique`)} UNIQUE (slug, locale)
|
|
175
|
+
`.execute(db);
|
|
176
|
+
|
|
177
|
+
// Backfill translation_group
|
|
178
|
+
await sql`UPDATE ${sql.ref(t)} SET translation_group = id`.execute(db);
|
|
179
|
+
|
|
180
|
+
// Create indexes
|
|
181
|
+
await sql`CREATE INDEX ${sql.ref(`idx_${t}_locale`)} ON ${sql.ref(t)} (locale)`.execute(db);
|
|
182
|
+
await sql`
|
|
183
|
+
CREATE INDEX ${sql.ref(`idx_${t}_translation_group`)}
|
|
184
|
+
ON ${sql.ref(t)} (translation_group)
|
|
185
|
+
`.execute(db);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Add translatable flag to fields table
|
|
189
|
+
const hasTranslatable = await sql<{ exists: boolean }>`
|
|
190
|
+
SELECT EXISTS(
|
|
191
|
+
SELECT 1 FROM information_schema.columns
|
|
192
|
+
WHERE table_schema = 'public' AND table_name = '_emdash_fields' AND column_name = 'translatable'
|
|
193
|
+
) as exists
|
|
194
|
+
`.execute(db);
|
|
195
|
+
if (hasTranslatable.rows[0]?.exists !== true) {
|
|
196
|
+
await sql`
|
|
197
|
+
ALTER TABLE _emdash_fields
|
|
198
|
+
ADD COLUMN translatable INTEGER NOT NULL DEFAULT 1
|
|
199
|
+
`.execute(db);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
204
|
+
if (!isSqlite(db)) {
|
|
205
|
+
return upPostgres(db);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Clean up orphaned tmp tables from a previous partial run.
|
|
209
|
+
// On D1 (no transactions), a crash mid-migration can leave these behind.
|
|
210
|
+
const orphanedTmps = await listTablesLike(db, "ec_%_i18n_tmp");
|
|
211
|
+
for (const tmpName of orphanedTmps) {
|
|
212
|
+
validateTableName(tmpName.replace(I18N_TMP_SUFFIX, ""));
|
|
213
|
+
await sql`DROP TABLE IF EXISTS ${sql.ref(tmpName)}`.execute(db);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Discover all ec_* content tables
|
|
217
|
+
const tableNames = await listTablesLike(db, "ec_%");
|
|
218
|
+
const tables = { rows: tableNames.map((name) => ({ name })) };
|
|
219
|
+
|
|
220
|
+
for (const table of tables.rows) {
|
|
221
|
+
const t = table.name;
|
|
222
|
+
validateTableName(t);
|
|
223
|
+
const tmp = `${t}_i18n_tmp`;
|
|
224
|
+
|
|
225
|
+
// Note: no transaction wrapper — D1 doesn't support transactions,
|
|
226
|
+
// and SQLite/better-sqlite3 is single-writer so crash-safety is
|
|
227
|
+
// handled by the journal mode. The tmp table approach is already
|
|
228
|
+
// crash-recoverable (orphaned tmp tables are harmless).
|
|
229
|
+
{
|
|
230
|
+
const trx = db;
|
|
231
|
+
// ── 1. Read existing column definitions ──────────────────────
|
|
232
|
+
const colResult = await sql<ColumnInfo>`
|
|
233
|
+
PRAGMA table_info(${sql.ref(t)})
|
|
234
|
+
`.execute(trx);
|
|
235
|
+
const columns = colResult.rows;
|
|
236
|
+
|
|
237
|
+
// ── Idempotency: skip tables already migrated ───────────────
|
|
238
|
+
// On D1, the migrator can't use transactions, so a partially-
|
|
239
|
+
// applied migration may not be recorded. If the table already
|
|
240
|
+
// has a `locale` column, it was already rebuilt — skip it.
|
|
241
|
+
if (columns.some((col) => col.name === "locale")) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── 2. Read existing indexes (to recreate after rebuild) ─────
|
|
246
|
+
const idxResult = await sql<IndexInfo>`
|
|
247
|
+
PRAGMA index_list(${sql.ref(t)})
|
|
248
|
+
`.execute(trx);
|
|
249
|
+
|
|
250
|
+
// Collect non-autoindex, non-primary-key indexes for recreation
|
|
251
|
+
const indexDefs: { name: string; unique: boolean; columns: string[]; partial: number }[] = [];
|
|
252
|
+
for (const idx of idxResult.rows) {
|
|
253
|
+
// Skip autoindexes (created by inline UNIQUE) — we're removing them
|
|
254
|
+
if (idx.origin === "pk" || idx.name.startsWith("sqlite_autoindex_")) continue;
|
|
255
|
+
|
|
256
|
+
const idxColResult = await sql<IndexColumnInfo>`
|
|
257
|
+
PRAGMA index_info(${sql.ref(idx.name)})
|
|
258
|
+
`.execute(trx);
|
|
259
|
+
|
|
260
|
+
indexDefs.push({
|
|
261
|
+
name: idx.name,
|
|
262
|
+
unique: idx.unique === 1,
|
|
263
|
+
columns: idxColResult.rows.map((c) => c.name),
|
|
264
|
+
partial: idx.partial,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// For partial indexes we need the original CREATE statement
|
|
269
|
+
const partialSqls = new Map<string, string>();
|
|
270
|
+
for (const idx of indexDefs) {
|
|
271
|
+
if (idx.partial) {
|
|
272
|
+
const createResult = await sql<{ sql: string }>`
|
|
273
|
+
SELECT sql FROM sqlite_master
|
|
274
|
+
WHERE type = 'index' AND name = ${idx.name}
|
|
275
|
+
`.execute(trx);
|
|
276
|
+
if (createResult.rows[0]?.sql) {
|
|
277
|
+
partialSqls.set(idx.name, createResult.rows[0].sql);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── 3. Build column defs for the new table ──────────────────
|
|
283
|
+
// Validate all column names from PRAGMA before using them in raw SQL.
|
|
284
|
+
// These originate from our own schema, but defense-in-depth matters.
|
|
285
|
+
for (const col of columns) {
|
|
286
|
+
validateIdentifier(col.name, "column name");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Replace slug's inline UNIQUE with a table-level UNIQUE(slug, locale)
|
|
290
|
+
const colDefs: string[] = [];
|
|
291
|
+
const colNames: string[] = [];
|
|
292
|
+
|
|
293
|
+
for (const col of columns) {
|
|
294
|
+
validateColumnType(col.type || "TEXT", col.name);
|
|
295
|
+
colNames.push(quoteIdent(col.name));
|
|
296
|
+
let def = `${quoteIdent(col.name)} ${col.type || "TEXT"}`;
|
|
297
|
+
|
|
298
|
+
if (col.pk) {
|
|
299
|
+
def += " PRIMARY KEY";
|
|
300
|
+
} else if (col.name === "slug") {
|
|
301
|
+
// Intentionally omit UNIQUE — compound unique below
|
|
302
|
+
} else {
|
|
303
|
+
if (col.notnull) def += " NOT NULL";
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (col.dflt_value !== null) {
|
|
307
|
+
validateDefaultValue(col.dflt_value, col.name);
|
|
308
|
+
def += ` DEFAULT ${normalizeDdlDefault(col.dflt_value)}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
colDefs.push(def);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Append new i18n columns
|
|
315
|
+
colDefs.push("\"locale\" TEXT NOT NULL DEFAULT 'en'");
|
|
316
|
+
colDefs.push('"translation_group" TEXT');
|
|
317
|
+
|
|
318
|
+
// Compound unique: same slug + locale must be unique
|
|
319
|
+
colDefs.push('UNIQUE("slug", "locale")');
|
|
320
|
+
|
|
321
|
+
const createColsSql = colDefs.join(",\n\t\t\t\t");
|
|
322
|
+
const selectColsSql = colNames.join(", ");
|
|
323
|
+
|
|
324
|
+
// ── 4. Rebuild the table ────────────────────────────────────
|
|
325
|
+
// Drop all existing indexes first (they reference the old table)
|
|
326
|
+
for (const idx of indexDefs) {
|
|
327
|
+
await sql`DROP INDEX IF EXISTS ${sql.ref(idx.name)}`.execute(trx);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Create new table with updated schema
|
|
331
|
+
await sql
|
|
332
|
+
.raw(`CREATE TABLE ${quoteIdent(tmp)} (\n\t\t\t\t${createColsSql}\n\t\t\t)`)
|
|
333
|
+
.execute(trx);
|
|
334
|
+
|
|
335
|
+
// Copy existing data, backfilling locale='en' and translation_group=id
|
|
336
|
+
await sql
|
|
337
|
+
.raw(
|
|
338
|
+
`INSERT INTO ${quoteIdent(tmp)} (${selectColsSql}, "locale", "translation_group")\n\t\t\t SELECT ${selectColsSql}, 'en', "id" FROM ${quoteIdent(t)}`,
|
|
339
|
+
)
|
|
340
|
+
.execute(trx);
|
|
341
|
+
|
|
342
|
+
// Swap tables
|
|
343
|
+
await sql`DROP TABLE ${sql.ref(t)}`.execute(trx);
|
|
344
|
+
await sql.raw(`ALTER TABLE ${quoteIdent(tmp)} RENAME TO ${quoteIdent(t)}`).execute(trx);
|
|
345
|
+
|
|
346
|
+
// ── 5. Recreate all original indexes ────────────────────────
|
|
347
|
+
for (const idx of indexDefs) {
|
|
348
|
+
// Skip the old slug-only index — replaced by slug_locale below
|
|
349
|
+
if (idx.name === `idx_${t}_slug`) continue;
|
|
350
|
+
|
|
351
|
+
if (idx.partial && partialSqls.has(idx.name)) {
|
|
352
|
+
// Partial indexes — validate the SQL before replaying
|
|
353
|
+
const idxSql = partialSqls.get(idx.name)!;
|
|
354
|
+
validateCreateIndexSql(idxSql, idx.name);
|
|
355
|
+
await sql.raw(idxSql).execute(trx);
|
|
356
|
+
} else {
|
|
357
|
+
// Validate index column names before interpolation
|
|
358
|
+
for (const c of idx.columns) {
|
|
359
|
+
validateIdentifier(c, "index column name");
|
|
360
|
+
}
|
|
361
|
+
const cols = idx.columns.map((c) => quoteIdent(c)).join(", ");
|
|
362
|
+
const unique = idx.unique ? "UNIQUE " : "";
|
|
363
|
+
await sql
|
|
364
|
+
.raw(`CREATE ${unique}INDEX ${quoteIdent(idx.name)} ON ${quoteIdent(t)} (${cols})`)
|
|
365
|
+
.execute(trx);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ── 6. Create new i18n indexes ──────────────────────────────
|
|
370
|
+
// slug_locale unique is handled by the table constraint above,
|
|
371
|
+
// but we still want a regular slug index for non-locale-aware queries
|
|
372
|
+
await sql`
|
|
373
|
+
CREATE INDEX ${sql.ref(`idx_${t}_slug`)}
|
|
374
|
+
ON ${sql.ref(t)} (slug)
|
|
375
|
+
`.execute(trx);
|
|
376
|
+
|
|
377
|
+
await sql`
|
|
378
|
+
CREATE INDEX ${sql.ref(`idx_${t}_locale`)}
|
|
379
|
+
ON ${sql.ref(t)} (locale)
|
|
380
|
+
`.execute(trx);
|
|
381
|
+
|
|
382
|
+
await sql`
|
|
383
|
+
CREATE INDEX ${sql.ref(`idx_${t}_translation_group`)}
|
|
384
|
+
ON ${sql.ref(t)} (translation_group)
|
|
385
|
+
`.execute(trx);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ── 7. Add translatable flag to fields table ────────────────────
|
|
390
|
+
// Guard against duplicate column — on D1 the migration may have
|
|
391
|
+
// partially applied without being recorded (no transaction support).
|
|
392
|
+
const fieldCols = await sql<ColumnInfo>`
|
|
393
|
+
PRAGMA table_info(_emdash_fields)
|
|
394
|
+
`.execute(db);
|
|
395
|
+
if (!fieldCols.rows.some((col) => col.name === "translatable")) {
|
|
396
|
+
await sql`
|
|
397
|
+
ALTER TABLE _emdash_fields
|
|
398
|
+
ADD COLUMN translatable INTEGER NOT NULL DEFAULT 1
|
|
399
|
+
`.execute(db);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* PostgreSQL down path: straightforward ALTER TABLE operations.
|
|
405
|
+
*/
|
|
406
|
+
async function downPostgres(db: Kysely<unknown>): Promise<void> {
|
|
407
|
+
await sql`ALTER TABLE _emdash_fields DROP COLUMN translatable`.execute(db);
|
|
408
|
+
|
|
409
|
+
const tableNames = await listTablesLike(db, "ec_%");
|
|
410
|
+
for (const t of tableNames) {
|
|
411
|
+
validateTableName(t);
|
|
412
|
+
|
|
413
|
+
// Drop i18n indexes
|
|
414
|
+
await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${t}_locale`)}`.execute(db);
|
|
415
|
+
await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${t}_translation_group`)}`.execute(db);
|
|
416
|
+
|
|
417
|
+
// Drop compound unique constraint
|
|
418
|
+
await sql`ALTER TABLE ${sql.ref(t)} DROP CONSTRAINT IF EXISTS ${sql.ref(`${t}_slug_locale_unique`)}`.execute(
|
|
419
|
+
db,
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
// Restore simple unique constraint on slug
|
|
423
|
+
await sql`ALTER TABLE ${sql.ref(t)} ADD CONSTRAINT ${sql.ref(`${t}_slug_unique`)} UNIQUE (slug)`.execute(
|
|
424
|
+
db,
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// Drop i18n columns
|
|
428
|
+
await sql`ALTER TABLE ${sql.ref(t)} DROP COLUMN locale`.execute(db);
|
|
429
|
+
await sql`ALTER TABLE ${sql.ref(t)} DROP COLUMN translation_group`.execute(db);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
434
|
+
if (!isSqlite(db)) {
|
|
435
|
+
return downPostgres(db);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Remove translatable column from fields table
|
|
439
|
+
await sql`
|
|
440
|
+
ALTER TABLE _emdash_fields
|
|
441
|
+
DROP COLUMN translatable
|
|
442
|
+
`.execute(db);
|
|
443
|
+
|
|
444
|
+
// Discover all ec_* content tables
|
|
445
|
+
const tableNames = await listTablesLike(db, "ec_%");
|
|
446
|
+
|
|
447
|
+
for (const tableName of tableNames) {
|
|
448
|
+
const t = tableName;
|
|
449
|
+
validateTableName(t);
|
|
450
|
+
const tmp = `${t}_i18n_tmp`;
|
|
451
|
+
|
|
452
|
+
// No transaction — see comment in up() above.
|
|
453
|
+
{
|
|
454
|
+
const trx = db;
|
|
455
|
+
// ── 1. Read current column definitions ──────────────────────
|
|
456
|
+
const colResult = await sql<ColumnInfo>`
|
|
457
|
+
PRAGMA table_info(${sql.ref(t)})
|
|
458
|
+
`.execute(trx);
|
|
459
|
+
const columns = colResult.rows;
|
|
460
|
+
|
|
461
|
+
// ── 2. Read current indexes ─────────────────────────────────
|
|
462
|
+
const idxResult = await sql<IndexInfo>`
|
|
463
|
+
PRAGMA index_list(${sql.ref(t)})
|
|
464
|
+
`.execute(trx);
|
|
465
|
+
|
|
466
|
+
const indexDefs: { name: string; unique: boolean; columns: string[]; partial: number }[] = [];
|
|
467
|
+
for (const idx of idxResult.rows) {
|
|
468
|
+
if (idx.origin === "pk" || idx.name.startsWith("sqlite_autoindex_")) continue;
|
|
469
|
+
|
|
470
|
+
const idxColResult = await sql<IndexColumnInfo>`
|
|
471
|
+
PRAGMA index_info(${sql.ref(idx.name)})
|
|
472
|
+
`.execute(trx);
|
|
473
|
+
|
|
474
|
+
indexDefs.push({
|
|
475
|
+
name: idx.name,
|
|
476
|
+
unique: idx.unique === 1,
|
|
477
|
+
columns: idxColResult.rows.map((c) => c.name),
|
|
478
|
+
partial: idx.partial,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Save partial index SQL
|
|
483
|
+
const partialSqls = new Map<string, string>();
|
|
484
|
+
for (const idx of indexDefs) {
|
|
485
|
+
if (idx.partial) {
|
|
486
|
+
const createResult = await sql<{ sql: string }>`
|
|
487
|
+
SELECT sql FROM sqlite_master
|
|
488
|
+
WHERE type = 'index' AND name = ${idx.name}
|
|
489
|
+
`.execute(trx);
|
|
490
|
+
if (createResult.rows[0]?.sql) {
|
|
491
|
+
partialSqls.set(idx.name, createResult.rows[0].sql);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ── 3. Build column defs WITHOUT locale/translation_group ───
|
|
497
|
+
// Validate all column names
|
|
498
|
+
for (const col of columns) {
|
|
499
|
+
if (col.name === "locale" || col.name === "translation_group") continue;
|
|
500
|
+
validateIdentifier(col.name, "column name");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Restore slug's inline UNIQUE
|
|
504
|
+
const colDefs: string[] = [];
|
|
505
|
+
const colNames: string[] = [];
|
|
506
|
+
|
|
507
|
+
for (const col of columns) {
|
|
508
|
+
// Skip i18n columns
|
|
509
|
+
if (col.name === "locale" || col.name === "translation_group") continue;
|
|
510
|
+
|
|
511
|
+
validateColumnType(col.type || "TEXT", col.name);
|
|
512
|
+
colNames.push(quoteIdent(col.name));
|
|
513
|
+
let def = `${quoteIdent(col.name)} ${col.type || "TEXT"}`;
|
|
514
|
+
|
|
515
|
+
if (col.pk) {
|
|
516
|
+
def += " PRIMARY KEY";
|
|
517
|
+
} else if (col.name === "slug") {
|
|
518
|
+
// Restore inline UNIQUE
|
|
519
|
+
def += " UNIQUE";
|
|
520
|
+
} else {
|
|
521
|
+
if (col.notnull) def += " NOT NULL";
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (col.dflt_value !== null) {
|
|
525
|
+
validateDefaultValue(col.dflt_value, col.name);
|
|
526
|
+
def += ` DEFAULT ${normalizeDdlDefault(col.dflt_value)}`;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
colDefs.push(def);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const createColsSql = colDefs.join(",\n\t\t\t\t");
|
|
533
|
+
const selectColsSql = colNames.join(", ");
|
|
534
|
+
|
|
535
|
+
// ── 4. Rebuild the table ────────────────────────────────────
|
|
536
|
+
// Drop all existing indexes first
|
|
537
|
+
for (const idx of indexDefs) {
|
|
538
|
+
await sql`DROP INDEX IF EXISTS ${sql.ref(idx.name)}`.execute(trx);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Create table with original schema (slug UNIQUE, no i18n columns)
|
|
542
|
+
await sql
|
|
543
|
+
.raw(`CREATE TABLE ${quoteIdent(tmp)} (\n\t\t\t\t${createColsSql}\n\t\t\t)`)
|
|
544
|
+
.execute(trx);
|
|
545
|
+
|
|
546
|
+
// Copy data — keep only one row per content item.
|
|
547
|
+
// Prefer locale='en' rows. For items without an 'en' row, pick the
|
|
548
|
+
// row with the smallest locale code (deterministic, unlike bare GROUP BY).
|
|
549
|
+
// Handle NULL translation_group by treating each such row as its own group.
|
|
550
|
+
// INSERT OR IGNORE skips any duplicate slugs from the fallback pass.
|
|
551
|
+
await sql
|
|
552
|
+
.raw(
|
|
553
|
+
`INSERT OR IGNORE INTO ${quoteIdent(tmp)} (${selectColsSql})
|
|
554
|
+
SELECT ${selectColsSql} FROM ${quoteIdent(t)}
|
|
555
|
+
WHERE "locale" = 'en'`,
|
|
556
|
+
)
|
|
557
|
+
.execute(trx);
|
|
558
|
+
|
|
559
|
+
await sql
|
|
560
|
+
.raw(
|
|
561
|
+
`INSERT OR IGNORE INTO ${quoteIdent(tmp)} (${selectColsSql})
|
|
562
|
+
SELECT ${selectColsSql} FROM ${quoteIdent(t)}
|
|
563
|
+
WHERE "id" NOT IN (SELECT "id" FROM ${quoteIdent(tmp)})
|
|
564
|
+
AND "id" IN (
|
|
565
|
+
SELECT "id" FROM ${quoteIdent(t)} AS t2
|
|
566
|
+
WHERE t2."translation_group" IS NOT NULL
|
|
567
|
+
AND t2."locale" = (
|
|
568
|
+
SELECT MIN(t3."locale") FROM ${quoteIdent(t)} AS t3
|
|
569
|
+
WHERE t3."translation_group" = t2."translation_group"
|
|
570
|
+
)
|
|
571
|
+
)`,
|
|
572
|
+
)
|
|
573
|
+
.execute(trx);
|
|
574
|
+
|
|
575
|
+
// Pick up any rows with NULL translation_group that weren't already copied
|
|
576
|
+
await sql
|
|
577
|
+
.raw(
|
|
578
|
+
`INSERT OR IGNORE INTO ${quoteIdent(tmp)} (${selectColsSql})
|
|
579
|
+
SELECT ${selectColsSql} FROM ${quoteIdent(t)}
|
|
580
|
+
WHERE "id" NOT IN (SELECT "id" FROM ${quoteIdent(tmp)})
|
|
581
|
+
AND "translation_group" IS NULL`,
|
|
582
|
+
)
|
|
583
|
+
.execute(trx);
|
|
584
|
+
|
|
585
|
+
// Swap tables
|
|
586
|
+
await sql`DROP TABLE ${sql.ref(t)}`.execute(trx);
|
|
587
|
+
await sql.raw(`ALTER TABLE ${quoteIdent(tmp)} RENAME TO ${quoteIdent(t)}`).execute(trx);
|
|
588
|
+
|
|
589
|
+
// ── 5. Recreate indexes ─────────────────────────────────────
|
|
590
|
+
for (const idx of indexDefs) {
|
|
591
|
+
// Skip i18n-specific indexes — they don't exist in the old schema
|
|
592
|
+
if (idx.name === `idx_${t}_locale`) continue;
|
|
593
|
+
if (idx.name === `idx_${t}_translation_group`) continue;
|
|
594
|
+
|
|
595
|
+
if (idx.partial && partialSqls.has(idx.name)) {
|
|
596
|
+
// Partial indexes — validate the SQL before replaying
|
|
597
|
+
const idxSql = partialSqls.get(idx.name)!;
|
|
598
|
+
validateCreateIndexSql(idxSql, idx.name);
|
|
599
|
+
await sql.raw(idxSql).execute(trx);
|
|
600
|
+
} else {
|
|
601
|
+
// Filter out i18n columns from any index that might reference them
|
|
602
|
+
const cols = idx.columns.filter((c) => c !== "locale" && c !== "translation_group");
|
|
603
|
+
if (cols.length === 0) continue;
|
|
604
|
+
|
|
605
|
+
// Validate column names
|
|
606
|
+
for (const c of cols) {
|
|
607
|
+
validateIdentifier(c, "index column name");
|
|
608
|
+
}
|
|
609
|
+
const colsSql = cols.map((c) => quoteIdent(c)).join(", ");
|
|
610
|
+
const unique = idx.unique ? "UNIQUE " : "";
|
|
611
|
+
await sql
|
|
612
|
+
.raw(`CREATE ${unique}INDEX ${quoteIdent(idx.name)} ON ${quoteIdent(t)} (${colsSql})`)
|
|
613
|
+
.execute(trx);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
|
+
import { sql } from "kysely";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Migration: URL pattern for collections
|
|
6
|
+
*
|
|
7
|
+
* Adds `url_pattern` column to `_emdash_collections` so each collection
|
|
8
|
+
* can declare its own URL structure (e.g. "/{slug}" for pages, "/blog/{slug}"
|
|
9
|
+
* for posts). Used for menu URL resolution, sitemaps, and path-based lookups.
|
|
10
|
+
*/
|
|
11
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
12
|
+
await sql`
|
|
13
|
+
ALTER TABLE _emdash_collections
|
|
14
|
+
ADD COLUMN url_pattern TEXT
|
|
15
|
+
`.execute(db);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
19
|
+
await sql`
|
|
20
|
+
ALTER TABLE _emdash_collections
|
|
21
|
+
DROP COLUMN url_pattern
|
|
22
|
+
`.execute(db);
|
|
23
|
+
}
|