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/taxonomies/index.ts
CHANGED
|
@@ -1,105 +1,134 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime API for taxonomies
|
|
2
|
+
* Runtime API for taxonomies.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* All helpers are locale-aware. When a locale is not passed explicitly we fall
|
|
5
|
+
* back to the request context or the configured `defaultLocale` (see
|
|
6
|
+
* `i18n/resolve.ts`).
|
|
7
|
+
*
|
|
8
|
+
* Because `content_taxonomies.taxonomy_id` stores the translation_group (not a
|
|
9
|
+
* specific term id), the joins here are `taxonomies.translation_group =
|
|
10
|
+
* content_taxonomies.taxonomy_id` + filter by `taxonomies.locale`, which picks
|
|
11
|
+
* the right per-locale term.
|
|
5
12
|
*/
|
|
6
13
|
|
|
14
|
+
import { resolveLocale, resolveLocaleChain } from "../i18n/resolve.js";
|
|
7
15
|
import { getDb } from "../loader.js";
|
|
8
|
-
import { requestCached, setRequestCacheEntry } from "../request-cache.js";
|
|
16
|
+
import { peekRequestCache, requestCached, setRequestCacheEntry } from "../request-cache.js";
|
|
9
17
|
import { chunks, SQL_BATCH_SIZE } from "../utils/chunks.js";
|
|
10
18
|
import { isMissingTableError } from "../utils/db-errors.js";
|
|
11
19
|
import type { TaxonomyDef, TaxonomyTerm, TaxonomyTermRow } from "./types.js";
|
|
12
20
|
|
|
21
|
+
export interface TaxonomyQueryOptions {
|
|
22
|
+
locale?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
/**
|
|
14
26
|
* No-op — kept for API compatibility.
|
|
15
|
-
*
|
|
16
|
-
* Used to invalidate a worker-lifetime "has any term assignments?" probe.
|
|
17
|
-
* That probe added a query on every cold isolate to save one query on
|
|
18
|
-
* sites with zero term assignments (i.e. the wrong tradeoff), so we
|
|
19
|
-
* dropped it. The batch term join below returns an empty map for empty
|
|
20
|
-
* sites at the same cost as the probe, without the pre-check.
|
|
21
27
|
*/
|
|
22
28
|
export function invalidateTermCache(): void {
|
|
23
29
|
// Intentionally empty.
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
/**
|
|
27
|
-
* Get
|
|
33
|
+
* Get every taxonomy definition. Definitions are per-locale (one row per
|
|
34
|
+
* locale inside the same translation_group) — by default we resolve to the
|
|
35
|
+
* active locale.
|
|
28
36
|
*/
|
|
29
|
-
export async function getTaxonomyDefs(): Promise<TaxonomyDef[]> {
|
|
30
|
-
|
|
37
|
+
export async function getTaxonomyDefs(options: TaxonomyQueryOptions = {}): Promise<TaxonomyDef[]> {
|
|
38
|
+
const locale = resolveLocale(options.locale);
|
|
39
|
+
return requestCached(`taxonomy-defs:${locale ?? "*"}`, async () => {
|
|
31
40
|
const db = await getDb();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return rows.map(
|
|
36
|
-
id: row.id,
|
|
37
|
-
name: row.name,
|
|
38
|
-
label: row.label,
|
|
39
|
-
labelSingular: row.label_singular ?? undefined,
|
|
40
|
-
hierarchical: row.hierarchical === 1,
|
|
41
|
-
collections: row.collections ? JSON.parse(row.collections) : [],
|
|
42
|
-
}));
|
|
41
|
+
let query = db.selectFrom("_emdash_taxonomy_defs").selectAll();
|
|
42
|
+
if (locale !== undefined) query = query.where("locale", "=", locale);
|
|
43
|
+
const rows = await query.execute();
|
|
44
|
+
return rows.map(rowToTaxonomyDef);
|
|
43
45
|
});
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
|
-
* Get a single taxonomy definition by name
|
|
49
|
+
* Get a single taxonomy definition by name. Uses the fallback chain so even
|
|
50
|
+
* if there is no translation for the active locale we still return something.
|
|
51
|
+
*
|
|
52
|
+
* If `getTaxonomyDefs()` has already loaded the full list in this request
|
|
53
|
+
* (which happens during entry-term hydration on every page that renders a
|
|
54
|
+
* collection), search the matching def in memory rather than running a
|
|
55
|
+
* second query against `_emdash_taxonomy_defs`.
|
|
48
56
|
*/
|
|
49
|
-
export async function getTaxonomyDef(
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
export async function getTaxonomyDef(
|
|
58
|
+
name: string,
|
|
59
|
+
options: TaxonomyQueryOptions = {},
|
|
60
|
+
): Promise<TaxonomyDef | null> {
|
|
61
|
+
const chain = resolveLocaleChain(options.locale);
|
|
62
|
+
const peekKey = `taxonomy-defs:${resolveLocale(options.locale) ?? "*"}`;
|
|
63
|
+
const allDefs = peekRequestCache<TaxonomyDef[]>(peekKey);
|
|
64
|
+
if (allDefs) {
|
|
65
|
+
const defs = await allDefs;
|
|
66
|
+
if (chain.length === 0) return defs.find((d) => d.name === name) ?? null;
|
|
67
|
+
for (const locale of chain) {
|
|
68
|
+
const found = defs.find((d) => d.name === name && d.locale === locale);
|
|
69
|
+
if (found) return found;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
52
73
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.selectAll()
|
|
56
|
-
.where("name", "=", name)
|
|
57
|
-
.executeTakeFirst();
|
|
74
|
+
return requestCached(`taxonomy-def:${name}:${chain.join(",")}`, async () => {
|
|
75
|
+
const db = await getDb();
|
|
58
76
|
|
|
59
|
-
if (
|
|
77
|
+
if (chain.length === 0) {
|
|
78
|
+
const row = await db
|
|
79
|
+
.selectFrom("_emdash_taxonomy_defs")
|
|
80
|
+
.selectAll()
|
|
81
|
+
.where("name", "=", name)
|
|
82
|
+
.orderBy("locale", "asc")
|
|
83
|
+
.executeTakeFirst();
|
|
84
|
+
return row ? rowToTaxonomyDef(row) : null;
|
|
85
|
+
}
|
|
60
86
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
for (const locale of chain) {
|
|
88
|
+
const row = await db
|
|
89
|
+
.selectFrom("_emdash_taxonomy_defs")
|
|
90
|
+
.selectAll()
|
|
91
|
+
.where("name", "=", name)
|
|
92
|
+
.where("locale", "=", locale)
|
|
93
|
+
.executeTakeFirst();
|
|
94
|
+
if (row) return rowToTaxonomyDef(row);
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
69
97
|
});
|
|
70
98
|
}
|
|
71
99
|
|
|
72
100
|
/**
|
|
73
|
-
*
|
|
101
|
+
* All terms of a taxonomy in a specific locale (flat for non-hierarchical,
|
|
102
|
+
* tree for hierarchical).
|
|
74
103
|
*/
|
|
75
|
-
export async function getTaxonomyTerms(
|
|
76
|
-
|
|
104
|
+
export async function getTaxonomyTerms(
|
|
105
|
+
taxonomyName: string,
|
|
106
|
+
options: TaxonomyQueryOptions = {},
|
|
107
|
+
): Promise<TaxonomyTerm[]> {
|
|
108
|
+
const locale = resolveLocale(options.locale);
|
|
109
|
+
return requestCached(`taxonomy-terms:${taxonomyName}:${locale ?? "*"}`, async () => {
|
|
77
110
|
const db = await getDb();
|
|
78
111
|
|
|
79
|
-
|
|
80
|
-
const def = await getTaxonomyDef(taxonomyName);
|
|
112
|
+
const def = await getTaxonomyDef(taxonomyName, options);
|
|
81
113
|
if (!def) return [];
|
|
82
114
|
|
|
83
|
-
|
|
84
|
-
const rows = await db
|
|
115
|
+
let termsQuery = db
|
|
85
116
|
.selectFrom("taxonomies")
|
|
86
117
|
.selectAll()
|
|
87
118
|
.where("name", "=", taxonomyName)
|
|
88
|
-
.orderBy("label", "asc")
|
|
89
|
-
|
|
119
|
+
.orderBy("label", "asc");
|
|
120
|
+
if (locale !== undefined) termsQuery = termsQuery.where("locale", "=", locale);
|
|
121
|
+
const rows = await termsQuery.execute();
|
|
90
122
|
|
|
91
|
-
//
|
|
123
|
+
// Counts are keyed by translation_group (what the pivot stores).
|
|
92
124
|
const countsResult = await db
|
|
93
125
|
.selectFrom("content_taxonomies")
|
|
94
126
|
.select(["taxonomy_id"])
|
|
95
127
|
.select((eb) => eb.fn.count<number>("entry_id").as("count"))
|
|
96
128
|
.groupBy("taxonomy_id")
|
|
97
129
|
.execute();
|
|
98
|
-
|
|
99
130
|
const counts = new Map<string, number>();
|
|
100
|
-
for (const row of countsResult)
|
|
101
|
-
counts.set(row.taxonomy_id, row.count);
|
|
102
|
-
}
|
|
131
|
+
for (const row of countsResult) counts.set(row.taxonomy_id, row.count);
|
|
103
132
|
|
|
104
133
|
const flatTerms: TaxonomyTermRow[] = rows.map((row) => ({
|
|
105
134
|
id: row.id,
|
|
@@ -108,12 +137,11 @@ export async function getTaxonomyTerms(taxonomyName: string): Promise<TaxonomyTe
|
|
|
108
137
|
label: row.label,
|
|
109
138
|
parent_id: row.parent_id,
|
|
110
139
|
data: row.data,
|
|
140
|
+
locale: row.locale,
|
|
141
|
+
translation_group: row.translation_group,
|
|
111
142
|
}));
|
|
112
143
|
|
|
113
|
-
|
|
114
|
-
if (def.hierarchical) {
|
|
115
|
-
return buildTree(flatTerms, counts);
|
|
116
|
-
}
|
|
144
|
+
if (def.hierarchical) return buildTree(flatTerms, counts);
|
|
117
145
|
|
|
118
146
|
return flatTerms.map((term) => ({
|
|
119
147
|
id: term.id,
|
|
@@ -121,50 +149,71 @@ export async function getTaxonomyTerms(taxonomyName: string): Promise<TaxonomyTe
|
|
|
121
149
|
slug: term.slug,
|
|
122
150
|
label: term.label,
|
|
123
151
|
children: [],
|
|
124
|
-
count: counts.get(term.id) ?? 0,
|
|
152
|
+
count: counts.get(term.translation_group ?? term.id) ?? 0,
|
|
153
|
+
locale: term.locale,
|
|
154
|
+
translationGroup: term.translation_group,
|
|
125
155
|
}));
|
|
126
156
|
});
|
|
127
157
|
}
|
|
128
158
|
|
|
129
159
|
/**
|
|
130
|
-
* Get a single term by taxonomy
|
|
160
|
+
* Get a single term by (taxonomy, slug). Honours the fallback chain — if the
|
|
161
|
+
* slug exists in a fallback locale, we return that row (useful for deep-linking
|
|
162
|
+
* to a term page when the translation is missing).
|
|
131
163
|
*/
|
|
132
|
-
export async function getTerm(
|
|
164
|
+
export async function getTerm(
|
|
165
|
+
taxonomyName: string,
|
|
166
|
+
slug: string,
|
|
167
|
+
options: TaxonomyQueryOptions = {},
|
|
168
|
+
): Promise<TaxonomyTerm | null> {
|
|
133
169
|
const db = await getDb();
|
|
170
|
+
const chain = resolveLocaleChain(options.locale);
|
|
134
171
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
172
|
+
let row: Awaited<ReturnType<ReturnType<typeof selectTerm>["executeTakeFirst"]>>;
|
|
173
|
+
const selectTerm = () =>
|
|
174
|
+
db
|
|
175
|
+
.selectFrom("taxonomies")
|
|
176
|
+
.selectAll()
|
|
177
|
+
.where("name", "=", taxonomyName)
|
|
178
|
+
.where("slug", "=", slug);
|
|
179
|
+
|
|
180
|
+
if (chain.length === 0) {
|
|
181
|
+
row = await selectTerm().orderBy("locale", "asc").executeTakeFirst();
|
|
182
|
+
} else {
|
|
183
|
+
row = undefined;
|
|
184
|
+
for (const locale of chain) {
|
|
185
|
+
row = await selectTerm().where("locale", "=", locale).executeTakeFirst();
|
|
186
|
+
if (row) break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
141
189
|
|
|
142
190
|
if (!row) return null;
|
|
143
191
|
|
|
144
|
-
// Get entry count
|
|
145
192
|
const countResult = await db
|
|
146
193
|
.selectFrom("content_taxonomies")
|
|
147
194
|
.select((eb) => eb.fn.count<number>("entry_id").as("count"))
|
|
148
|
-
.where("taxonomy_id", "=", row.id)
|
|
195
|
+
.where("taxonomy_id", "=", row.translation_group ?? row.id)
|
|
149
196
|
.executeTakeFirst();
|
|
150
|
-
|
|
151
197
|
const count = countResult?.count ?? 0;
|
|
152
198
|
|
|
153
|
-
|
|
154
|
-
const childRows = await db
|
|
199
|
+
let childrenQuery = db
|
|
155
200
|
.selectFrom("taxonomies")
|
|
156
201
|
.selectAll()
|
|
157
202
|
.where("parent_id", "=", row.id)
|
|
158
|
-
.orderBy("label", "asc")
|
|
159
|
-
|
|
203
|
+
.orderBy("label", "asc");
|
|
204
|
+
const termLocale = row.locale;
|
|
205
|
+
if (termLocale) childrenQuery = childrenQuery.where("locale", "=", termLocale);
|
|
206
|
+
const childRows = await childrenQuery.execute();
|
|
160
207
|
|
|
161
|
-
const children = childRows.map((child) => ({
|
|
208
|
+
const children = childRows.map<TaxonomyTerm>((child) => ({
|
|
162
209
|
id: child.id,
|
|
163
210
|
name: child.name,
|
|
164
211
|
slug: child.slug,
|
|
165
212
|
label: child.label,
|
|
166
213
|
parentId: child.parent_id ?? undefined,
|
|
167
214
|
children: [],
|
|
215
|
+
locale: child.locale,
|
|
216
|
+
translationGroup: child.translation_group,
|
|
168
217
|
}));
|
|
169
218
|
|
|
170
219
|
return {
|
|
@@ -176,89 +225,75 @@ export async function getTerm(taxonomyName: string, slug: string): Promise<Taxon
|
|
|
176
225
|
description: row.data ? JSON.parse(row.data).description : undefined,
|
|
177
226
|
children,
|
|
178
227
|
count,
|
|
228
|
+
locale: row.locale,
|
|
229
|
+
translationGroup: row.translation_group,
|
|
179
230
|
};
|
|
180
231
|
}
|
|
181
232
|
|
|
182
233
|
/**
|
|
183
|
-
*
|
|
234
|
+
* Terms assigned to a content entry, resolved into the active locale. Terms
|
|
235
|
+
* whose translation_group lacks a row in the requested locale are omitted.
|
|
184
236
|
*/
|
|
185
237
|
export function getEntryTerms(
|
|
186
238
|
collection: string,
|
|
187
239
|
entryId: string,
|
|
188
240
|
taxonomyName?: string,
|
|
241
|
+
options: TaxonomyQueryOptions = {},
|
|
189
242
|
): Promise<TaxonomyTerm[]> {
|
|
190
|
-
|
|
191
|
-
|
|
243
|
+
const locale = resolveLocale(options.locale);
|
|
244
|
+
return requestCached(
|
|
245
|
+
`terms:${collection}:${entryId}:${taxonomyName ?? "*"}:${locale ?? "*"}`,
|
|
246
|
+
async () => {
|
|
247
|
+
const db = await getDb();
|
|
192
248
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (taxonomyName) {
|
|
201
|
-
query = query.where("taxonomies.name", "=", taxonomyName);
|
|
202
|
-
}
|
|
249
|
+
let query = db
|
|
250
|
+
.selectFrom("content_taxonomies")
|
|
251
|
+
.innerJoin("taxonomies", "taxonomies.translation_group", "content_taxonomies.taxonomy_id")
|
|
252
|
+
.selectAll("taxonomies")
|
|
253
|
+
.where("content_taxonomies.collection", "=", collection)
|
|
254
|
+
.where("content_taxonomies.entry_id", "=", entryId);
|
|
203
255
|
|
|
204
|
-
|
|
256
|
+
if (taxonomyName) query = query.where("taxonomies.name", "=", taxonomyName);
|
|
257
|
+
if (locale !== undefined) query = query.where("taxonomies.locale", "=", locale);
|
|
205
258
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
259
|
+
const rows = await query.execute();
|
|
260
|
+
return rows.map<TaxonomyTerm>((row) => ({
|
|
261
|
+
id: row.id,
|
|
262
|
+
name: row.name,
|
|
263
|
+
slug: row.slug,
|
|
264
|
+
label: row.label,
|
|
265
|
+
parentId: row.parent_id ?? undefined,
|
|
266
|
+
children: [],
|
|
267
|
+
locale: row.locale,
|
|
268
|
+
translationGroup: row.translation_group,
|
|
269
|
+
}));
|
|
270
|
+
},
|
|
271
|
+
);
|
|
215
272
|
}
|
|
216
273
|
|
|
217
274
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* This is more efficient than calling getEntryTerms for each entry
|
|
221
|
-
* when you need terms for a list of entries.
|
|
222
|
-
*
|
|
223
|
-
* @param collection - The collection type (e.g., "posts")
|
|
224
|
-
* @param entryIds - Array of entry IDs
|
|
225
|
-
* @param taxonomyName - The taxonomy name (e.g., "categories")
|
|
226
|
-
* @returns Map from entry ID to array of terms
|
|
275
|
+
* Terms for multiple entries of one taxonomy, single query.
|
|
227
276
|
*/
|
|
228
277
|
export async function getTermsForEntries(
|
|
229
278
|
collection: string,
|
|
230
279
|
entryIds: string[],
|
|
231
280
|
taxonomyName: string,
|
|
281
|
+
options: TaxonomyQueryOptions = {},
|
|
232
282
|
): Promise<Map<string, TaxonomyTerm[]>> {
|
|
233
283
|
const result = new Map<string, TaxonomyTerm[]>();
|
|
234
|
-
|
|
235
|
-
// Initialize all entry IDs with empty arrays so callers can always
|
|
236
|
-
// expect the key to be present.
|
|
237
284
|
const uniqueIds = [...new Set(entryIds)];
|
|
238
|
-
for (const id of uniqueIds)
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (uniqueIds.length === 0) {
|
|
243
|
-
return result;
|
|
244
|
-
}
|
|
285
|
+
for (const id of uniqueIds) result.set(id, []);
|
|
286
|
+
if (uniqueIds.length === 0) return result;
|
|
245
287
|
|
|
246
288
|
const db = await getDb();
|
|
289
|
+
const locale = resolveLocale(options.locale);
|
|
247
290
|
|
|
248
|
-
// Chunk the IN clause so we stay below D1's ~100 bound-parameter limit
|
|
249
|
-
// (and equivalent limits on other dialects). Matches getContentBylinesMany.
|
|
250
|
-
//
|
|
251
|
-
// Sites with no term assignments get back empty rows for one query —
|
|
252
|
-
// the previous "has any term assignments" probe spent a round-trip on
|
|
253
|
-
// every request to save that single query on empty sites, which is
|
|
254
|
-
// backwards. Pre-migration databases (content_taxonomies missing) fall
|
|
255
|
-
// through to the `isMissingTableError` catch and return empties.
|
|
256
291
|
for (const chunk of chunks(uniqueIds, SQL_BATCH_SIZE)) {
|
|
257
292
|
let rows;
|
|
258
293
|
try {
|
|
259
|
-
|
|
294
|
+
let query = db
|
|
260
295
|
.selectFrom("content_taxonomies")
|
|
261
|
-
.innerJoin("taxonomies", "taxonomies.
|
|
296
|
+
.innerJoin("taxonomies", "taxonomies.translation_group", "content_taxonomies.taxonomy_id")
|
|
262
297
|
.select([
|
|
263
298
|
"content_taxonomies.entry_id",
|
|
264
299
|
"taxonomies.id",
|
|
@@ -266,18 +301,20 @@ export async function getTermsForEntries(
|
|
|
266
301
|
"taxonomies.slug",
|
|
267
302
|
"taxonomies.label",
|
|
268
303
|
"taxonomies.parent_id",
|
|
304
|
+
"taxonomies.locale",
|
|
305
|
+
"taxonomies.translation_group",
|
|
269
306
|
])
|
|
270
307
|
.where("content_taxonomies.collection", "=", collection)
|
|
271
308
|
.where("content_taxonomies.entry_id", "in", chunk)
|
|
272
|
-
.where("taxonomies.name", "=", taxonomyName)
|
|
273
|
-
|
|
309
|
+
.where("taxonomies.name", "=", taxonomyName);
|
|
310
|
+
if (locale !== undefined) query = query.where("taxonomies.locale", "=", locale);
|
|
311
|
+
rows = await query.execute();
|
|
274
312
|
} catch (error) {
|
|
275
313
|
if (isMissingTableError(error)) return result;
|
|
276
314
|
throw error;
|
|
277
315
|
}
|
|
278
316
|
|
|
279
317
|
for (const row of rows) {
|
|
280
|
-
const entryId = row.entry_id;
|
|
281
318
|
const term: TaxonomyTerm = {
|
|
282
319
|
id: row.id,
|
|
283
320
|
name: row.name,
|
|
@@ -285,12 +322,11 @@ export async function getTermsForEntries(
|
|
|
285
322
|
label: row.label,
|
|
286
323
|
parentId: row.parent_id ?? undefined,
|
|
287
324
|
children: [],
|
|
325
|
+
locale: row.locale,
|
|
326
|
+
translationGroup: row.translation_group,
|
|
288
327
|
};
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (terms) {
|
|
292
|
-
terms.push(term);
|
|
293
|
-
}
|
|
328
|
+
const terms = result.get(row.entry_id);
|
|
329
|
+
if (terms) terms.push(term);
|
|
294
330
|
}
|
|
295
331
|
}
|
|
296
332
|
|
|
@@ -298,57 +334,29 @@ export async function getTermsForEntries(
|
|
|
298
334
|
}
|
|
299
335
|
|
|
300
336
|
/**
|
|
301
|
-
* Batch-fetch terms for multiple entries across ALL taxonomies in
|
|
302
|
-
*
|
|
303
|
-
* Returns a Map keyed by entry ID, where each value is a Record keyed by
|
|
304
|
-
* taxonomy name with the matching terms as an array. Used by
|
|
305
|
-
* getEmDashCollection to eagerly hydrate `entry.data.terms` and avoid
|
|
306
|
-
* the N+1 pattern that callers hit when they loop and call getEntryTerms.
|
|
307
|
-
*
|
|
308
|
-
* Pre-migration databases (content_taxonomies missing) return an empty
|
|
309
|
-
* Map — the join falls through to the `isMissingTableError` branch.
|
|
337
|
+
* Batch-fetch terms for multiple entries across ALL taxonomies in one query.
|
|
338
|
+
* Primes the request-cache for subsequent per-entry calls to `getEntryTerms`.
|
|
310
339
|
*/
|
|
311
340
|
export async function getAllTermsForEntries(
|
|
312
341
|
collection: string,
|
|
313
342
|
entryIds: string[],
|
|
343
|
+
options: TaxonomyQueryOptions = {},
|
|
314
344
|
): Promise<Map<string, Record<string, TaxonomyTerm[]>>> {
|
|
315
345
|
const result = new Map<string, Record<string, TaxonomyTerm[]>>();
|
|
316
|
-
|
|
317
|
-
// Initialize unique entry IDs with empty objects so callers can always
|
|
318
|
-
// expect the key to be present. Deduping also reduces wasted bound
|
|
319
|
-
// parameters when a caller accidentally passes duplicates.
|
|
320
346
|
const uniqueIds = [...new Set(entryIds)];
|
|
321
|
-
for (const id of uniqueIds) {
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (uniqueIds.length === 0) {
|
|
326
|
-
return result;
|
|
327
|
-
}
|
|
347
|
+
for (const id of uniqueIds) result.set(id, {});
|
|
348
|
+
if (uniqueIds.length === 0) return result;
|
|
328
349
|
|
|
329
350
|
const db = await getDb();
|
|
351
|
+
const locale = resolveLocale(options.locale);
|
|
352
|
+
const applicableTaxonomyNames = await getCollectionTaxonomyNames(collection, { locale });
|
|
330
353
|
|
|
331
|
-
// Look up which taxonomies apply to this collection. Used below to
|
|
332
|
-
// seed empty arrays for taxonomies the entry has no terms in — so
|
|
333
|
-
// callers (including the pre-populated getEntryTerms cache) get a
|
|
334
|
-
// deterministic `[]` back rather than a cache miss that triggers a DB
|
|
335
|
-
// round-trip just to confirm "no terms".
|
|
336
|
-
const applicableTaxonomyNames = await getCollectionTaxonomyNames(collection);
|
|
337
|
-
|
|
338
|
-
// Chunk the IN clause to stay below D1's ~100 bound-parameter limit
|
|
339
|
-
// (and equivalent limits on other dialects). Matches getContentBylinesMany.
|
|
340
|
-
//
|
|
341
|
-
// Previously we did a separate "has any assignments" probe to skip the
|
|
342
|
-
// join on empty sites. That traded one query per request for a query
|
|
343
|
-
// saved only on empty sites — backwards. Now the join runs directly
|
|
344
|
-
// (returning zero rows cheaply) and pre-migration databases are caught
|
|
345
|
-
// by the `isMissingTableError` branch below.
|
|
346
354
|
for (const chunk of chunks(uniqueIds, SQL_BATCH_SIZE)) {
|
|
347
355
|
let rows;
|
|
348
356
|
try {
|
|
349
|
-
|
|
357
|
+
let query = db
|
|
350
358
|
.selectFrom("content_taxonomies")
|
|
351
|
-
.innerJoin("taxonomies", "taxonomies.
|
|
359
|
+
.innerJoin("taxonomies", "taxonomies.translation_group", "content_taxonomies.taxonomy_id")
|
|
352
360
|
.select([
|
|
353
361
|
"content_taxonomies.entry_id",
|
|
354
362
|
"taxonomies.id",
|
|
@@ -356,15 +364,18 @@ export async function getAllTermsForEntries(
|
|
|
356
364
|
"taxonomies.slug",
|
|
357
365
|
"taxonomies.label",
|
|
358
366
|
"taxonomies.parent_id",
|
|
367
|
+
"taxonomies.locale",
|
|
368
|
+
"taxonomies.translation_group",
|
|
359
369
|
])
|
|
360
370
|
.where("content_taxonomies.collection", "=", collection)
|
|
361
371
|
.where("content_taxonomies.entry_id", "in", chunk)
|
|
362
|
-
.orderBy("taxonomies.label", "asc")
|
|
363
|
-
|
|
372
|
+
.orderBy("taxonomies.label", "asc");
|
|
373
|
+
if (locale !== undefined) query = query.where("taxonomies.locale", "=", locale);
|
|
374
|
+
rows = await query.execute();
|
|
364
375
|
} catch (error) {
|
|
365
376
|
if (isMissingTableError(error)) {
|
|
366
377
|
for (const id of uniqueIds) {
|
|
367
|
-
primeEntryTermsCache(collection, id, {}, applicableTaxonomyNames);
|
|
378
|
+
primeEntryTermsCache(collection, id, {}, applicableTaxonomyNames, locale);
|
|
368
379
|
}
|
|
369
380
|
return result;
|
|
370
381
|
}
|
|
@@ -372,7 +383,6 @@ export async function getAllTermsForEntries(
|
|
|
372
383
|
}
|
|
373
384
|
|
|
374
385
|
for (const row of rows) {
|
|
375
|
-
const entryId = row.entry_id;
|
|
376
386
|
const term: TaxonomyTerm = {
|
|
377
387
|
id: row.id,
|
|
378
388
|
name: row.name,
|
|
@@ -380,25 +390,19 @@ export async function getAllTermsForEntries(
|
|
|
380
390
|
label: row.label,
|
|
381
391
|
parentId: row.parent_id ?? undefined,
|
|
382
392
|
children: [],
|
|
393
|
+
locale: row.locale,
|
|
394
|
+
translationGroup: row.translation_group,
|
|
383
395
|
};
|
|
384
|
-
|
|
385
|
-
const byTaxonomy = result.get(entryId);
|
|
396
|
+
const byTaxonomy = result.get(row.entry_id);
|
|
386
397
|
if (!byTaxonomy) continue;
|
|
387
398
|
const existing = byTaxonomy[row.name];
|
|
388
|
-
if (existing)
|
|
389
|
-
|
|
390
|
-
} else {
|
|
391
|
-
byTaxonomy[row.name] = [term];
|
|
392
|
-
}
|
|
399
|
+
if (existing) existing.push(term);
|
|
400
|
+
else byTaxonomy[row.name] = [term];
|
|
393
401
|
}
|
|
394
402
|
}
|
|
395
403
|
|
|
396
|
-
// Prime the request-scoped cache so legacy callers of getEntryTerms
|
|
397
|
-
// (which still work per-entry) hit the in-memory cache instead of
|
|
398
|
-
// re-querying. This is what gives us the N+1 win in existing templates
|
|
399
|
-
// without requiring them to be rewritten.
|
|
400
404
|
for (const [entryId, byTaxonomy] of result) {
|
|
401
|
-
primeEntryTermsCache(collection, entryId, byTaxonomy, applicableTaxonomyNames);
|
|
405
|
+
primeEntryTermsCache(collection, entryId, byTaxonomy, applicableTaxonomyNames, locale);
|
|
402
406
|
}
|
|
403
407
|
|
|
404
408
|
return result;
|
|
@@ -410,9 +414,12 @@ export async function getAllTermsForEntries(
|
|
|
410
414
|
*
|
|
411
415
|
* Returns an empty list when taxonomies haven't been defined yet.
|
|
412
416
|
*/
|
|
413
|
-
async function getCollectionTaxonomyNames(
|
|
417
|
+
async function getCollectionTaxonomyNames(
|
|
418
|
+
collection: string,
|
|
419
|
+
options: TaxonomyQueryOptions,
|
|
420
|
+
): Promise<string[]> {
|
|
414
421
|
try {
|
|
415
|
-
const defs = await getTaxonomyDefs();
|
|
422
|
+
const defs = await getTaxonomyDefs(options);
|
|
416
423
|
return defs.filter((d) => d.collections.includes(collection)).map((d) => d.name);
|
|
417
424
|
} catch (error) {
|
|
418
425
|
if (isMissingTableError(error)) return [];
|
|
@@ -437,44 +444,64 @@ function primeEntryTermsCache(
|
|
|
437
444
|
entryId: string,
|
|
438
445
|
byTaxonomy: Record<string, TaxonomyTerm[]>,
|
|
439
446
|
applicableTaxonomyNames: string[],
|
|
447
|
+
locale: string | undefined,
|
|
440
448
|
): void {
|
|
441
|
-
|
|
442
|
-
// getEntryTerms(collection, id, "tag") doesn't miss the cache when an
|
|
443
|
-
// entry has no tags.
|
|
449
|
+
const localeKey = locale ?? "*";
|
|
444
450
|
for (const name of applicableTaxonomyNames) {
|
|
445
|
-
setRequestCacheEntry(
|
|
451
|
+
setRequestCacheEntry(
|
|
452
|
+
`terms:${collection}:${entryId}:${name}:${localeKey}`,
|
|
453
|
+
byTaxonomy[name] ?? [],
|
|
454
|
+
);
|
|
446
455
|
}
|
|
447
|
-
// Also seed individual names that show up in data but aren't listed
|
|
448
|
-
// as applicable (e.g. taxonomy reassigned to a different collection
|
|
449
|
-
// since the terms were written).
|
|
450
456
|
for (const [name, terms] of Object.entries(byTaxonomy)) {
|
|
451
|
-
setRequestCacheEntry(`terms:${collection}:${entryId}:${name}`, terms);
|
|
457
|
+
setRequestCacheEntry(`terms:${collection}:${entryId}:${name}:${localeKey}`, terms);
|
|
452
458
|
}
|
|
453
|
-
// Flattened `*` view — all terms across all taxonomies in one array.
|
|
454
459
|
const allTerms = Object.values(byTaxonomy).flat();
|
|
455
|
-
setRequestCacheEntry(`terms:${collection}:${entryId}
|
|
460
|
+
setRequestCacheEntry(`terms:${collection}:${entryId}:*:${localeKey}`, allTerms);
|
|
456
461
|
}
|
|
457
462
|
|
|
458
463
|
/**
|
|
459
|
-
* Get entries by term (
|
|
464
|
+
* Get entries by term. Both the lookup (term slug in the active locale) and
|
|
465
|
+
* the content query respect the active locale.
|
|
460
466
|
*/
|
|
461
467
|
export async function getEntriesByTerm(
|
|
462
468
|
collection: string,
|
|
463
469
|
taxonomyName: string,
|
|
464
470
|
termSlug: string,
|
|
471
|
+
options: TaxonomyQueryOptions = {},
|
|
465
472
|
): Promise<Array<{ id: string; data: Record<string, unknown> }>> {
|
|
466
473
|
const { getEmDashCollection } = await import("../query.js");
|
|
467
474
|
|
|
468
|
-
|
|
469
|
-
// a generic options object with `where` for filtering by taxonomy
|
|
470
|
-
const options: Record<string, unknown> = {
|
|
475
|
+
const queryOptions: Record<string, unknown> = {
|
|
471
476
|
where: { [taxonomyName]: termSlug },
|
|
472
477
|
};
|
|
473
|
-
|
|
474
|
-
|
|
478
|
+
if (options.locale !== undefined) queryOptions.locale = options.locale;
|
|
479
|
+
const { entries } = await getEmDashCollection(collection, queryOptions);
|
|
475
480
|
return entries;
|
|
476
481
|
}
|
|
477
482
|
|
|
483
|
+
function rowToTaxonomyDef(row: {
|
|
484
|
+
id: string;
|
|
485
|
+
name: string;
|
|
486
|
+
label: string;
|
|
487
|
+
label_singular: string | null;
|
|
488
|
+
hierarchical: number;
|
|
489
|
+
collections: string | null;
|
|
490
|
+
locale: string;
|
|
491
|
+
translation_group: string | null;
|
|
492
|
+
}): TaxonomyDef {
|
|
493
|
+
return {
|
|
494
|
+
id: row.id,
|
|
495
|
+
name: row.name,
|
|
496
|
+
label: row.label,
|
|
497
|
+
labelSingular: row.label_singular ?? undefined,
|
|
498
|
+
hierarchical: row.hierarchical === 1,
|
|
499
|
+
collections: row.collections ? JSON.parse(row.collections) : [],
|
|
500
|
+
locale: row.locale,
|
|
501
|
+
translationGroup: row.translation_group,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
478
505
|
/**
|
|
479
506
|
* Build tree structure from flat terms
|
|
480
507
|
*/
|
|
@@ -482,7 +509,6 @@ function buildTree(flatTerms: TaxonomyTermRow[], counts: Map<string, number>): T
|
|
|
482
509
|
const map = new Map<string, TaxonomyTerm>();
|
|
483
510
|
const roots: TaxonomyTerm[] = [];
|
|
484
511
|
|
|
485
|
-
// First pass: create nodes
|
|
486
512
|
for (const term of flatTerms) {
|
|
487
513
|
map.set(term.id, {
|
|
488
514
|
id: term.id,
|
|
@@ -492,11 +518,12 @@ function buildTree(flatTerms: TaxonomyTermRow[], counts: Map<string, number>): T
|
|
|
492
518
|
parentId: term.parent_id ?? undefined,
|
|
493
519
|
description: term.data ? JSON.parse(term.data).description : undefined,
|
|
494
520
|
children: [],
|
|
495
|
-
count: counts.get(term.id) ?? 0,
|
|
521
|
+
count: counts.get(term.translation_group ?? term.id) ?? 0,
|
|
522
|
+
locale: term.locale,
|
|
523
|
+
translationGroup: term.translation_group,
|
|
496
524
|
});
|
|
497
525
|
}
|
|
498
526
|
|
|
499
|
-
// Second pass: build tree
|
|
500
527
|
for (const term of map.values()) {
|
|
501
528
|
if (term.parentId && map.has(term.parentId)) {
|
|
502
529
|
map.get(term.parentId)!.children.push(term);
|