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,1141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed engine - applies seed files to database
|
|
3
|
+
*
|
|
4
|
+
* This is the core implementation that bootstraps an EmDash site from a seed file.
|
|
5
|
+
* Apply order is critical for foreign keys and references.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { imageSize } from "image-size";
|
|
9
|
+
import type { Kysely } from "kysely";
|
|
10
|
+
import mime from "mime/lite";
|
|
11
|
+
import { ulid } from "ulidx";
|
|
12
|
+
|
|
13
|
+
import { BylineRepository } from "../database/repositories/byline.js";
|
|
14
|
+
import { ContentRepository } from "../database/repositories/content.js";
|
|
15
|
+
import { MediaRepository } from "../database/repositories/media.js";
|
|
16
|
+
import { RedirectRepository } from "../database/repositories/redirect.js";
|
|
17
|
+
import { TaxonomyRepository } from "../database/repositories/taxonomy.js";
|
|
18
|
+
import type { Database } from "../database/types.js";
|
|
19
|
+
import type { MediaValue } from "../fields/types.js";
|
|
20
|
+
import { ssrfSafeFetch, validateExternalUrl } from "../import/ssrf.js";
|
|
21
|
+
import { SchemaRegistry } from "../schema/registry.js";
|
|
22
|
+
import { FTSManager } from "../search/fts-manager.js";
|
|
23
|
+
import { setSiteSettings } from "../settings/index.js";
|
|
24
|
+
import type { Storage } from "../storage/types.js";
|
|
25
|
+
import type {
|
|
26
|
+
SeedFile,
|
|
27
|
+
SeedApplyOptions,
|
|
28
|
+
SeedApplyResult,
|
|
29
|
+
SeedTaxonomyTerm,
|
|
30
|
+
SeedMenuItem,
|
|
31
|
+
SeedWidget,
|
|
32
|
+
SeedMediaReference,
|
|
33
|
+
} from "./types.js";
|
|
34
|
+
|
|
35
|
+
const FILE_EXTENSION_PATTERN = /\.([a-z0-9]+)(?:\?|$)/i;
|
|
36
|
+
import { validateSeed } from "./validate.js";
|
|
37
|
+
|
|
38
|
+
/** Pattern to remove file extensions */
|
|
39
|
+
const EXTENSION_PATTERN = /\.[^.]+$/;
|
|
40
|
+
|
|
41
|
+
/** Pattern to remove query parameters */
|
|
42
|
+
const QUERY_PARAM_PATTERN = /\?.*$/;
|
|
43
|
+
|
|
44
|
+
/** Pattern to remove non-alphanumeric characters (except dash and underscore) */
|
|
45
|
+
const SANITIZE_PATTERN = /[^a-zA-Z0-9_-]/g;
|
|
46
|
+
|
|
47
|
+
/** Pattern to collapse multiple hyphens */
|
|
48
|
+
const MULTIPLE_HYPHENS_PATTERN = /-+/g;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Apply a seed file to the database
|
|
52
|
+
*
|
|
53
|
+
* This function is idempotent - safe to run multiple times.
|
|
54
|
+
*
|
|
55
|
+
* @param db - Kysely database instance
|
|
56
|
+
* @param seed - Seed file to apply
|
|
57
|
+
* @param options - Application options
|
|
58
|
+
* @returns Result summary
|
|
59
|
+
*/
|
|
60
|
+
export async function applySeed(
|
|
61
|
+
db: Kysely<Database>,
|
|
62
|
+
seed: SeedFile,
|
|
63
|
+
options: SeedApplyOptions = {},
|
|
64
|
+
): Promise<SeedApplyResult> {
|
|
65
|
+
// Validate seed first
|
|
66
|
+
const validation = validateSeed(seed);
|
|
67
|
+
if (!validation.valid) {
|
|
68
|
+
throw new Error(`Invalid seed file:\n${validation.errors.join("\n")}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const {
|
|
72
|
+
includeContent = false,
|
|
73
|
+
storage,
|
|
74
|
+
skipMediaDownload = false,
|
|
75
|
+
onConflict = "skip",
|
|
76
|
+
} = options;
|
|
77
|
+
|
|
78
|
+
// Result counters
|
|
79
|
+
const result: SeedApplyResult = {
|
|
80
|
+
collections: { created: 0, skipped: 0, updated: 0 },
|
|
81
|
+
fields: { created: 0, skipped: 0, updated: 0 },
|
|
82
|
+
taxonomies: { created: 0, terms: 0 },
|
|
83
|
+
bylines: { created: 0, skipped: 0, updated: 0 },
|
|
84
|
+
menus: { created: 0, items: 0 },
|
|
85
|
+
redirects: { created: 0, skipped: 0, updated: 0 },
|
|
86
|
+
widgetAreas: { created: 0, widgets: 0 },
|
|
87
|
+
sections: { created: 0, skipped: 0, updated: 0 },
|
|
88
|
+
settings: { applied: 0 },
|
|
89
|
+
content: { created: 0, skipped: 0, updated: 0 },
|
|
90
|
+
media: { created: 0, skipped: 0 },
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Media context for $media resolution
|
|
94
|
+
const mediaContext: MediaContext = {
|
|
95
|
+
db,
|
|
96
|
+
storage: storage ?? null,
|
|
97
|
+
skipMediaDownload,
|
|
98
|
+
mediaCache: new Map(), // Cache downloaded media by URL to avoid re-downloading
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Apply order (critical for foreign keys and references):
|
|
102
|
+
// 1. Site settings
|
|
103
|
+
// 2. Collections + Fields
|
|
104
|
+
// 3. Taxonomy definitions + Terms
|
|
105
|
+
// 4. Content (so menu refs can resolve)
|
|
106
|
+
// 5. Menus + Menu items (can now resolve content refs)
|
|
107
|
+
// 6. Redirects
|
|
108
|
+
// 7. Widget areas + Widgets
|
|
109
|
+
|
|
110
|
+
// Track seed content IDs for reference resolution (shared across content and menus)
|
|
111
|
+
const seedIdMap = new Map<string, string>(); // seed id -> real entry id
|
|
112
|
+
const seedBylineIdMap = new Map<string, string>(); // seed byline id -> real byline id
|
|
113
|
+
|
|
114
|
+
// 1. Site settings
|
|
115
|
+
if (seed.settings) {
|
|
116
|
+
await setSiteSettings(seed.settings, db);
|
|
117
|
+
result.settings.applied = Object.keys(seed.settings).length;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 2-3. Collections and Fields
|
|
121
|
+
if (seed.collections) {
|
|
122
|
+
const registry = new SchemaRegistry(db);
|
|
123
|
+
|
|
124
|
+
for (const collection of seed.collections) {
|
|
125
|
+
// Check if collection exists
|
|
126
|
+
const existing = await registry.getCollection(collection.slug);
|
|
127
|
+
|
|
128
|
+
if (existing) {
|
|
129
|
+
if (onConflict === "error") {
|
|
130
|
+
throw new Error(`Conflict: collection "${collection.slug}" already exists`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (onConflict === "update") {
|
|
134
|
+
await registry.updateCollection(collection.slug, {
|
|
135
|
+
label: collection.label,
|
|
136
|
+
labelSingular: collection.labelSingular,
|
|
137
|
+
description: collection.description,
|
|
138
|
+
icon: collection.icon,
|
|
139
|
+
supports: collection.supports || [],
|
|
140
|
+
urlPattern: collection.urlPattern,
|
|
141
|
+
commentsEnabled: collection.commentsEnabled,
|
|
142
|
+
});
|
|
143
|
+
result.collections.updated++;
|
|
144
|
+
|
|
145
|
+
// Update or create fields
|
|
146
|
+
for (const field of collection.fields) {
|
|
147
|
+
const existingField = await registry.getField(collection.slug, field.slug);
|
|
148
|
+
if (existingField) {
|
|
149
|
+
await registry.updateField(collection.slug, field.slug, {
|
|
150
|
+
label: field.label,
|
|
151
|
+
required: field.required || false,
|
|
152
|
+
unique: field.unique || false,
|
|
153
|
+
searchable: field.searchable || false,
|
|
154
|
+
defaultValue: field.defaultValue,
|
|
155
|
+
validation: field.validation,
|
|
156
|
+
widget: field.widget,
|
|
157
|
+
options: field.options,
|
|
158
|
+
});
|
|
159
|
+
result.fields.updated++;
|
|
160
|
+
} else {
|
|
161
|
+
await registry.createField(collection.slug, {
|
|
162
|
+
slug: field.slug,
|
|
163
|
+
label: field.label,
|
|
164
|
+
type: field.type,
|
|
165
|
+
required: field.required || false,
|
|
166
|
+
unique: field.unique || false,
|
|
167
|
+
searchable: field.searchable || false,
|
|
168
|
+
defaultValue: field.defaultValue,
|
|
169
|
+
validation: field.validation,
|
|
170
|
+
widget: field.widget,
|
|
171
|
+
options: field.options,
|
|
172
|
+
});
|
|
173
|
+
result.fields.created++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// skip
|
|
180
|
+
result.collections.skipped++;
|
|
181
|
+
result.fields.skipped += collection.fields.length;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Create collection
|
|
186
|
+
await registry.createCollection({
|
|
187
|
+
slug: collection.slug,
|
|
188
|
+
label: collection.label,
|
|
189
|
+
labelSingular: collection.labelSingular,
|
|
190
|
+
description: collection.description,
|
|
191
|
+
icon: collection.icon,
|
|
192
|
+
supports: collection.supports || [],
|
|
193
|
+
source: "seed",
|
|
194
|
+
urlPattern: collection.urlPattern,
|
|
195
|
+
commentsEnabled: collection.commentsEnabled,
|
|
196
|
+
});
|
|
197
|
+
result.collections.created++;
|
|
198
|
+
|
|
199
|
+
// Create fields
|
|
200
|
+
for (const field of collection.fields) {
|
|
201
|
+
await registry.createField(collection.slug, {
|
|
202
|
+
slug: field.slug,
|
|
203
|
+
label: field.label,
|
|
204
|
+
type: field.type,
|
|
205
|
+
required: field.required || false,
|
|
206
|
+
unique: field.unique || false,
|
|
207
|
+
searchable: field.searchable || false,
|
|
208
|
+
defaultValue: field.defaultValue,
|
|
209
|
+
validation: field.validation,
|
|
210
|
+
widget: field.widget,
|
|
211
|
+
options: field.options,
|
|
212
|
+
});
|
|
213
|
+
result.fields.created++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 4-5. Taxonomies
|
|
219
|
+
if (seed.taxonomies) {
|
|
220
|
+
for (const taxonomy of seed.taxonomies) {
|
|
221
|
+
// Check if taxonomy definition exists
|
|
222
|
+
const existingDef = await db
|
|
223
|
+
.selectFrom("_emdash_taxonomy_defs")
|
|
224
|
+
.selectAll()
|
|
225
|
+
.where("name", "=", taxonomy.name)
|
|
226
|
+
.executeTakeFirst();
|
|
227
|
+
|
|
228
|
+
if (existingDef) {
|
|
229
|
+
if (onConflict === "error") {
|
|
230
|
+
throw new Error(`Conflict: taxonomy "${taxonomy.name}" already exists`);
|
|
231
|
+
}
|
|
232
|
+
if (onConflict === "update") {
|
|
233
|
+
await db
|
|
234
|
+
.updateTable("_emdash_taxonomy_defs")
|
|
235
|
+
.set({
|
|
236
|
+
label: taxonomy.label,
|
|
237
|
+
label_singular: taxonomy.labelSingular ?? null,
|
|
238
|
+
hierarchical: taxonomy.hierarchical ? 1 : 0,
|
|
239
|
+
collections: JSON.stringify(taxonomy.collections),
|
|
240
|
+
})
|
|
241
|
+
.where("id", "=", existingDef.id)
|
|
242
|
+
.execute();
|
|
243
|
+
// Taxonomy defs don't track an "updated" counter -- just the definition is updated
|
|
244
|
+
}
|
|
245
|
+
// skip: do nothing for the definition
|
|
246
|
+
} else {
|
|
247
|
+
// Create taxonomy definition
|
|
248
|
+
await db
|
|
249
|
+
.insertInto("_emdash_taxonomy_defs")
|
|
250
|
+
.values({
|
|
251
|
+
id: ulid(),
|
|
252
|
+
name: taxonomy.name,
|
|
253
|
+
label: taxonomy.label,
|
|
254
|
+
label_singular: taxonomy.labelSingular ?? null,
|
|
255
|
+
hierarchical: taxonomy.hierarchical ? 1 : 0,
|
|
256
|
+
collections: JSON.stringify(taxonomy.collections),
|
|
257
|
+
})
|
|
258
|
+
.execute();
|
|
259
|
+
result.taxonomies.created++;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Create terms (if provided)
|
|
263
|
+
if (taxonomy.terms && taxonomy.terms.length > 0) {
|
|
264
|
+
const termRepo = new TaxonomyRepository(db);
|
|
265
|
+
|
|
266
|
+
// For hierarchical taxonomies, we need to create parents before children
|
|
267
|
+
if (taxonomy.hierarchical) {
|
|
268
|
+
await applyHierarchicalTerms(termRepo, taxonomy.name, taxonomy.terms, result, onConflict);
|
|
269
|
+
} else {
|
|
270
|
+
// Flat taxonomy - create all terms
|
|
271
|
+
for (const term of taxonomy.terms) {
|
|
272
|
+
const existing = await termRepo.findBySlug(taxonomy.name, term.slug);
|
|
273
|
+
if (existing) {
|
|
274
|
+
if (onConflict === "error") {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Conflict: taxonomy term "${term.slug}" in "${taxonomy.name}" already exists`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
if (onConflict === "update") {
|
|
280
|
+
await termRepo.update(existing.id, {
|
|
281
|
+
label: term.label,
|
|
282
|
+
data: term.description ? { description: term.description } : {},
|
|
283
|
+
});
|
|
284
|
+
result.taxonomies.terms++;
|
|
285
|
+
}
|
|
286
|
+
// skip: do nothing
|
|
287
|
+
} else {
|
|
288
|
+
await termRepo.create({
|
|
289
|
+
name: taxonomy.name,
|
|
290
|
+
slug: term.slug,
|
|
291
|
+
label: term.label,
|
|
292
|
+
data: term.description ? { description: term.description } : undefined,
|
|
293
|
+
});
|
|
294
|
+
result.taxonomies.terms++;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 6. Bylines
|
|
303
|
+
if (seed.bylines) {
|
|
304
|
+
const bylineRepo = new BylineRepository(db);
|
|
305
|
+
for (const byline of seed.bylines) {
|
|
306
|
+
const existing = await bylineRepo.findBySlug(byline.slug);
|
|
307
|
+
if (existing) {
|
|
308
|
+
if (onConflict === "error") {
|
|
309
|
+
throw new Error(`Conflict: byline "${byline.slug}" already exists`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (onConflict === "update") {
|
|
313
|
+
await bylineRepo.update(existing.id, {
|
|
314
|
+
displayName: byline.displayName,
|
|
315
|
+
bio: byline.bio ?? null,
|
|
316
|
+
websiteUrl: byline.websiteUrl ?? null,
|
|
317
|
+
isGuest: byline.isGuest,
|
|
318
|
+
});
|
|
319
|
+
seedBylineIdMap.set(byline.id, existing.id);
|
|
320
|
+
result.bylines.updated++;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// skip
|
|
325
|
+
seedBylineIdMap.set(byline.id, existing.id);
|
|
326
|
+
result.bylines.skipped++;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const created = await bylineRepo.create({
|
|
331
|
+
slug: byline.slug,
|
|
332
|
+
displayName: byline.displayName,
|
|
333
|
+
bio: byline.bio ?? null,
|
|
334
|
+
websiteUrl: byline.websiteUrl ?? null,
|
|
335
|
+
isGuest: byline.isGuest,
|
|
336
|
+
});
|
|
337
|
+
seedBylineIdMap.set(byline.id, created.id);
|
|
338
|
+
result.bylines.created++;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 7. Content (created before menus so refs can resolve)
|
|
343
|
+
if (includeContent && seed.content) {
|
|
344
|
+
const contentRepo = new ContentRepository(db);
|
|
345
|
+
const bylineRepo = new BylineRepository(db);
|
|
346
|
+
|
|
347
|
+
// Create content entries
|
|
348
|
+
for (const [collectionSlug, entries] of Object.entries(seed.content)) {
|
|
349
|
+
for (const entry of entries) {
|
|
350
|
+
// Check if entry exists (by slug + locale for locale-aware lookup)
|
|
351
|
+
const existing = await contentRepo.findBySlug(collectionSlug, entry.slug, entry.locale);
|
|
352
|
+
|
|
353
|
+
if (existing) {
|
|
354
|
+
if (onConflict === "error") {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`Conflict: content "${entry.slug}" in "${collectionSlug}" already exists`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (onConflict === "update") {
|
|
361
|
+
// Resolve $ref and $media in data
|
|
362
|
+
const resolvedData = await resolveReferences(
|
|
363
|
+
entry.data,
|
|
364
|
+
seedIdMap,
|
|
365
|
+
mediaContext,
|
|
366
|
+
result,
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const status = entry.status || "published";
|
|
370
|
+
await contentRepo.update(collectionSlug, existing.id, {
|
|
371
|
+
status,
|
|
372
|
+
data: resolvedData,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
seedIdMap.set(entry.id, existing.id);
|
|
376
|
+
result.content.updated++;
|
|
377
|
+
|
|
378
|
+
// Update bylines and taxonomy assignments
|
|
379
|
+
await applyContentBylines(
|
|
380
|
+
bylineRepo,
|
|
381
|
+
collectionSlug,
|
|
382
|
+
existing.id,
|
|
383
|
+
entry,
|
|
384
|
+
seedBylineIdMap,
|
|
385
|
+
true,
|
|
386
|
+
);
|
|
387
|
+
await applyContentTaxonomies(db, collectionSlug, existing.id, entry, true);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// skip
|
|
392
|
+
result.content.skipped++;
|
|
393
|
+
seedIdMap.set(entry.id, existing.id);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Resolve $ref and $media in data
|
|
398
|
+
const resolvedData = await resolveReferences(entry.data, seedIdMap, mediaContext, result);
|
|
399
|
+
|
|
400
|
+
// Resolve translationOf: map from seed-local ID to real EmDash ID
|
|
401
|
+
let translationOf: string | undefined;
|
|
402
|
+
if (entry.translationOf) {
|
|
403
|
+
const sourceId = seedIdMap.get(entry.translationOf);
|
|
404
|
+
if (!sourceId) {
|
|
405
|
+
console.warn(
|
|
406
|
+
`content.${collectionSlug}: translationOf "${entry.translationOf}" not found (not yet created or missing). Skipping translation link.`,
|
|
407
|
+
);
|
|
408
|
+
} else {
|
|
409
|
+
translationOf = sourceId;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Create entry
|
|
414
|
+
const status = entry.status || "published";
|
|
415
|
+
const created = await contentRepo.create({
|
|
416
|
+
type: collectionSlug,
|
|
417
|
+
slug: entry.slug,
|
|
418
|
+
status,
|
|
419
|
+
data: resolvedData,
|
|
420
|
+
locale: entry.locale,
|
|
421
|
+
translationOf,
|
|
422
|
+
// Set published_at for published content so RSS/Archives work correctly
|
|
423
|
+
publishedAt: status === "published" ? new Date().toISOString() : null,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
seedIdMap.set(entry.id, created.id);
|
|
427
|
+
result.content.created++;
|
|
428
|
+
|
|
429
|
+
await applyContentBylines(bylineRepo, collectionSlug, created.id, entry, seedBylineIdMap);
|
|
430
|
+
await applyContentTaxonomies(db, collectionSlug, created.id, entry, false);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// 8. Menus and Menu Items (after content so refs can resolve)
|
|
436
|
+
if (seed.menus) {
|
|
437
|
+
for (const menu of seed.menus) {
|
|
438
|
+
// Check if menu exists
|
|
439
|
+
const existingMenu = await db
|
|
440
|
+
.selectFrom("_emdash_menus")
|
|
441
|
+
.selectAll()
|
|
442
|
+
.where("name", "=", menu.name)
|
|
443
|
+
.executeTakeFirst();
|
|
444
|
+
|
|
445
|
+
let menuId: string;
|
|
446
|
+
|
|
447
|
+
if (existingMenu) {
|
|
448
|
+
menuId = existingMenu.id;
|
|
449
|
+
// Clear existing items (menus are recreated)
|
|
450
|
+
await db.deleteFrom("_emdash_menu_items").where("menu_id", "=", menuId).execute();
|
|
451
|
+
} else {
|
|
452
|
+
// Create menu
|
|
453
|
+
menuId = ulid();
|
|
454
|
+
await db
|
|
455
|
+
.insertInto("_emdash_menus")
|
|
456
|
+
.values({
|
|
457
|
+
id: menuId,
|
|
458
|
+
name: menu.name,
|
|
459
|
+
label: menu.label,
|
|
460
|
+
created_at: new Date().toISOString(),
|
|
461
|
+
updated_at: new Date().toISOString(),
|
|
462
|
+
})
|
|
463
|
+
.execute();
|
|
464
|
+
result.menus.created++;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Create menu items
|
|
468
|
+
const itemCount = await applyMenuItems(
|
|
469
|
+
db,
|
|
470
|
+
menuId,
|
|
471
|
+
menu.items,
|
|
472
|
+
null, // parent_id
|
|
473
|
+
0, // sort_order
|
|
474
|
+
seedIdMap,
|
|
475
|
+
);
|
|
476
|
+
result.menus.items += itemCount;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// 9. Redirects
|
|
481
|
+
if (seed.redirects) {
|
|
482
|
+
const redirectRepo = new RedirectRepository(db);
|
|
483
|
+
|
|
484
|
+
for (const redirect of seed.redirects) {
|
|
485
|
+
const existing = await redirectRepo.findBySource(redirect.source);
|
|
486
|
+
if (existing) {
|
|
487
|
+
if (onConflict === "error") {
|
|
488
|
+
throw new Error(`Conflict: redirect "${redirect.source}" already exists`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (onConflict === "update") {
|
|
492
|
+
await redirectRepo.update(existing.id, {
|
|
493
|
+
destination: redirect.destination,
|
|
494
|
+
type: redirect.type,
|
|
495
|
+
enabled: redirect.enabled,
|
|
496
|
+
groupName: redirect.groupName,
|
|
497
|
+
});
|
|
498
|
+
result.redirects.updated++;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// skip
|
|
503
|
+
result.redirects.skipped++;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
await redirectRepo.create({
|
|
508
|
+
source: redirect.source,
|
|
509
|
+
destination: redirect.destination,
|
|
510
|
+
type: redirect.type,
|
|
511
|
+
enabled: redirect.enabled,
|
|
512
|
+
groupName: redirect.groupName,
|
|
513
|
+
});
|
|
514
|
+
result.redirects.created++;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// 10. Widget Areas and Widgets
|
|
519
|
+
if (seed.widgetAreas) {
|
|
520
|
+
for (const area of seed.widgetAreas) {
|
|
521
|
+
// Check if area exists
|
|
522
|
+
const existingArea = await db
|
|
523
|
+
.selectFrom("_emdash_widget_areas")
|
|
524
|
+
.selectAll()
|
|
525
|
+
.where("name", "=", area.name)
|
|
526
|
+
.executeTakeFirst();
|
|
527
|
+
|
|
528
|
+
let areaId: string;
|
|
529
|
+
|
|
530
|
+
if (existingArea) {
|
|
531
|
+
areaId = existingArea.id;
|
|
532
|
+
// Clear existing widgets (areas are recreated)
|
|
533
|
+
await db.deleteFrom("_emdash_widgets").where("area_id", "=", areaId).execute();
|
|
534
|
+
} else {
|
|
535
|
+
// Create area
|
|
536
|
+
areaId = ulid();
|
|
537
|
+
await db
|
|
538
|
+
.insertInto("_emdash_widget_areas")
|
|
539
|
+
.values({
|
|
540
|
+
id: areaId,
|
|
541
|
+
name: area.name,
|
|
542
|
+
label: area.label,
|
|
543
|
+
description: area.description ?? null,
|
|
544
|
+
})
|
|
545
|
+
.execute();
|
|
546
|
+
result.widgetAreas.created++;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Create widgets
|
|
550
|
+
for (let i = 0; i < area.widgets.length; i++) {
|
|
551
|
+
const widget = area.widgets[i];
|
|
552
|
+
await applyWidget(db, areaId, widget, i);
|
|
553
|
+
result.widgetAreas.widgets++;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// 11. Sections
|
|
559
|
+
if (seed.sections) {
|
|
560
|
+
for (const section of seed.sections) {
|
|
561
|
+
// Check if section exists
|
|
562
|
+
const existing = await db
|
|
563
|
+
.selectFrom("_emdash_sections")
|
|
564
|
+
.select("id")
|
|
565
|
+
.where("slug", "=", section.slug)
|
|
566
|
+
.executeTakeFirst();
|
|
567
|
+
|
|
568
|
+
if (existing) {
|
|
569
|
+
if (onConflict === "error") {
|
|
570
|
+
throw new Error(`Conflict: section "${section.slug}" already exists`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (onConflict === "update") {
|
|
574
|
+
await db
|
|
575
|
+
.updateTable("_emdash_sections")
|
|
576
|
+
.set({
|
|
577
|
+
title: section.title,
|
|
578
|
+
description: section.description ?? null,
|
|
579
|
+
keywords: section.keywords ? JSON.stringify(section.keywords) : null,
|
|
580
|
+
content: JSON.stringify(section.content),
|
|
581
|
+
source: section.source || "theme",
|
|
582
|
+
updated_at: new Date().toISOString(),
|
|
583
|
+
})
|
|
584
|
+
.where("id", "=", existing.id)
|
|
585
|
+
.execute();
|
|
586
|
+
result.sections.updated++;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// skip
|
|
591
|
+
result.sections.skipped++;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const id = ulid();
|
|
596
|
+
const now = new Date().toISOString();
|
|
597
|
+
|
|
598
|
+
await db
|
|
599
|
+
.insertInto("_emdash_sections")
|
|
600
|
+
.values({
|
|
601
|
+
id,
|
|
602
|
+
slug: section.slug,
|
|
603
|
+
title: section.title,
|
|
604
|
+
description: section.description ?? null,
|
|
605
|
+
keywords: section.keywords ? JSON.stringify(section.keywords) : null,
|
|
606
|
+
content: JSON.stringify(section.content),
|
|
607
|
+
preview_media_id: null,
|
|
608
|
+
source: section.source || "theme",
|
|
609
|
+
theme_id: section.source === "theme" ? section.slug : null,
|
|
610
|
+
created_at: now,
|
|
611
|
+
updated_at: now,
|
|
612
|
+
})
|
|
613
|
+
.execute();
|
|
614
|
+
|
|
615
|
+
result.sections.created++;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// 11. Enable search for collections that have `search` in supports
|
|
620
|
+
if (seed.collections) {
|
|
621
|
+
const ftsManager = new FTSManager(db);
|
|
622
|
+
|
|
623
|
+
for (const collection of seed.collections) {
|
|
624
|
+
if (collection.supports?.includes("search")) {
|
|
625
|
+
// Check if there are searchable fields
|
|
626
|
+
const searchableFields = await ftsManager.getSearchableFields(collection.slug);
|
|
627
|
+
if (searchableFields.length > 0) {
|
|
628
|
+
try {
|
|
629
|
+
await ftsManager.enableSearch(collection.slug);
|
|
630
|
+
} catch (err) {
|
|
631
|
+
// Log but don't fail - search can be enabled manually later
|
|
632
|
+
console.warn(`Failed to enable search for ${collection.slug}:`, err);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return result;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Apply hierarchical taxonomy terms (parents before children)
|
|
644
|
+
*/
|
|
645
|
+
async function applyHierarchicalTerms(
|
|
646
|
+
termRepo: TaxonomyRepository,
|
|
647
|
+
taxonomyName: string,
|
|
648
|
+
terms: SeedTaxonomyTerm[],
|
|
649
|
+
result: SeedApplyResult,
|
|
650
|
+
onConflict: "skip" | "update" | "error" = "skip",
|
|
651
|
+
): Promise<void> {
|
|
652
|
+
// Map slugs to IDs
|
|
653
|
+
const slugToId = new Map<string, string>();
|
|
654
|
+
|
|
655
|
+
// Multiple passes to handle deep nesting
|
|
656
|
+
let remaining = [...terms];
|
|
657
|
+
let maxPasses = 10; // Prevent infinite loop
|
|
658
|
+
|
|
659
|
+
while (remaining.length > 0 && maxPasses > 0) {
|
|
660
|
+
const processedThisPass: string[] = [];
|
|
661
|
+
|
|
662
|
+
for (const term of remaining) {
|
|
663
|
+
// Check if parent exists (or no parent)
|
|
664
|
+
if (!term.parent || slugToId.has(term.parent)) {
|
|
665
|
+
const parentId = term.parent ? slugToId.get(term.parent) : undefined;
|
|
666
|
+
|
|
667
|
+
const existing = await termRepo.findBySlug(taxonomyName, term.slug);
|
|
668
|
+
if (existing) {
|
|
669
|
+
if (onConflict === "error") {
|
|
670
|
+
throw new Error(
|
|
671
|
+
`Conflict: taxonomy term "${term.slug}" in "${taxonomyName}" already exists`,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
if (onConflict === "update") {
|
|
675
|
+
await termRepo.update(existing.id, {
|
|
676
|
+
label: term.label,
|
|
677
|
+
parentId,
|
|
678
|
+
data: term.description ? { description: term.description } : {},
|
|
679
|
+
});
|
|
680
|
+
result.taxonomies.terms++;
|
|
681
|
+
}
|
|
682
|
+
slugToId.set(term.slug, existing.id);
|
|
683
|
+
} else {
|
|
684
|
+
const created = await termRepo.create({
|
|
685
|
+
name: taxonomyName,
|
|
686
|
+
slug: term.slug,
|
|
687
|
+
label: term.label,
|
|
688
|
+
parentId,
|
|
689
|
+
data: term.description ? { description: term.description } : undefined,
|
|
690
|
+
});
|
|
691
|
+
slugToId.set(term.slug, created.id);
|
|
692
|
+
result.taxonomies.terms++;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
processedThisPass.push(term.slug);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Remove processed terms
|
|
700
|
+
remaining = remaining.filter((t) => !processedThisPass.includes(t.slug));
|
|
701
|
+
maxPasses--;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (remaining.length > 0) {
|
|
705
|
+
console.warn(`Could not process ${remaining.length} terms due to missing parents`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Apply byline credits to a content entry.
|
|
711
|
+
* In update mode, clears existing credits even if the seed has none.
|
|
712
|
+
*/
|
|
713
|
+
async function applyContentBylines(
|
|
714
|
+
bylineRepo: BylineRepository,
|
|
715
|
+
collectionSlug: string,
|
|
716
|
+
contentId: string,
|
|
717
|
+
entry: { slug: string; bylines?: Array<{ byline: string; roleLabel?: string }> },
|
|
718
|
+
seedBylineIdMap: Map<string, string>,
|
|
719
|
+
isUpdate = false,
|
|
720
|
+
): Promise<void> {
|
|
721
|
+
if (!entry.bylines || entry.bylines.length === 0) {
|
|
722
|
+
// In update mode, clear existing bylines when the seed entry has none
|
|
723
|
+
if (isUpdate) {
|
|
724
|
+
await bylineRepo.setContentBylines(collectionSlug, contentId, []);
|
|
725
|
+
}
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const credits = entry.bylines
|
|
730
|
+
.map((credit) => {
|
|
731
|
+
const bylineId = seedBylineIdMap.get(credit.byline);
|
|
732
|
+
if (!bylineId) return null;
|
|
733
|
+
return {
|
|
734
|
+
bylineId,
|
|
735
|
+
roleLabel: credit.roleLabel ?? null,
|
|
736
|
+
};
|
|
737
|
+
})
|
|
738
|
+
.filter((credit): credit is { bylineId: string; roleLabel: string | null } => Boolean(credit));
|
|
739
|
+
|
|
740
|
+
if (credits.length !== entry.bylines.length) {
|
|
741
|
+
console.warn(
|
|
742
|
+
`content.${collectionSlug}.${entry.slug}: one or more byline refs could not be resolved`,
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// In update mode, always call setContentBylines (even with empty credits)
|
|
747
|
+
// to clear stale assignments when all byline refs fail to resolve.
|
|
748
|
+
// In create mode, only call if there are credits to assign.
|
|
749
|
+
if (credits.length > 0 || isUpdate) {
|
|
750
|
+
await bylineRepo.setContentBylines(collectionSlug, contentId, credits);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Apply taxonomy term assignments to a content entry.
|
|
756
|
+
* In update mode, clears existing assignments before re-attaching.
|
|
757
|
+
*/
|
|
758
|
+
async function applyContentTaxonomies(
|
|
759
|
+
db: Kysely<Database>,
|
|
760
|
+
collectionSlug: string,
|
|
761
|
+
contentId: string,
|
|
762
|
+
entry: { taxonomies?: Record<string, string[]> },
|
|
763
|
+
isUpdate: boolean,
|
|
764
|
+
): Promise<void> {
|
|
765
|
+
// In update mode, clear existing taxonomy assignments first
|
|
766
|
+
if (isUpdate) {
|
|
767
|
+
await db
|
|
768
|
+
.deleteFrom("content_taxonomies")
|
|
769
|
+
.where("collection", "=", collectionSlug)
|
|
770
|
+
.where("entry_id", "=", contentId)
|
|
771
|
+
.execute();
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (!entry.taxonomies) return;
|
|
775
|
+
|
|
776
|
+
for (const [taxonomyName, termSlugs] of Object.entries(entry.taxonomies)) {
|
|
777
|
+
const termRepo = new TaxonomyRepository(db);
|
|
778
|
+
|
|
779
|
+
for (const termSlug of termSlugs) {
|
|
780
|
+
const term = await termRepo.findBySlug(taxonomyName, termSlug);
|
|
781
|
+
if (term) {
|
|
782
|
+
await termRepo.attachToEntry(collectionSlug, contentId, term.id);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Apply menu items recursively
|
|
790
|
+
*/
|
|
791
|
+
async function applyMenuItems(
|
|
792
|
+
db: Kysely<Database>,
|
|
793
|
+
menuId: string,
|
|
794
|
+
items: SeedMenuItem[],
|
|
795
|
+
parentId: string | null,
|
|
796
|
+
startOrder: number,
|
|
797
|
+
seedIdMap: Map<string, string>,
|
|
798
|
+
): Promise<number> {
|
|
799
|
+
let count = 0;
|
|
800
|
+
let order = startOrder;
|
|
801
|
+
|
|
802
|
+
for (const item of items) {
|
|
803
|
+
const itemId = ulid();
|
|
804
|
+
|
|
805
|
+
// Resolve reference if needed
|
|
806
|
+
let referenceId: string | null = null;
|
|
807
|
+
let referenceCollection: string | null = null;
|
|
808
|
+
|
|
809
|
+
if (item.type === "page" || item.type === "post") {
|
|
810
|
+
// Try to resolve from seedIdMap
|
|
811
|
+
if (item.ref && seedIdMap.has(item.ref)) {
|
|
812
|
+
referenceId = seedIdMap.get(item.ref)!;
|
|
813
|
+
// Default to plural collection name (pages/posts) if not specified
|
|
814
|
+
referenceCollection = item.collection || `${item.type}s`;
|
|
815
|
+
}
|
|
816
|
+
// If not in map, the content might not exist yet (will be broken link)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Insert menu item
|
|
820
|
+
await db
|
|
821
|
+
.insertInto("_emdash_menu_items")
|
|
822
|
+
.values({
|
|
823
|
+
id: itemId,
|
|
824
|
+
menu_id: menuId,
|
|
825
|
+
parent_id: parentId,
|
|
826
|
+
sort_order: order,
|
|
827
|
+
type: item.type,
|
|
828
|
+
reference_collection: referenceCollection,
|
|
829
|
+
reference_id: referenceId,
|
|
830
|
+
custom_url: item.url ?? null,
|
|
831
|
+
label: item.label || "",
|
|
832
|
+
title_attr: item.titleAttr ?? null,
|
|
833
|
+
target: item.target ?? null,
|
|
834
|
+
css_classes: item.cssClasses ?? null,
|
|
835
|
+
created_at: new Date().toISOString(),
|
|
836
|
+
})
|
|
837
|
+
.execute();
|
|
838
|
+
|
|
839
|
+
count++;
|
|
840
|
+
order++;
|
|
841
|
+
|
|
842
|
+
// Process children
|
|
843
|
+
if (item.children && item.children.length > 0) {
|
|
844
|
+
const childCount = await applyMenuItems(db, menuId, item.children, itemId, 0, seedIdMap);
|
|
845
|
+
count += childCount;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return count;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Apply a widget
|
|
854
|
+
*/
|
|
855
|
+
async function applyWidget(
|
|
856
|
+
db: Kysely<Database>,
|
|
857
|
+
areaId: string,
|
|
858
|
+
widget: SeedWidget,
|
|
859
|
+
sortOrder: number,
|
|
860
|
+
): Promise<void> {
|
|
861
|
+
await db
|
|
862
|
+
.insertInto("_emdash_widgets")
|
|
863
|
+
.values({
|
|
864
|
+
id: ulid(),
|
|
865
|
+
area_id: areaId,
|
|
866
|
+
sort_order: sortOrder,
|
|
867
|
+
type: widget.type,
|
|
868
|
+
title: widget.title ?? null,
|
|
869
|
+
content: widget.content ? JSON.stringify(widget.content) : null,
|
|
870
|
+
menu_name: widget.menuName ?? null,
|
|
871
|
+
component_id: widget.componentId ?? null,
|
|
872
|
+
component_props: widget.props ? JSON.stringify(widget.props) : null,
|
|
873
|
+
})
|
|
874
|
+
.execute();
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Context for media resolution during seed application
|
|
879
|
+
*/
|
|
880
|
+
interface MediaContext {
|
|
881
|
+
db: Kysely<Database>;
|
|
882
|
+
storage: Storage | null;
|
|
883
|
+
skipMediaDownload: boolean;
|
|
884
|
+
mediaCache: Map<string, MediaValue>; // URL -> resolved MediaValue
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Type guard for $media reference
|
|
889
|
+
*/
|
|
890
|
+
function isSeedMediaReference(value: unknown): value is SeedMediaReference {
|
|
891
|
+
if (typeof value !== "object" || value === null || !("$media" in value)) {
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
const media = (value as Record<string, unknown>).$media;
|
|
895
|
+
return (
|
|
896
|
+
typeof media === "object" &&
|
|
897
|
+
media !== null &&
|
|
898
|
+
"url" in media &&
|
|
899
|
+
typeof (media as Record<string, unknown>).url === "string"
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Resolve $ref: and $media references in content data
|
|
905
|
+
*/
|
|
906
|
+
async function resolveReferences(
|
|
907
|
+
data: Record<string, unknown>,
|
|
908
|
+
seedIdMap: Map<string, string>,
|
|
909
|
+
mediaContext: MediaContext,
|
|
910
|
+
result: SeedApplyResult,
|
|
911
|
+
): Promise<Record<string, unknown>> {
|
|
912
|
+
const resolved: Record<string, unknown> = {};
|
|
913
|
+
|
|
914
|
+
for (const [key, value] of Object.entries(data)) {
|
|
915
|
+
resolved[key] = await resolveValue(value, seedIdMap, mediaContext, result);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return resolved;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Resolve a single value recursively
|
|
923
|
+
*/
|
|
924
|
+
async function resolveValue(
|
|
925
|
+
value: unknown,
|
|
926
|
+
seedIdMap: Map<string, string>,
|
|
927
|
+
mediaContext: MediaContext,
|
|
928
|
+
result: SeedApplyResult,
|
|
929
|
+
): Promise<unknown> {
|
|
930
|
+
// Handle $ref: syntax
|
|
931
|
+
if (typeof value === "string" && value.startsWith("$ref:")) {
|
|
932
|
+
const seedId = value.slice(5);
|
|
933
|
+
return seedIdMap.get(seedId) ?? value; // Return unresolved if not found
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Handle $media syntax
|
|
937
|
+
if (isSeedMediaReference(value)) {
|
|
938
|
+
return resolveMedia(value, mediaContext, result);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Handle arrays
|
|
942
|
+
if (Array.isArray(value)) {
|
|
943
|
+
return Promise.all(value.map((item) => resolveValue(item, seedIdMap, mediaContext, result)));
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Handle objects recursively
|
|
947
|
+
if (typeof value === "object" && value !== null) {
|
|
948
|
+
const resolved: Record<string, unknown> = {};
|
|
949
|
+
for (const [k, v] of Object.entries(value)) {
|
|
950
|
+
resolved[k] = await resolveValue(v, seedIdMap, mediaContext, result);
|
|
951
|
+
}
|
|
952
|
+
return resolved;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return value;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Resolve a $media reference by downloading and uploading the media
|
|
960
|
+
*/
|
|
961
|
+
async function resolveMedia(
|
|
962
|
+
ref: SeedMediaReference,
|
|
963
|
+
ctx: MediaContext,
|
|
964
|
+
result: SeedApplyResult,
|
|
965
|
+
): Promise<MediaValue | null> {
|
|
966
|
+
const { url, alt, filename, caption } = ref.$media;
|
|
967
|
+
|
|
968
|
+
// Check cache first
|
|
969
|
+
const cached = ctx.mediaCache.get(url);
|
|
970
|
+
if (cached) {
|
|
971
|
+
result.media.skipped++;
|
|
972
|
+
return { ...cached, alt: alt ?? cached.alt };
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// When skipMediaDownload is set, resolve $media to an external URL reference
|
|
976
|
+
// without downloading or storing anything. Used by playground mode.
|
|
977
|
+
if (ctx.skipMediaDownload) {
|
|
978
|
+
const mediaValue: MediaValue = {
|
|
979
|
+
provider: "external",
|
|
980
|
+
id: ulid(),
|
|
981
|
+
src: url,
|
|
982
|
+
alt: alt ?? undefined,
|
|
983
|
+
filename: filename ?? undefined,
|
|
984
|
+
};
|
|
985
|
+
ctx.mediaCache.set(url, mediaValue);
|
|
986
|
+
result.media.created++;
|
|
987
|
+
return mediaValue;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Storage is required for $media resolution
|
|
991
|
+
if (!ctx.storage) {
|
|
992
|
+
console.warn(`Skipping $media reference (no storage configured): ${url}`);
|
|
993
|
+
result.media.skipped++;
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
try {
|
|
998
|
+
// SSRF protection: validate URL before downloading
|
|
999
|
+
validateExternalUrl(url);
|
|
1000
|
+
|
|
1001
|
+
// Download the media (ssrfSafeFetch re-validates redirect targets)
|
|
1002
|
+
console.log(` 📥 Downloading: ${url}`);
|
|
1003
|
+
const response = await ssrfSafeFetch(url, {
|
|
1004
|
+
headers: {
|
|
1005
|
+
// Some services like Unsplash require a user-agent
|
|
1006
|
+
"User-Agent": "EmDash-CMS/1.0",
|
|
1007
|
+
},
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
if (!response.ok) {
|
|
1011
|
+
console.warn(` ⚠️ Failed to download ${url}: ${response.status}`);
|
|
1012
|
+
result.media.skipped++;
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Get content type and determine extension
|
|
1017
|
+
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
1018
|
+
const ext = getExtensionFromContentType(contentType) || getExtensionFromUrl(url) || ".bin";
|
|
1019
|
+
|
|
1020
|
+
// Generate filename and storage key
|
|
1021
|
+
const id = ulid();
|
|
1022
|
+
const finalFilename = filename || generateFilename(url, ext);
|
|
1023
|
+
const storageKey = `${id}${ext}`;
|
|
1024
|
+
|
|
1025
|
+
// Get the body as buffer
|
|
1026
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1027
|
+
const body = new Uint8Array(arrayBuffer);
|
|
1028
|
+
|
|
1029
|
+
// Get image dimensions if it's an image
|
|
1030
|
+
let width: number | undefined;
|
|
1031
|
+
let height: number | undefined;
|
|
1032
|
+
if (contentType.startsWith("image/")) {
|
|
1033
|
+
const dimensions = getImageDimensions(body);
|
|
1034
|
+
width = dimensions?.width;
|
|
1035
|
+
height = dimensions?.height;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Upload to storage
|
|
1039
|
+
await ctx.storage.upload({
|
|
1040
|
+
key: storageKey,
|
|
1041
|
+
body,
|
|
1042
|
+
contentType,
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
// Create media record
|
|
1046
|
+
const mediaRepo = new MediaRepository(ctx.db);
|
|
1047
|
+
await mediaRepo.create({
|
|
1048
|
+
filename: finalFilename,
|
|
1049
|
+
mimeType: contentType,
|
|
1050
|
+
size: body.length,
|
|
1051
|
+
width,
|
|
1052
|
+
height,
|
|
1053
|
+
alt,
|
|
1054
|
+
caption,
|
|
1055
|
+
storageKey,
|
|
1056
|
+
status: "ready",
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
// Create the MediaValue - only store id, URL is built at runtime by EmDashMedia
|
|
1060
|
+
const mediaValue: MediaValue = {
|
|
1061
|
+
provider: "local",
|
|
1062
|
+
id,
|
|
1063
|
+
alt: alt ?? undefined,
|
|
1064
|
+
width,
|
|
1065
|
+
height,
|
|
1066
|
+
mimeType: contentType,
|
|
1067
|
+
filename: finalFilename,
|
|
1068
|
+
meta: { storageKey },
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
// Cache for reuse
|
|
1072
|
+
ctx.mediaCache.set(url, mediaValue);
|
|
1073
|
+
result.media.created++;
|
|
1074
|
+
|
|
1075
|
+
console.log(` ✅ Uploaded: ${finalFilename}`);
|
|
1076
|
+
return mediaValue;
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
console.warn(
|
|
1079
|
+
` ⚠️ Error processing $media ${url}:`,
|
|
1080
|
+
error instanceof Error ? error.message : error,
|
|
1081
|
+
);
|
|
1082
|
+
result.media.skipped++;
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Get file extension from content type
|
|
1089
|
+
*/
|
|
1090
|
+
function getExtensionFromContentType(contentType: string): string | null {
|
|
1091
|
+
// Handle content-type with parameters like "image/jpeg; charset=utf-8"
|
|
1092
|
+
const baseMime = contentType.split(";")[0].trim();
|
|
1093
|
+
const ext = mime.getExtension(baseMime);
|
|
1094
|
+
return ext ? `.${ext}` : null;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Get file extension from URL
|
|
1099
|
+
*/
|
|
1100
|
+
function getExtensionFromUrl(url: string): string | null {
|
|
1101
|
+
try {
|
|
1102
|
+
const pathname = new URL(url).pathname;
|
|
1103
|
+
const match = pathname.match(FILE_EXTENSION_PATTERN);
|
|
1104
|
+
return match ? `.${match[1]}` : null;
|
|
1105
|
+
} catch {
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Generate a filename from URL
|
|
1112
|
+
*/
|
|
1113
|
+
function generateFilename(url: string, ext: string): string {
|
|
1114
|
+
try {
|
|
1115
|
+
const pathname = new URL(url).pathname;
|
|
1116
|
+
const basename = pathname.split("/").pop() || "media";
|
|
1117
|
+
// Remove any existing extension and query params
|
|
1118
|
+
const name = basename.replace(EXTENSION_PATTERN, "").replace(QUERY_PARAM_PATTERN, "");
|
|
1119
|
+
// Sanitize: only alphanumeric, dash, underscore
|
|
1120
|
+
const sanitized = name.replace(SANITIZE_PATTERN, "-").replace(MULTIPLE_HYPHENS_PATTERN, "-");
|
|
1121
|
+
return `${sanitized || "media"}${ext}`;
|
|
1122
|
+
} catch {
|
|
1123
|
+
return `media${ext}`;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Get image dimensions from buffer using image-size.
|
|
1129
|
+
* Supports PNG, JPEG, GIF, WebP, AVIF, SVG, TIFF, and more.
|
|
1130
|
+
*/
|
|
1131
|
+
function getImageDimensions(buffer: Uint8Array): { width: number; height: number } | null {
|
|
1132
|
+
try {
|
|
1133
|
+
const result = imageSize(buffer);
|
|
1134
|
+
if (result.width != null && result.height != null) {
|
|
1135
|
+
return { width: result.width, height: result.height };
|
|
1136
|
+
}
|
|
1137
|
+
return null;
|
|
1138
|
+
} catch {
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
}
|