emdash 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapters-BKSf3T9R.d.mts → adapters-BktHA7EO.d.mts} +1 -1
- package/dist/{adapters-BKSf3T9R.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
- package/dist/{apply-x0eMK1lX.mjs → apply-UsrFuO7l.mjs} +207 -355
- package/dist/apply-UsrFuO7l.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +118 -4
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +6 -7
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +14 -57
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.d.mts.map +1 -1
- package/dist/astro/middleware/redirect.mjs +15 -10
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +8 -5
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +70 -121
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +25 -10
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-Chbr2GoP.mjs → byline-C3vnhIpU.mjs} +4 -4
- package/dist/{byline-Chbr2GoP.mjs.map → byline-C3vnhIpU.mjs.map} +1 -1
- package/dist/bylines-esI7ioa9.mjs +113 -0
- package/dist/bylines-esI7ioa9.mjs.map +1 -0
- package/dist/cache-fTzxgMFJ.mjs +65 -0
- package/dist/cache-fTzxgMFJ.mjs.map +1 -0
- package/dist/{chunks-HGz06Soa.mjs → chunks-Da2-b-oA.mjs} +8 -2
- package/dist/{chunks-HGz06Soa.mjs.map → chunks-Da2-b-oA.mjs.map} +1 -1
- package/dist/cli/index.mjs +456 -90
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +3 -3
- package/dist/client/index.mjs.map +1 -1
- package/dist/{config-BXwuX8Bx.mjs → config-CVssduLe.mjs} +1 -1
- package/dist/{config-BXwuX8Bx.mjs.map → config-CVssduLe.mjs.map} +1 -1
- package/dist/{content-BcQPYxdV.mjs → content-C7G4QXkK.mjs} +42 -14
- package/dist/content-C7G4QXkK.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/libsql.d.mts.map +1 -1
- package/dist/db/libsql.mjs +7 -2
- package/dist/db/libsql.mjs.map +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.d.mts.map +1 -1
- package/dist/db/sqlite.mjs +8 -3
- package/dist/db/sqlite.mjs.map +1 -1
- package/dist/{db-errors-l1Qh2RPR.mjs → db-errors-B7P2pSCn.mjs} +1 -1
- package/dist/{db-errors-l1Qh2RPR.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
- package/dist/{default-DCVqE5ib.mjs → default-pHuz9WF6.mjs} +1 -1
- package/dist/{default-DCVqE5ib.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
- package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
- package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
- package/dist/{error-zG5T1UGA.mjs → error-DqnRMM5z.mjs} +1 -1
- package/dist/{error-zG5T1UGA.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
- package/dist/{index-DIb-CzNx.d.mts → index-DjPMOfO0.d.mts} +162 -87
- package/dist/index-DjPMOfO0.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +27 -24
- package/dist/{load-CyEoextb.mjs → load-sXRuM7Us.mjs} +2 -2
- package/dist/{load-CyEoextb.mjs.map → load-sXRuM7Us.mjs.map} +1 -1
- package/dist/{loader-CndGj8kM.mjs → loader-Bx2_9-5e.mjs} +53 -8
- package/dist/loader-Bx2_9-5e.mjs.map +1 -0
- package/dist/{manifest-schema-DH9xhc6t.mjs → manifest-schema-CXAbd1vH.mjs} +33 -3
- package/dist/manifest-schema-CXAbd1vH.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/{mode-BnAOqItE.mjs → mode-YhqNVef_.mjs} +1 -1
- package/dist/{mode-BnAOqItE.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
- package/dist/options-nPxWnrya.mjs +117 -0
- package/dist/options-nPxWnrya.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
- package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
- package/dist/{placeholder-D29tWZ7o.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
- package/dist/{placeholder-D29tWZ7o.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
- package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
- package/dist/public-url-B1AxbbbQ.mjs +51 -0
- package/dist/public-url-B1AxbbbQ.mjs.map +1 -0
- package/dist/{query-fqEdLFms.mjs → query-Bo-msrmu.mjs} +114 -16
- package/dist/query-Bo-msrmu.mjs.map +1 -0
- package/dist/{redirect-D_pshWdf.mjs → redirect-C5H7VGIX.mjs} +11 -6
- package/dist/redirect-C5H7VGIX.mjs.map +1 -0
- package/dist/{registry-C3Mr0ODu.mjs → registry-Beb7wxFc.mjs} +39 -5
- package/dist/registry-Beb7wxFc.mjs.map +1 -0
- package/dist/{request-cache-Ci7f5pBb.mjs → request-cache-C-tIpYIw.mjs} +1 -1
- package/dist/{request-cache-Ci7f5pBb.mjs.map → request-cache-C-tIpYIw.mjs.map} +1 -1
- package/dist/runner-Clwe4Mme.d.mts +44 -0
- package/dist/runner-Clwe4Mme.d.mts.map +1 -0
- package/dist/{runner-tQ7BJ4T7.mjs → runner-DMnlIkh4.mjs} +616 -191
- package/dist/runner-DMnlIkh4.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-BoZYFuUk.mjs → search-DkN-BqsS.mjs} +270 -152
- package/dist/search-DkN-BqsS.mjs.map +1 -0
- package/dist/secrets-CZ8rxLX3.mjs +314 -0
- package/dist/secrets-CZ8rxLX3.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +13 -11
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/taxonomies-CTtewrSQ.mjs +407 -0
- package/dist/taxonomies-CTtewrSQ.mjs.map +1 -0
- package/dist/taxonomy-DSxx2K2L.mjs +218 -0
- package/dist/taxonomy-DSxx2K2L.mjs.map +1 -0
- package/dist/{tokens-D9vnZqYS.mjs → tokens-CyRDPVW2.mjs} +1 -1
- package/dist/{tokens-D9vnZqYS.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts → transport-DX_5rpsq.d.mts} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
- package/dist/{transport-C9ugt2Nr.mjs → transport-xpzIjCIB.mjs} +6 -5
- package/dist/{transport-C9ugt2Nr.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
- package/dist/{types-BrA0xf5I.d.mts → types-B_CXXnzh.d.mts} +1 -1
- package/dist/{types-BrA0xf5I.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
- package/dist/{types-DIMwPFub.d.mts → types-C-aFbqmA.d.mts} +1 -1
- package/dist/{types-DIMwPFub.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
- package/dist/types-CoO6mpV3.mjs +68 -0
- package/dist/types-CoO6mpV3.mjs.map +1 -0
- package/dist/{types-i36XcA_X.d.mts → types-D19uBYWn.d.mts} +83 -7
- package/dist/types-D19uBYWn.d.mts.map +1 -0
- package/dist/{types-BmPPSUEx.d.mts → types-Dl1fgFjn.d.mts} +24 -2
- package/dist/{types-BmPPSUEx.d.mts.map → types-Dl1fgFjn.d.mts.map} +1 -1
- package/dist/{types-CS8FIX7L.d.mts → types-Dtx1mSMX.d.mts} +9 -1
- package/dist/types-Dtx1mSMX.d.mts.map +1 -0
- package/dist/{types-Bm1dn-q3.mjs → types-Eg829jj9.mjs} +1 -1
- package/dist/{types-Bm1dn-q3.mjs.map → types-Eg829jj9.mjs.map} +1 -1
- package/dist/{types-CgqmmMJB.mjs → types-K-EkEQCI.mjs} +1 -1
- package/dist/{types-CgqmmMJB.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
- package/dist/{validate-CxVsLehf.mjs → validate-CBIbxM3L.mjs} +14 -10
- package/dist/validate-CBIbxM3L.mjs.map +1 -0
- package/dist/{validate-DHxmpFJt.d.mts → validate-DHGwADqO.d.mts} +18 -5
- package/dist/validate-DHGwADqO.d.mts.map +1 -0
- package/dist/{validation-C-ZpN2GI.mjs → validation-B1NYiEos.mjs} +6 -6
- package/dist/{validation-C-ZpN2GI.mjs.map → validation-B1NYiEos.mjs.map} +1 -1
- package/dist/version-CMD42IRC.mjs +7 -0
- package/dist/{version-Bbq8TCrz.mjs.map → version-CMD42IRC.mjs.map} +1 -1
- package/dist/{zod-generator-CpwccCIv.mjs → zod-generator-BNJDQBSZ.mjs} +11 -6
- package/dist/{zod-generator-CpwccCIv.mjs.map → zod-generator-BNJDQBSZ.mjs.map} +1 -1
- package/locals.d.ts +1 -6
- package/package.json +9 -8
- package/src/api/handlers/comments.ts +6 -4
- package/src/api/handlers/content.ts +40 -1
- package/src/api/handlers/dashboard.ts +29 -36
- package/src/api/handlers/device-flow.ts +5 -0
- package/src/api/handlers/marketplace.ts +11 -4
- package/src/api/handlers/menus.ts +256 -75
- package/src/api/handlers/oauth-authorization.ts +72 -33
- package/src/api/handlers/revision.ts +23 -14
- package/src/api/handlers/taxonomies.ts +273 -100
- package/src/api/public-url.ts +48 -2
- package/src/api/schemas/comments.ts +2 -2
- package/src/api/schemas/common.ts +7 -0
- package/src/api/schemas/content.ts +17 -0
- package/src/api/schemas/menus.ts +23 -0
- package/src/api/schemas/sections.ts +3 -3
- package/src/api/schemas/taxonomies.ts +39 -0
- package/src/api/schemas/users.ts +1 -1
- package/src/api/types.ts +5 -1
- package/src/astro/integration/index.ts +17 -0
- package/src/astro/integration/routes.ts +10 -0
- package/src/astro/integration/runtime.ts +30 -0
- package/src/astro/integration/virtual-modules.ts +32 -2
- package/src/astro/integration/vite-config.ts +6 -1
- package/src/astro/middleware/auth.ts +13 -6
- package/src/astro/middleware/redirect.ts +29 -16
- package/src/astro/middleware/request-context.ts +15 -5
- package/src/astro/middleware.ts +23 -9
- package/src/astro/routes/api/auth/invite/complete.ts +6 -1
- package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
- package/src/astro/routes/api/auth/signup/complete.ts +6 -1
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
- package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
- package/src/astro/routes/api/import/wordpress/prepare.ts +7 -8
- package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
- package/src/astro/routes/api/manifest.ts +62 -45
- package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
- package/src/astro/routes/api/menus/[name]/items.ts +16 -6
- package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
- package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
- package/src/astro/routes/api/menus/[name].ts +19 -10
- package/src/astro/routes/api/menus/index.ts +9 -6
- package/src/astro/routes/api/openapi.json.ts +27 -10
- package/src/astro/routes/api/redirects/404s/index.ts +10 -4
- package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
- package/src/astro/routes/api/redirects/[id].ts +10 -4
- package/src/astro/routes/api/redirects/index.ts +7 -3
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
- package/src/astro/routes/api/schema/collections/index.ts +1 -1
- package/src/astro/routes/api/search/index.ts +10 -2
- package/src/astro/routes/api/sections/[slug].ts +10 -4
- package/src/astro/routes/api/sections/index.ts +7 -3
- package/src/astro/routes/api/setup/admin-verify.ts +6 -1
- package/src/astro/routes/api/snapshot.ts +44 -18
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
- package/src/astro/routes/api/taxonomies/index.ts +9 -7
- package/src/astro/routes/api/themes/preview.ts +11 -5
- package/src/astro/types.ts +23 -3
- package/src/auth/allowed-origins.ts +168 -0
- package/src/auth/passkey-config.ts +35 -13
- package/src/bylines/index.ts +37 -88
- package/src/cli/commands/auth.ts +28 -6
- package/src/cli/commands/bundle-utils.ts +11 -2
- package/src/cli/commands/bundle.ts +28 -8
- package/src/cli/commands/content.ts +13 -0
- package/src/cli/commands/export-seed.ts +82 -21
- package/src/cli/commands/login.ts +8 -1
- package/src/cli/commands/plugin-init.ts +216 -90
- package/src/cli/commands/publish.ts +24 -0
- package/src/cli/commands/secrets.ts +183 -0
- package/src/cli/credentials.ts +1 -1
- package/src/cli/index.ts +5 -1
- package/src/client/index.ts +4 -4
- package/src/client/transport.ts +17 -7
- package/src/components/Break.astro +2 -2
- package/src/components/EmDashHead.astro +18 -13
- package/src/components/Embed.astro +1 -1
- package/src/components/Gallery.astro +1 -1
- package/src/components/Image.astro +1 -1
- package/src/components/InlinePortableTextEditor.tsx +104 -18
- package/src/config/secrets.ts +528 -0
- package/src/database/dialect-helpers.ts +50 -0
- package/src/database/migrations/034_published_at_index.ts +1 -1
- package/src/database/migrations/035_bounded_404_log.ts +56 -39
- package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
- package/src/database/migrations/runner.ts +158 -23
- package/src/database/repositories/content.ts +47 -12
- package/src/database/repositories/redirect.ts +14 -3
- package/src/database/repositories/taxonomy.ts +212 -82
- package/src/database/types.ts +10 -2
- package/src/db/libsql.ts +1 -3
- package/src/db/sqlite.ts +2 -5
- package/src/emdash-runtime.ts +84 -159
- package/src/i18n/resolve.ts +37 -0
- package/src/index.ts +9 -0
- package/src/loader.ts +73 -3
- package/src/mcp/server.ts +180 -54
- package/src/menus/index.ts +143 -124
- package/src/menus/types.ts +15 -1
- package/src/page/site-identity.ts +58 -0
- package/src/plugins/adapt-sandbox-entry.ts +22 -10
- package/src/plugins/context.ts +13 -10
- package/src/plugins/define-plugin.ts +40 -12
- package/src/plugins/hooks.ts +23 -19
- package/src/plugins/index.ts +9 -0
- package/src/plugins/manifest-schema.ts +37 -2
- package/src/plugins/types.ts +151 -11
- package/src/preview/urls.ts +23 -3
- package/src/query.ts +148 -5
- package/src/redirects/cache.ts +38 -18
- package/src/schema/registry.ts +56 -0
- package/src/schema/zod-generator.ts +39 -7
- package/src/seed/apply.ts +142 -54
- package/src/seed/types.ts +14 -1
- package/src/seed/validate.ts +27 -13
- package/src/settings/index.ts +80 -6
- package/src/settings/types.ts +23 -1
- package/src/taxonomies/index.ts +237 -210
- package/src/taxonomies/types.ts +10 -0
- package/dist/apply-x0eMK1lX.mjs.map +0 -1
- package/dist/bylines-CRNsVG88.mjs +0 -157
- package/dist/bylines-CRNsVG88.mjs.map +0 -1
- package/dist/cache-BkKBuIvS.mjs +0 -56
- package/dist/cache-BkKBuIvS.mjs.map +0 -1
- package/dist/chunk-ClPoSABd.mjs +0 -21
- package/dist/content-BcQPYxdV.mjs.map +0 -1
- package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
- package/dist/index-DIb-CzNx.d.mts.map +0 -1
- package/dist/loader-CndGj8kM.mjs.map +0 -1
- package/dist/manifest-schema-DH9xhc6t.mjs.map +0 -1
- package/dist/query-fqEdLFms.mjs.map +0 -1
- package/dist/redirect-D_pshWdf.mjs.map +0 -1
- package/dist/registry-C3Mr0ODu.mjs.map +0 -1
- package/dist/runner-OURCaApa.d.mts +0 -34
- package/dist/runner-OURCaApa.d.mts.map +0 -1
- package/dist/runner-tQ7BJ4T7.mjs.map +0 -1
- package/dist/search-BoZYFuUk.mjs.map +0 -1
- package/dist/taxonomies-B4IAshV8.mjs +0 -308
- package/dist/taxonomies-B4IAshV8.mjs.map +0 -1
- package/dist/types-CS8FIX7L.d.mts.map +0 -1
- package/dist/types-i36XcA_X.d.mts.map +0 -1
- package/dist/validate-CxVsLehf.mjs.map +0 -1
- package/dist/validate-DHxmpFJt.d.mts.map +0 -1
- package/dist/version-Bbq8TCrz.mjs +0 -7
package/src/redirects/cache.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Redirect
|
|
2
|
+
* Redirect rule cache.
|
|
3
3
|
*
|
|
4
|
-
* Module-level cache for
|
|
5
|
-
*
|
|
4
|
+
* Module-level cache for enabled redirect rules. The middleware populates this
|
|
5
|
+
* on first request; route handlers invalidate it on writes.
|
|
6
|
+
*
|
|
7
|
+
* Both exact-match and pattern rules are loaded from one query and cached
|
|
8
|
+
* together: exact rules indexed by source path in a Map, pattern rules
|
|
9
|
+
* pre-compiled into an array. A single warm request issues zero database
|
|
10
|
+
* queries; a cold isolate issues one.
|
|
6
11
|
*
|
|
7
12
|
* This module deliberately has NO Astro imports so it can be safely imported
|
|
8
13
|
* from handlers, seed, CLI, and tests without dragging in `astro:middleware`.
|
|
@@ -17,36 +22,51 @@ export interface CachedRedirectRule {
|
|
|
17
22
|
compiled: CompiledPattern;
|
|
18
23
|
}
|
|
19
24
|
|
|
25
|
+
export interface CachedRedirects {
|
|
26
|
+
/** Exact-match rules indexed by source path (`source` -> `Redirect`). */
|
|
27
|
+
exact: Map<string, Redirect>;
|
|
28
|
+
/** Pattern rules with their compiled regexes, preserving insertion order. */
|
|
29
|
+
patterns: CachedRedirectRule[];
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
/**
|
|
21
|
-
* Cached
|
|
22
|
-
* null = not yet populated,
|
|
33
|
+
* Cached enabled redirects.
|
|
34
|
+
* null = not yet populated, object = cached.
|
|
23
35
|
*/
|
|
24
|
-
let
|
|
36
|
+
let cachedRedirects: CachedRedirects | null = null;
|
|
25
37
|
|
|
26
38
|
/**
|
|
27
|
-
* Invalidate the cached
|
|
39
|
+
* Invalidate the cached redirects (both exact and pattern).
|
|
28
40
|
* Call when redirects are created, updated, or deleted.
|
|
29
41
|
*/
|
|
30
42
|
export function invalidateRedirectCache(): void {
|
|
31
|
-
|
|
43
|
+
cachedRedirects = null;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
/**
|
|
35
|
-
* Get the cached
|
|
47
|
+
* Get the cached redirects, or null if the cache is cold.
|
|
36
48
|
*/
|
|
37
|
-
export function
|
|
38
|
-
return
|
|
49
|
+
export function getCachedRedirects(): CachedRedirects | null {
|
|
50
|
+
return cachedRedirects;
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
/**
|
|
42
|
-
* Populate the
|
|
54
|
+
* Populate the cache from a list of enabled redirects (both exact and
|
|
55
|
+
* pattern). The caller is responsible for passing only enabled rows — the
|
|
56
|
+
* cache stores them as-is.
|
|
43
57
|
*/
|
|
44
|
-
export function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
export function setCachedRedirects(redirects: Redirect[]): CachedRedirects {
|
|
59
|
+
const exact = new Map<string, Redirect>();
|
|
60
|
+
const patterns: CachedRedirectRule[] = [];
|
|
61
|
+
for (const r of redirects) {
|
|
62
|
+
if (r.isPattern) {
|
|
63
|
+
patterns.push({ redirect: r, compiled: compilePattern(r.source) });
|
|
64
|
+
} else {
|
|
65
|
+
exact.set(r.source, r);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
cachedRedirects = { exact, patterns };
|
|
69
|
+
return cachedRedirects;
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
/**
|
package/src/schema/registry.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { withTransaction } from "../database/transaction.js";
|
|
|
8
8
|
import type { CollectionTable, Database, FieldTable } from "../database/types.js";
|
|
9
9
|
import { validateIdentifier } from "../database/validate.js";
|
|
10
10
|
import { FTSManager } from "../search/fts-manager.js";
|
|
11
|
+
import { chunks, SQL_BATCH_SIZE } from "../utils/chunks.js";
|
|
11
12
|
import {
|
|
12
13
|
type Collection,
|
|
13
14
|
type CollectionSource,
|
|
@@ -143,6 +144,61 @@ export class SchemaRegistry {
|
|
|
143
144
|
return { ...collection, fields };
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
/**
|
|
148
|
+
* List every collection together with its fields in O(1) query shapes
|
|
149
|
+
* — one for collections, then one batched query for the fields of every
|
|
150
|
+
* returned collection — instead of the N+1 pattern of `listCollections`
|
|
151
|
+
* + per-collection `listFields`. The fields query is chunked at
|
|
152
|
+
* `SQL_BATCH_SIZE` to stay under D1's bound-parameter limit, so on
|
|
153
|
+
* sites with more than `SQL_BATCH_SIZE` collections the field fetch
|
|
154
|
+
* becomes `ceil(collectionCount / SQL_BATCH_SIZE)` queries — still
|
|
155
|
+
* a constant factor, not N+1. Typical sites have well under
|
|
156
|
+
* `SQL_BATCH_SIZE` collections, so this is two queries in practice.
|
|
157
|
+
*
|
|
158
|
+
* Used by the manifest build, which previously paid N+1 round-trips on
|
|
159
|
+
* every admin request. Each round-trip costs ~80–150ms against the D1
|
|
160
|
+
* primary on a busy link, so a 10-collection site spent ~1 s rebuilding
|
|
161
|
+
* a manifest that is now built fresh per admin request (no cache).
|
|
162
|
+
*/
|
|
163
|
+
async listCollectionsWithFields(): Promise<CollectionWithFields[]> {
|
|
164
|
+
const collectionRows = await this.db
|
|
165
|
+
.selectFrom("_emdash_collections")
|
|
166
|
+
.selectAll()
|
|
167
|
+
.orderBy("slug", "asc")
|
|
168
|
+
.execute();
|
|
169
|
+
|
|
170
|
+
if (collectionRows.length === 0) return [];
|
|
171
|
+
|
|
172
|
+
const fieldsByCollection = new Map<string, Field[]>();
|
|
173
|
+
// Chunk to stay under D1's bound-parameter limit. Typical sites have
|
|
174
|
+
// well under SQL_BATCH_SIZE collections, so this is a single query
|
|
175
|
+
// in practice; on larger sites it becomes a small constant number
|
|
176
|
+
// of queries, never N+1.
|
|
177
|
+
for (const idChunk of chunks(
|
|
178
|
+
collectionRows.map((c) => c.id),
|
|
179
|
+
SQL_BATCH_SIZE,
|
|
180
|
+
)) {
|
|
181
|
+
const fieldRows = await this.db
|
|
182
|
+
.selectFrom("_emdash_fields")
|
|
183
|
+
.where("collection_id", "in", idChunk)
|
|
184
|
+
.selectAll()
|
|
185
|
+
.orderBy("collection_id", "asc")
|
|
186
|
+
.orderBy("sort_order", "asc")
|
|
187
|
+
.orderBy("created_at", "asc")
|
|
188
|
+
.execute();
|
|
189
|
+
for (const row of fieldRows) {
|
|
190
|
+
const list = fieldsByCollection.get(row.collection_id) ?? [];
|
|
191
|
+
list.push(this.mapFieldRow(row));
|
|
192
|
+
fieldsByCollection.set(row.collection_id, list);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return collectionRows.map((c) => ({
|
|
197
|
+
...this.mapCollectionRow(c),
|
|
198
|
+
fields: fieldsByCollection.get(c.id) ?? [],
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
|
|
146
202
|
/**
|
|
147
203
|
* Create a new collection
|
|
148
204
|
*/
|
|
@@ -35,9 +35,16 @@ export function generateFieldSchema(field: Field): ZodTypeAny {
|
|
|
35
35
|
schema = applyValidation(schema, field);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// Apply required/optional
|
|
38
|
+
// Apply required/optional. Non-required fields use `.nullish()` rather
|
|
39
|
+
// than `.optional()` because the underlying SQLite columns are nullable
|
|
40
|
+
// (see `SchemaRegistry.addFieldColumn` -- non-required fields are added
|
|
41
|
+
// without `NOT NULL`). The admin re-sends what it loaded from the
|
|
42
|
+
// server on autosave, so any field that's actually `null` in the DB
|
|
43
|
+
// must round-trip cleanly through the validator. `.optional()` only
|
|
44
|
+
// accepts `undefined`; `.nullish()` accepts both `undefined` and
|
|
45
|
+
// `null`. (#867 — autosave failures on seeded entries.)
|
|
39
46
|
if (!field.required) {
|
|
40
|
-
schema = schema.
|
|
47
|
+
schema = schema.nullish();
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
// Apply default value
|
|
@@ -68,7 +75,15 @@ function getBaseSchema(type: FieldType, field: Field): ZodTypeAny {
|
|
|
68
75
|
return z.number().int();
|
|
69
76
|
|
|
70
77
|
case "boolean":
|
|
71
|
-
|
|
78
|
+
// Boolean fields map to `INTEGER` columns (`FIELD_TYPE_TO_COLUMN`
|
|
79
|
+
// in `schema/types.ts`) and `serializeValue` in
|
|
80
|
+
// `database/repositories/content.ts` writes booleans as 0/1.
|
|
81
|
+
// `deserializeValue` never converts them back, so reads return
|
|
82
|
+
// numbers. Coerce the stored 0/1 shape here so a GET → POST
|
|
83
|
+
// round-trip on a boolean field passes validation. Other inputs
|
|
84
|
+
// (strings, other numbers) fall through to `z.boolean()` and
|
|
85
|
+
// produce its standard rejection.
|
|
86
|
+
return z.preprocess((v) => (v === 0 || v === 1 ? Boolean(v) : v), z.boolean());
|
|
72
87
|
|
|
73
88
|
case "datetime":
|
|
74
89
|
return z.string().datetime().or(z.string().date());
|
|
@@ -92,12 +107,19 @@ function getBaseSchema(type: FieldType, field: Field): ZodTypeAny {
|
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
case "portableText":
|
|
95
|
-
// Portable Text is an array of blocks
|
|
110
|
+
// Portable Text is an array of blocks. We require `_type` because
|
|
111
|
+
// renderers dispatch on it, but `_key` is intentionally optional:
|
|
112
|
+
// it's a UI-layer concern that the editor regenerates on every
|
|
113
|
+
// change (see `PortableTextEditor`), and the rest of this schema
|
|
114
|
+
// uses `.passthrough()` for everything below the top level. Making
|
|
115
|
+
// `_key` strictly required here was an accidentally tight invariant
|
|
116
|
+
// that rejected any seed/import data not authored against the
|
|
117
|
+
// editor (#867 — autosave failures on seeded template content).
|
|
96
118
|
return z.array(
|
|
97
119
|
z
|
|
98
120
|
.object({
|
|
99
121
|
_type: z.string(),
|
|
100
|
-
_key: z.string(),
|
|
122
|
+
_key: z.string().optional(),
|
|
101
123
|
})
|
|
102
124
|
.passthrough(),
|
|
103
125
|
);
|
|
@@ -109,6 +131,12 @@ function getBaseSchema(type: FieldType, field: Field): ZodTypeAny {
|
|
|
109
131
|
alt: z.string().optional(),
|
|
110
132
|
width: z.number().optional(),
|
|
111
133
|
height: z.number().optional(),
|
|
134
|
+
/** Provider ID (e.g. "local", "cloudflare-images") */
|
|
135
|
+
provider: z.string().optional(),
|
|
136
|
+
/** Admin-side preview URL for external providers (not persisted by plugins) */
|
|
137
|
+
previewUrl: z.string().optional(),
|
|
138
|
+
/** Provider-specific metadata; for local media this carries storageKey */
|
|
139
|
+
meta: z.record(z.string(), z.unknown()).optional(),
|
|
112
140
|
});
|
|
113
141
|
|
|
114
142
|
case "file":
|
|
@@ -118,6 +146,10 @@ function getBaseSchema(type: FieldType, field: Field): ZodTypeAny {
|
|
|
118
146
|
filename: z.string().optional(),
|
|
119
147
|
mimeType: z.string().optional(),
|
|
120
148
|
size: z.number().optional(),
|
|
149
|
+
/** Provider ID (e.g. "local", "s3") */
|
|
150
|
+
provider: z.string().optional(),
|
|
151
|
+
/** Provider-specific metadata; for local media this carries storageKey */
|
|
152
|
+
meta: z.record(z.string(), z.unknown()).optional(),
|
|
121
153
|
});
|
|
122
154
|
|
|
123
155
|
case "reference":
|
|
@@ -362,10 +394,10 @@ function fieldTypeToTypeScript(field: Field): string {
|
|
|
362
394
|
return "PortableTextBlock[]";
|
|
363
395
|
|
|
364
396
|
case "image":
|
|
365
|
-
return "{ id: string; src?: string; alt?: string; width?: number; height?: number }";
|
|
397
|
+
return "{ id: string; src?: string; alt?: string; width?: number; height?: number; provider?: string; previewUrl?: string; meta?: Record<string, unknown> }";
|
|
366
398
|
|
|
367
399
|
case "file":
|
|
368
|
-
return "{ id: string; src?: string; filename?: string; mimeType?: string; size?: number }";
|
|
400
|
+
return "{ id: string; src?: string; filename?: string; mimeType?: string; size?: number; provider?: string; meta?: Record<string, unknown> }";
|
|
369
401
|
|
|
370
402
|
case "reference":
|
|
371
403
|
// Could be enhanced to include the referenced collection type
|
package/src/seed/apply.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { TaxonomyRepository } from "../database/repositories/taxonomy.js";
|
|
|
19
19
|
import { withTransaction } from "../database/transaction.js";
|
|
20
20
|
import type { Database } from "../database/types.js";
|
|
21
21
|
import type { MediaValue } from "../fields/types.js";
|
|
22
|
+
import { getI18nConfig } from "../i18n/config.js";
|
|
22
23
|
import { ssrfSafeFetch, validateExternalUrl } from "../import/ssrf.js";
|
|
23
24
|
import { SchemaRegistry } from "../schema/registry.js";
|
|
24
25
|
import { FTSManager } from "../search/fts-manager.js";
|
|
@@ -219,17 +220,30 @@ export async function applySeed(
|
|
|
219
220
|
|
|
220
221
|
// 4-5. Taxonomies
|
|
221
222
|
if (seed.taxonomies) {
|
|
223
|
+
// seed-local id -> resolved info, used to wire `translationOf` refs.
|
|
224
|
+
const defSeedIdMap = new Map<string, { id: string; translationGroup: string }>();
|
|
225
|
+
const termSeedIdMap = new Map<string, string>();
|
|
226
|
+
const fallbackLocale = getI18nConfig()?.defaultLocale ?? "en";
|
|
227
|
+
|
|
222
228
|
for (const taxonomy of seed.taxonomies) {
|
|
223
|
-
|
|
229
|
+
const defLocale = taxonomy.locale ?? fallbackLocale;
|
|
230
|
+
|
|
231
|
+
// (name, locale) is the UNIQUE key after migration 036.
|
|
224
232
|
const existingDef = await db
|
|
225
233
|
.selectFrom("_emdash_taxonomy_defs")
|
|
226
234
|
.selectAll()
|
|
227
235
|
.where("name", "=", taxonomy.name)
|
|
236
|
+
.where("locale", "=", defLocale)
|
|
228
237
|
.executeTakeFirst();
|
|
229
238
|
|
|
239
|
+
let defId: string;
|
|
240
|
+
let defTranslationGroup: string;
|
|
241
|
+
|
|
230
242
|
if (existingDef) {
|
|
243
|
+
defId = existingDef.id;
|
|
244
|
+
defTranslationGroup = existingDef.translation_group ?? existingDef.id;
|
|
231
245
|
if (onConflict === "error") {
|
|
232
|
-
throw new Error(`Conflict: taxonomy "${taxonomy.name}" already exists`);
|
|
246
|
+
throw new Error(`Conflict: taxonomy "${taxonomy.name}" (${defLocale}) already exists`);
|
|
233
247
|
}
|
|
234
248
|
if (onConflict === "update") {
|
|
235
249
|
await db
|
|
@@ -242,40 +256,59 @@ export async function applySeed(
|
|
|
242
256
|
})
|
|
243
257
|
.where("id", "=", existingDef.id)
|
|
244
258
|
.execute();
|
|
245
|
-
// Taxonomy defs don't track an "updated" counter -- just the definition is updated
|
|
246
259
|
}
|
|
247
|
-
// skip: do nothing for the definition
|
|
248
260
|
} else {
|
|
249
|
-
|
|
261
|
+
defId = ulid();
|
|
262
|
+
defTranslationGroup = defId;
|
|
263
|
+
if (taxonomy.translationOf) {
|
|
264
|
+
const source = defSeedIdMap.get(taxonomy.translationOf);
|
|
265
|
+
if (source) defTranslationGroup = source.translationGroup;
|
|
266
|
+
else
|
|
267
|
+
console.warn(
|
|
268
|
+
`taxonomy "${taxonomy.name}" (${defLocale}): translationOf "${taxonomy.translationOf}" not found yet; minting a fresh group.`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
250
271
|
await db
|
|
251
272
|
.insertInto("_emdash_taxonomy_defs")
|
|
252
273
|
.values({
|
|
253
|
-
id:
|
|
274
|
+
id: defId,
|
|
254
275
|
name: taxonomy.name,
|
|
255
276
|
label: taxonomy.label,
|
|
256
277
|
label_singular: taxonomy.labelSingular ?? null,
|
|
257
278
|
hierarchical: taxonomy.hierarchical ? 1 : 0,
|
|
258
279
|
collections: JSON.stringify(taxonomy.collections),
|
|
280
|
+
locale: defLocale,
|
|
281
|
+
translation_group: defTranslationGroup,
|
|
259
282
|
})
|
|
260
283
|
.execute();
|
|
261
284
|
result.taxonomies.created++;
|
|
262
285
|
}
|
|
263
286
|
|
|
287
|
+
if (taxonomy.id)
|
|
288
|
+
defSeedIdMap.set(taxonomy.id, { id: defId, translationGroup: defTranslationGroup });
|
|
289
|
+
|
|
264
290
|
// Create terms (if provided)
|
|
265
291
|
if (taxonomy.terms && taxonomy.terms.length > 0) {
|
|
266
292
|
const termRepo = new TaxonomyRepository(db);
|
|
267
293
|
|
|
268
|
-
// For hierarchical taxonomies, we need to create parents before children
|
|
269
294
|
if (taxonomy.hierarchical) {
|
|
270
|
-
await applyHierarchicalTerms(
|
|
295
|
+
await applyHierarchicalTerms(
|
|
296
|
+
termRepo,
|
|
297
|
+
taxonomy.name,
|
|
298
|
+
defLocale,
|
|
299
|
+
taxonomy.terms,
|
|
300
|
+
termSeedIdMap,
|
|
301
|
+
result,
|
|
302
|
+
onConflict,
|
|
303
|
+
);
|
|
271
304
|
} else {
|
|
272
|
-
// Flat taxonomy - create all terms
|
|
273
305
|
for (const term of taxonomy.terms) {
|
|
274
|
-
const
|
|
306
|
+
const termLocale = term.locale ?? defLocale;
|
|
307
|
+
const existing = await termRepo.findBySlug(taxonomy.name, term.slug, termLocale);
|
|
275
308
|
if (existing) {
|
|
276
309
|
if (onConflict === "error") {
|
|
277
310
|
throw new Error(
|
|
278
|
-
`Conflict: taxonomy term "${term.slug}" in "${taxonomy.name}" already exists`,
|
|
311
|
+
`Conflict: taxonomy term "${term.slug}" in "${taxonomy.name}" (${termLocale}) already exists`,
|
|
279
312
|
);
|
|
280
313
|
}
|
|
281
314
|
if (onConflict === "update") {
|
|
@@ -285,14 +318,20 @@ export async function applySeed(
|
|
|
285
318
|
});
|
|
286
319
|
result.taxonomies.terms++;
|
|
287
320
|
}
|
|
288
|
-
|
|
321
|
+
if (term.id) termSeedIdMap.set(term.id, existing.id);
|
|
289
322
|
} else {
|
|
290
|
-
|
|
323
|
+
const translationOf = term.translationOf
|
|
324
|
+
? termSeedIdMap.get(term.translationOf)
|
|
325
|
+
: undefined;
|
|
326
|
+
const created = await termRepo.create({
|
|
291
327
|
name: taxonomy.name,
|
|
292
328
|
slug: term.slug,
|
|
293
329
|
label: term.label,
|
|
294
330
|
data: term.description ? { description: term.description } : undefined,
|
|
331
|
+
locale: termLocale,
|
|
332
|
+
translationOf,
|
|
295
333
|
});
|
|
334
|
+
if (term.id) termSeedIdMap.set(term.id, created.id);
|
|
296
335
|
result.taxonomies.terms++;
|
|
297
336
|
}
|
|
298
337
|
}
|
|
@@ -471,23 +510,39 @@ export async function applySeed(
|
|
|
471
510
|
|
|
472
511
|
// 8. Menus and Menu Items (after content so refs can resolve)
|
|
473
512
|
if (seed.menus) {
|
|
513
|
+
// seed-local id -> resolved info, used to wire `translationOf` refs.
|
|
514
|
+
const menuSeedIdMap = new Map<string, { id: string; translationGroup: string }>();
|
|
515
|
+
const fallbackLocale = getI18nConfig()?.defaultLocale ?? "en";
|
|
516
|
+
|
|
474
517
|
for (const menu of seed.menus) {
|
|
475
|
-
|
|
476
|
-
|
|
518
|
+
const locale = menu.locale ?? fallbackLocale;
|
|
519
|
+
let lookup = db
|
|
477
520
|
.selectFrom("_emdash_menus")
|
|
478
521
|
.selectAll()
|
|
479
522
|
.where("name", "=", menu.name)
|
|
480
|
-
.
|
|
523
|
+
.where("locale", "=", locale);
|
|
524
|
+
const existingMenu = await lookup.executeTakeFirst();
|
|
481
525
|
|
|
482
526
|
let menuId: string;
|
|
527
|
+
let translationGroup: string;
|
|
483
528
|
|
|
484
529
|
if (existingMenu) {
|
|
485
530
|
menuId = existingMenu.id;
|
|
531
|
+
translationGroup = existingMenu.translation_group ?? existingMenu.id;
|
|
486
532
|
// Clear existing items (menus are recreated)
|
|
487
533
|
await db.deleteFrom("_emdash_menu_items").where("menu_id", "=", menuId).execute();
|
|
488
534
|
} else {
|
|
489
|
-
// Create menu
|
|
490
535
|
menuId = ulid();
|
|
536
|
+
// Resolve translationOf to the source menu's translation_group.
|
|
537
|
+
translationGroup = menuId;
|
|
538
|
+
if (menu.translationOf) {
|
|
539
|
+
const source = menuSeedIdMap.get(menu.translationOf);
|
|
540
|
+
if (source) translationGroup = source.translationGroup;
|
|
541
|
+
else
|
|
542
|
+
console.warn(
|
|
543
|
+
`menu "${menu.name}" (${locale}): translationOf "${menu.translationOf}" not found yet; minting a fresh group.`,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
491
546
|
await db
|
|
492
547
|
.insertInto("_emdash_menus")
|
|
493
548
|
.values({
|
|
@@ -496,15 +551,20 @@ export async function applySeed(
|
|
|
496
551
|
label: menu.label,
|
|
497
552
|
created_at: new Date().toISOString(),
|
|
498
553
|
updated_at: new Date().toISOString(),
|
|
554
|
+
locale,
|
|
555
|
+
translation_group: translationGroup,
|
|
499
556
|
})
|
|
500
557
|
.execute();
|
|
501
558
|
result.menus.created++;
|
|
502
559
|
}
|
|
503
560
|
|
|
561
|
+
if (menu.id) menuSeedIdMap.set(menu.id, { id: menuId, translationGroup });
|
|
562
|
+
|
|
504
563
|
// Create menu items
|
|
505
564
|
const itemCount = await applyMenuItems(
|
|
506
565
|
db,
|
|
507
566
|
menuId,
|
|
567
|
+
locale,
|
|
508
568
|
menu.items,
|
|
509
569
|
null, // parent_id
|
|
510
570
|
0, // sort_order
|
|
@@ -692,64 +752,75 @@ export async function applySeed(
|
|
|
692
752
|
async function applyHierarchicalTerms(
|
|
693
753
|
termRepo: TaxonomyRepository,
|
|
694
754
|
taxonomyName: string,
|
|
755
|
+
defLocale: string,
|
|
695
756
|
terms: SeedTaxonomyTerm[],
|
|
757
|
+
termSeedIdMap: Map<string, string>,
|
|
696
758
|
result: SeedApplyResult,
|
|
697
759
|
onConflict: "skip" | "update" | "error" = "skip",
|
|
698
760
|
): Promise<void> {
|
|
699
|
-
//
|
|
761
|
+
// "locale::slug" -> id, so the same slug can resolve per locale.
|
|
700
762
|
const slugToId = new Map<string, string>();
|
|
701
763
|
|
|
702
|
-
// Multiple passes
|
|
764
|
+
// Multiple passes — handles deep nesting and translationOf forward refs.
|
|
703
765
|
let remaining = [...terms];
|
|
704
|
-
let maxPasses = 10;
|
|
766
|
+
let maxPasses = 10;
|
|
705
767
|
|
|
706
768
|
while (remaining.length > 0 && maxPasses > 0) {
|
|
707
769
|
const processedThisPass: string[] = [];
|
|
708
770
|
|
|
709
771
|
for (const term of remaining) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
772
|
+
const termLocale = term.locale ?? defLocale;
|
|
773
|
+
const parentReady = !term.parent || slugToId.has(`${termLocale}::${term.parent}`);
|
|
774
|
+
const translationReady = !term.translationOf || termSeedIdMap.has(term.translationOf);
|
|
713
775
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
slugToId.set(term.slug, existing.id);
|
|
730
|
-
} else {
|
|
731
|
-
const created = await termRepo.create({
|
|
732
|
-
name: taxonomyName,
|
|
733
|
-
slug: term.slug,
|
|
776
|
+
if (!parentReady || !translationReady) continue;
|
|
777
|
+
|
|
778
|
+
const parentId = term.parent ? slugToId.get(`${termLocale}::${term.parent}`) : undefined;
|
|
779
|
+
const translationOf = term.translationOf ? termSeedIdMap.get(term.translationOf) : undefined;
|
|
780
|
+
|
|
781
|
+
const existing = await termRepo.findBySlug(taxonomyName, term.slug, termLocale);
|
|
782
|
+
if (existing) {
|
|
783
|
+
if (onConflict === "error") {
|
|
784
|
+
throw new Error(
|
|
785
|
+
`Conflict: taxonomy term "${term.slug}" in "${taxonomyName}" (${termLocale}) already exists`,
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
if (onConflict === "update") {
|
|
789
|
+
await termRepo.update(existing.id, {
|
|
734
790
|
label: term.label,
|
|
735
791
|
parentId,
|
|
736
|
-
data: term.description ? { description: term.description } :
|
|
792
|
+
data: term.description ? { description: term.description } : {},
|
|
737
793
|
});
|
|
738
|
-
slugToId.set(term.slug, created.id);
|
|
739
794
|
result.taxonomies.terms++;
|
|
740
795
|
}
|
|
741
|
-
|
|
742
|
-
|
|
796
|
+
slugToId.set(`${termLocale}::${term.slug}`, existing.id);
|
|
797
|
+
if (term.id) termSeedIdMap.set(term.id, existing.id);
|
|
798
|
+
} else {
|
|
799
|
+
const created = await termRepo.create({
|
|
800
|
+
name: taxonomyName,
|
|
801
|
+
slug: term.slug,
|
|
802
|
+
label: term.label,
|
|
803
|
+
parentId,
|
|
804
|
+
data: term.description ? { description: term.description } : undefined,
|
|
805
|
+
locale: termLocale,
|
|
806
|
+
translationOf,
|
|
807
|
+
});
|
|
808
|
+
slugToId.set(`${termLocale}::${term.slug}`, created.id);
|
|
809
|
+
if (term.id) termSeedIdMap.set(term.id, created.id);
|
|
810
|
+
result.taxonomies.terms++;
|
|
743
811
|
}
|
|
812
|
+
|
|
813
|
+
processedThisPass.push(term.slug + "::" + termLocale);
|
|
744
814
|
}
|
|
745
815
|
|
|
746
|
-
|
|
747
|
-
|
|
816
|
+
remaining = remaining.filter(
|
|
817
|
+
(t) => !processedThisPass.includes(t.slug + "::" + (t.locale ?? defLocale)),
|
|
818
|
+
);
|
|
748
819
|
maxPasses--;
|
|
749
820
|
}
|
|
750
821
|
|
|
751
822
|
if (remaining.length > 0) {
|
|
752
|
-
console.warn(`Could not process ${remaining.length} terms due to missing parents`);
|
|
823
|
+
console.warn(`Could not process ${remaining.length} terms due to missing parents/translations`);
|
|
753
824
|
}
|
|
754
825
|
}
|
|
755
826
|
|
|
@@ -847,11 +918,18 @@ async function applyContentTaxonomies(
|
|
|
847
918
|
}
|
|
848
919
|
|
|
849
920
|
/**
|
|
850
|
-
* Apply menu items recursively
|
|
921
|
+
* Apply menu items recursively.
|
|
922
|
+
*
|
|
923
|
+
* Each item gets a fresh `translation_group` (= its own id). The seed format's
|
|
924
|
+
* `SeedMenuItem` has no `id`/`translationOf` fields, so we can't express the
|
|
925
|
+
* cross-locale "same nav entry" link here — items diverge across locales on
|
|
926
|
+
* re-apply. Runtime navigation still resolves correctly because `reference_id`
|
|
927
|
+
* already holds the content's translation_group.
|
|
851
928
|
*/
|
|
852
929
|
async function applyMenuItems(
|
|
853
930
|
db: Kysely<Database>,
|
|
854
931
|
menuId: string,
|
|
932
|
+
locale: string,
|
|
855
933
|
items: SeedMenuItem[],
|
|
856
934
|
parentId: string | null,
|
|
857
935
|
startOrder: number,
|
|
@@ -877,7 +955,6 @@ async function applyMenuItems(
|
|
|
877
955
|
// If not in map, the content might not exist yet (will be broken link)
|
|
878
956
|
}
|
|
879
957
|
|
|
880
|
-
// Insert menu item
|
|
881
958
|
await db
|
|
882
959
|
.insertInto("_emdash_menu_items")
|
|
883
960
|
.values({
|
|
@@ -894,15 +971,24 @@ async function applyMenuItems(
|
|
|
894
971
|
target: item.target ?? null,
|
|
895
972
|
css_classes: item.cssClasses ?? null,
|
|
896
973
|
created_at: new Date().toISOString(),
|
|
974
|
+
locale,
|
|
975
|
+
translation_group: itemId,
|
|
897
976
|
})
|
|
898
977
|
.execute();
|
|
899
978
|
|
|
900
979
|
count++;
|
|
901
980
|
order++;
|
|
902
981
|
|
|
903
|
-
// Process children
|
|
904
982
|
if (item.children && item.children.length > 0) {
|
|
905
|
-
const childCount = await applyMenuItems(
|
|
983
|
+
const childCount = await applyMenuItems(
|
|
984
|
+
db,
|
|
985
|
+
menuId,
|
|
986
|
+
locale,
|
|
987
|
+
item.children,
|
|
988
|
+
itemId,
|
|
989
|
+
0,
|
|
990
|
+
seedIdMap,
|
|
991
|
+
);
|
|
906
992
|
count += childCount;
|
|
907
993
|
}
|
|
908
994
|
}
|
|
@@ -927,6 +1013,8 @@ async function applyWidget(
|
|
|
927
1013
|
sort_order: sortOrder,
|
|
928
1014
|
type: widget.type,
|
|
929
1015
|
title: widget.title ?? null,
|
|
1016
|
+
// `widget.content` is Portable Text for content-type widgets;
|
|
1017
|
+
// for other widget kinds it's null.
|
|
930
1018
|
content: widget.content ? JSON.stringify(widget.content) : null,
|
|
931
1019
|
menu_name: widget.menuName ?? null,
|
|
932
1020
|
component_id: widget.componentId ?? null,
|
package/src/seed/types.ts
CHANGED
|
@@ -87,14 +87,19 @@ export interface SeedField {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* Taxonomy definition in seed
|
|
90
|
+
* Taxonomy definition in seed. For multi-locale exports each locale variant
|
|
91
|
+
* is its own entry, linked via `translationOf` (referencing another entry's `id`).
|
|
91
92
|
*/
|
|
92
93
|
export interface SeedTaxonomy {
|
|
94
|
+
/** Optional seed-local id, e.g. "tax:category:en". Target of `translationOf`. */
|
|
95
|
+
id?: string;
|
|
93
96
|
name: string;
|
|
94
97
|
label: string;
|
|
95
98
|
labelSingular?: string;
|
|
96
99
|
hierarchical: boolean;
|
|
97
100
|
collections: string[];
|
|
101
|
+
locale?: string;
|
|
102
|
+
translationOf?: string;
|
|
98
103
|
terms?: SeedTaxonomyTerm[];
|
|
99
104
|
}
|
|
100
105
|
|
|
@@ -102,18 +107,26 @@ export interface SeedTaxonomy {
|
|
|
102
107
|
* Taxonomy term in seed
|
|
103
108
|
*/
|
|
104
109
|
export interface SeedTaxonomyTerm {
|
|
110
|
+
/** Optional seed-local id, e.g. "term:category:news:en". */
|
|
111
|
+
id?: string;
|
|
105
112
|
slug: string;
|
|
106
113
|
label: string;
|
|
107
114
|
description?: string;
|
|
108
115
|
parent?: string; // Slug of parent term (for hierarchical taxonomies)
|
|
116
|
+
locale?: string;
|
|
117
|
+
translationOf?: string;
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
/**
|
|
112
121
|
* Menu definition in seed
|
|
113
122
|
*/
|
|
114
123
|
export interface SeedMenu {
|
|
124
|
+
/** Optional seed-local id, e.g. "menu:primary:en". */
|
|
125
|
+
id?: string;
|
|
115
126
|
name: string;
|
|
116
127
|
label: string;
|
|
128
|
+
locale?: string;
|
|
129
|
+
translationOf?: string;
|
|
117
130
|
items: SeedMenuItem[];
|
|
118
131
|
}
|
|
119
132
|
|