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
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Taxonomy and term CRUD handlers
|
|
2
|
+
* Taxonomy and term CRUD handlers.
|
|
3
|
+
*
|
|
4
|
+
* i18n: terms and defs are per-locale. `(name, slug, locale)` is unique for
|
|
5
|
+
* terms; `(name, locale)` for defs. Translations of the same term/def share a
|
|
6
|
+
* `translation_group`. The content_taxonomies pivot stores translation_groups
|
|
7
|
+
* so assignments span every locale of a post.
|
|
3
8
|
*/
|
|
4
9
|
|
|
5
|
-
import type { Kysely } from "kysely";
|
|
10
|
+
import type { Kysely, Selectable } from "kysely";
|
|
6
11
|
import { ulid } from "ulidx";
|
|
7
12
|
|
|
8
13
|
import { TaxonomyRepository } from "../../database/repositories/taxonomy.js";
|
|
9
|
-
import type { Database } from "../../database/types.js";
|
|
14
|
+
import type { Database, TaxonomyDefTable } from "../../database/types.js";
|
|
10
15
|
import { invalidateTermCache } from "../../taxonomies/index.js";
|
|
11
16
|
import type { ApiResult } from "../types.js";
|
|
12
17
|
|
|
13
|
-
/** Taxonomy name validation pattern: lowercase alphanumeric + underscores, starts with letter */
|
|
14
18
|
const NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
15
19
|
|
|
16
20
|
// ---------------------------------------------------------------------------
|
|
@@ -24,6 +28,8 @@ export interface TaxonomyDef {
|
|
|
24
28
|
labelSingular?: string;
|
|
25
29
|
hierarchical: boolean;
|
|
26
30
|
collections: string[];
|
|
31
|
+
locale: string;
|
|
32
|
+
translationGroup: string | null;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
export interface TaxonomyListResponse {
|
|
@@ -37,6 +43,8 @@ export interface TermData {
|
|
|
37
43
|
label: string;
|
|
38
44
|
parentId: string | null;
|
|
39
45
|
description?: string;
|
|
46
|
+
locale: string;
|
|
47
|
+
translationGroup: string | null;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
export interface TermWithCount extends TermData {
|
|
@@ -59,6 +67,26 @@ export interface TermGetResponse {
|
|
|
59
67
|
};
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
export interface TermTranslationsResponse {
|
|
71
|
+
translationGroup: string | null;
|
|
72
|
+
translations: Array<{
|
|
73
|
+
id: string;
|
|
74
|
+
slug: string;
|
|
75
|
+
label: string;
|
|
76
|
+
locale: string;
|
|
77
|
+
}>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface TaxonomyDefTranslationsResponse {
|
|
81
|
+
translationGroup: string | null;
|
|
82
|
+
translations: Array<{
|
|
83
|
+
id: string;
|
|
84
|
+
name: string;
|
|
85
|
+
label: string;
|
|
86
|
+
locale: string;
|
|
87
|
+
}>;
|
|
88
|
+
}
|
|
89
|
+
|
|
62
90
|
// ---------------------------------------------------------------------------
|
|
63
91
|
// Helpers
|
|
64
92
|
// ---------------------------------------------------------------------------
|
|
@@ -69,11 +97,7 @@ export interface TermGetResponse {
|
|
|
69
97
|
function buildTree(flatTerms: TermWithCount[]): TermWithCount[] {
|
|
70
98
|
const map = new Map<string, TermWithCount>();
|
|
71
99
|
const roots: TermWithCount[] = [];
|
|
72
|
-
|
|
73
|
-
for (const term of flatTerms) {
|
|
74
|
-
map.set(term.id, term);
|
|
75
|
-
}
|
|
76
|
-
|
|
100
|
+
for (const term of flatTerms) map.set(term.id, term);
|
|
77
101
|
for (const term of flatTerms) {
|
|
78
102
|
if (term.parentId && map.has(term.parentId)) {
|
|
79
103
|
map.get(term.parentId)!.children.push(term);
|
|
@@ -81,38 +105,48 @@ function buildTree(flatTerms: TermWithCount[]): TermWithCount[] {
|
|
|
81
105
|
roots.push(term);
|
|
82
106
|
}
|
|
83
107
|
}
|
|
84
|
-
|
|
85
108
|
return roots;
|
|
86
109
|
}
|
|
87
110
|
|
|
88
111
|
/**
|
|
89
|
-
* Look up a taxonomy definition by name
|
|
112
|
+
* Look up a taxonomy definition by name (optionally scoped to a locale).
|
|
113
|
+
* Returns the lowest-locale match when no locale is provided.
|
|
90
114
|
*/
|
|
91
115
|
async function requireTaxonomyDef(
|
|
92
116
|
db: Kysely<Database>,
|
|
93
117
|
name: string,
|
|
118
|
+
locale?: string,
|
|
94
119
|
): Promise<
|
|
95
|
-
| { success: true; def:
|
|
120
|
+
| { success: true; def: Selectable<TaxonomyDefTable> }
|
|
96
121
|
| { success: false; error: { code: string; message: string } }
|
|
97
122
|
> {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.where("name", "=", name)
|
|
102
|
-
.executeTakeFirst();
|
|
103
|
-
|
|
123
|
+
let query = db.selectFrom("_emdash_taxonomy_defs").selectAll().where("name", "=", name);
|
|
124
|
+
if (locale !== undefined) query = query.where("locale", "=", locale);
|
|
125
|
+
const def = await query.orderBy("locale", "asc").executeTakeFirst();
|
|
104
126
|
if (!def) {
|
|
105
127
|
return {
|
|
106
128
|
success: false,
|
|
107
129
|
error: { code: "NOT_FOUND", message: `Taxonomy '${name}' not found` },
|
|
108
130
|
};
|
|
109
131
|
}
|
|
110
|
-
|
|
111
132
|
return { success: true, def };
|
|
112
133
|
}
|
|
113
134
|
|
|
135
|
+
function rowToDef(row: Selectable<TaxonomyDefTable>): TaxonomyDef {
|
|
136
|
+
return {
|
|
137
|
+
id: row.id,
|
|
138
|
+
name: row.name,
|
|
139
|
+
label: row.label,
|
|
140
|
+
labelSingular: row.label_singular ?? undefined,
|
|
141
|
+
hierarchical: row.hierarchical === 1,
|
|
142
|
+
collections: row.collections ? JSON.parse(row.collections) : [],
|
|
143
|
+
locale: row.locale,
|
|
144
|
+
translationGroup: row.translation_group,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
114
148
|
// ---------------------------------------------------------------------------
|
|
115
|
-
//
|
|
149
|
+
// Taxonomy definition handlers
|
|
116
150
|
// ---------------------------------------------------------------------------
|
|
117
151
|
|
|
118
152
|
/**
|
|
@@ -120,10 +154,13 @@ async function requireTaxonomyDef(
|
|
|
120
154
|
*/
|
|
121
155
|
export async function handleTaxonomyList(
|
|
122
156
|
db: Kysely<Database>,
|
|
157
|
+
options: { locale?: string } = {},
|
|
123
158
|
): Promise<ApiResult<TaxonomyListResponse>> {
|
|
124
159
|
try {
|
|
160
|
+
let query = db.selectFrom("_emdash_taxonomy_defs").selectAll();
|
|
161
|
+
if (options.locale !== undefined) query = query.where("locale", "=", options.locale);
|
|
125
162
|
const [rows, collectionRows] = await Promise.all([
|
|
126
|
-
|
|
163
|
+
query.execute(),
|
|
127
164
|
db.selectFrom("_emdash_collections").select("slug").execute(),
|
|
128
165
|
]);
|
|
129
166
|
|
|
@@ -133,15 +170,8 @@ export async function handleTaxonomyList(
|
|
|
133
170
|
const realCollections = new Set(collectionRows.map((r) => r.slug));
|
|
134
171
|
|
|
135
172
|
const taxonomies: TaxonomyDef[] = rows.map((row) => {
|
|
136
|
-
const
|
|
137
|
-
return {
|
|
138
|
-
id: row.id,
|
|
139
|
-
name: row.name,
|
|
140
|
-
label: row.label,
|
|
141
|
-
labelSingular: row.label_singular ?? undefined,
|
|
142
|
-
hierarchical: row.hierarchical === 1,
|
|
143
|
-
collections: stored.filter((slug) => realCollections.has(slug)),
|
|
144
|
-
};
|
|
173
|
+
const def = rowToDef(row);
|
|
174
|
+
return { ...def, collections: def.collections.filter((slug) => realCollections.has(slug)) };
|
|
145
175
|
});
|
|
146
176
|
|
|
147
177
|
return { success: true, data: { taxonomies } };
|
|
@@ -158,10 +188,17 @@ export async function handleTaxonomyList(
|
|
|
158
188
|
*/
|
|
159
189
|
export async function handleTaxonomyCreate(
|
|
160
190
|
db: Kysely<Database>,
|
|
161
|
-
input: {
|
|
191
|
+
input: {
|
|
192
|
+
name: string;
|
|
193
|
+
label: string;
|
|
194
|
+
labelSingular?: string;
|
|
195
|
+
hierarchical?: boolean;
|
|
196
|
+
collections?: string[];
|
|
197
|
+
locale?: string;
|
|
198
|
+
translationOf?: string;
|
|
199
|
+
},
|
|
162
200
|
): Promise<ApiResult<{ taxonomy: TaxonomyDef }>> {
|
|
163
201
|
try {
|
|
164
|
-
// Validate name format
|
|
165
202
|
if (!NAME_PATTERN.test(input.name)) {
|
|
166
203
|
return {
|
|
167
204
|
success: false,
|
|
@@ -174,15 +211,12 @@ export async function handleTaxonomyCreate(
|
|
|
174
211
|
}
|
|
175
212
|
|
|
176
213
|
const collections = [...new Set(input.collections ?? [])];
|
|
177
|
-
|
|
178
|
-
// Validate that referenced collections exist
|
|
179
214
|
if (collections.length > 0) {
|
|
180
215
|
const existingCollections = await db
|
|
181
216
|
.selectFrom("_emdash_collections")
|
|
182
217
|
.select("slug")
|
|
183
218
|
.where("slug", "in", collections)
|
|
184
219
|
.execute();
|
|
185
|
-
|
|
186
220
|
const existingSlugs = new Set(existingCollections.map((c) => c.slug));
|
|
187
221
|
const invalid = collections.filter((c) => !existingSlugs.has(c));
|
|
188
222
|
if (invalid.length > 0) {
|
|
@@ -196,58 +230,68 @@ export async function handleTaxonomyCreate(
|
|
|
196
230
|
}
|
|
197
231
|
}
|
|
198
232
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
233
|
+
let translationGroup: string | null = null;
|
|
234
|
+
if (input.translationOf) {
|
|
235
|
+
const source = await db
|
|
236
|
+
.selectFrom("_emdash_taxonomy_defs")
|
|
237
|
+
.selectAll()
|
|
238
|
+
.where("id", "=", input.translationOf)
|
|
239
|
+
.executeTakeFirst();
|
|
240
|
+
if (!source) {
|
|
241
|
+
return {
|
|
242
|
+
success: false,
|
|
243
|
+
error: { code: "NOT_FOUND", message: "Source taxonomy for translation not found" },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
translationGroup = source.translation_group ?? source.id;
|
|
247
|
+
}
|
|
205
248
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
249
|
+
// Duplicate guard scoped to locale (so the same name can exist in ES
|
|
250
|
+
// and EN).
|
|
251
|
+
if (input.locale !== undefined) {
|
|
252
|
+
const existing = await db
|
|
253
|
+
.selectFrom("_emdash_taxonomy_defs")
|
|
254
|
+
.select("id")
|
|
255
|
+
.where("name", "=", input.name)
|
|
256
|
+
.where("locale", "=", input.locale)
|
|
257
|
+
.executeTakeFirst();
|
|
258
|
+
if (existing) {
|
|
259
|
+
return {
|
|
260
|
+
success: false,
|
|
261
|
+
error: {
|
|
262
|
+
code: "CONFLICT",
|
|
263
|
+
message: `Taxonomy '${input.name}' already exists in locale '${input.locale}'`,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
214
267
|
}
|
|
215
268
|
|
|
216
269
|
const id = ulid();
|
|
217
|
-
|
|
218
270
|
await db
|
|
219
271
|
.insertInto("_emdash_taxonomy_defs")
|
|
220
272
|
.values({
|
|
221
273
|
id,
|
|
222
274
|
name: input.name,
|
|
223
275
|
label: input.label,
|
|
224
|
-
label_singular: null,
|
|
276
|
+
label_singular: input.labelSingular ?? null,
|
|
225
277
|
hierarchical: input.hierarchical ? 1 : 0,
|
|
226
278
|
collections: JSON.stringify(collections),
|
|
279
|
+
...(input.locale !== undefined ? { locale: input.locale } : {}),
|
|
280
|
+
translation_group: translationGroup ?? id,
|
|
227
281
|
})
|
|
228
282
|
.execute();
|
|
229
283
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
label: input.label,
|
|
237
|
-
hierarchical: input.hierarchical ?? false,
|
|
238
|
-
collections,
|
|
239
|
-
},
|
|
240
|
-
},
|
|
241
|
-
};
|
|
284
|
+
const row = await db
|
|
285
|
+
.selectFrom("_emdash_taxonomy_defs")
|
|
286
|
+
.selectAll()
|
|
287
|
+
.where("id", "=", id)
|
|
288
|
+
.executeTakeFirstOrThrow();
|
|
289
|
+
return { success: true, data: { taxonomy: rowToDef(row) } };
|
|
242
290
|
} catch (error) {
|
|
243
|
-
// Handle UNIQUE constraint violation from concurrent duplicate inserts
|
|
244
291
|
if (error instanceof Error && error.message.includes("UNIQUE constraint failed")) {
|
|
245
292
|
return {
|
|
246
293
|
success: false,
|
|
247
|
-
error: {
|
|
248
|
-
code: "CONFLICT",
|
|
249
|
-
message: `Taxonomy '${input.name}' already exists`,
|
|
250
|
-
},
|
|
294
|
+
error: { code: "CONFLICT", message: `Taxonomy '${input.name}' already exists` },
|
|
251
295
|
};
|
|
252
296
|
}
|
|
253
297
|
return {
|
|
@@ -257,26 +301,81 @@ export async function handleTaxonomyCreate(
|
|
|
257
301
|
}
|
|
258
302
|
}
|
|
259
303
|
|
|
304
|
+
/**
|
|
305
|
+
* List every locale translation of a taxonomy def (by id or translation_group).
|
|
306
|
+
*/
|
|
307
|
+
export async function handleTaxonomyDefTranslations(
|
|
308
|
+
db: Kysely<Database>,
|
|
309
|
+
idOrGroup: string,
|
|
310
|
+
): Promise<ApiResult<TaxonomyDefTranslationsResponse>> {
|
|
311
|
+
try {
|
|
312
|
+
const anchor = await db
|
|
313
|
+
.selectFrom("_emdash_taxonomy_defs")
|
|
314
|
+
.selectAll()
|
|
315
|
+
.where((eb) => eb.or([eb("id", "=", idOrGroup), eb("translation_group", "=", idOrGroup)]))
|
|
316
|
+
.executeTakeFirst();
|
|
317
|
+
if (!anchor) {
|
|
318
|
+
return {
|
|
319
|
+
success: false,
|
|
320
|
+
error: { code: "NOT_FOUND", message: "Taxonomy not found" },
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const group = anchor.translation_group ?? anchor.id;
|
|
324
|
+
const rows = await db
|
|
325
|
+
.selectFrom("_emdash_taxonomy_defs")
|
|
326
|
+
.selectAll()
|
|
327
|
+
.where("translation_group", "=", group)
|
|
328
|
+
.orderBy("locale", "asc")
|
|
329
|
+
.execute();
|
|
330
|
+
return {
|
|
331
|
+
success: true,
|
|
332
|
+
data: {
|
|
333
|
+
translationGroup: group,
|
|
334
|
+
translations: rows.map((r) => ({
|
|
335
|
+
id: r.id,
|
|
336
|
+
name: r.name,
|
|
337
|
+
label: r.label,
|
|
338
|
+
locale: r.locale,
|
|
339
|
+
})),
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
} catch {
|
|
343
|
+
return {
|
|
344
|
+
success: false,
|
|
345
|
+
error: {
|
|
346
|
+
code: "TAXONOMY_TRANSLATIONS_ERROR",
|
|
347
|
+
message: "Failed to list taxonomy translations",
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// Term handlers
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
|
|
260
357
|
/**
|
|
261
358
|
* List all terms for a taxonomy (returns tree for hierarchical taxonomies)
|
|
262
359
|
*/
|
|
263
360
|
export async function handleTermList(
|
|
264
361
|
db: Kysely<Database>,
|
|
265
362
|
taxonomyName: string,
|
|
363
|
+
options: { locale?: string } = {},
|
|
266
364
|
): Promise<ApiResult<TermListResponse>> {
|
|
267
365
|
try {
|
|
366
|
+
// Definitions are per-locale but terms aren't bound to the def's locale —
|
|
367
|
+
// just ensure the taxonomy exists somewhere.
|
|
268
368
|
const lookup = await requireTaxonomyDef(db, taxonomyName);
|
|
269
369
|
if (!lookup.success) return lookup;
|
|
270
370
|
|
|
271
371
|
const repo = new TaxonomyRepository(db);
|
|
272
|
-
const terms = await repo.findByName(taxonomyName);
|
|
372
|
+
const terms = await repo.findByName(taxonomyName, { locale: options.locale });
|
|
273
373
|
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
374
|
+
// Batch count entries per term in a single query (replaces N+1 pattern).
|
|
375
|
+
// content_taxonomies.taxonomy_id stores the translation_group, so we
|
|
376
|
+
// look up by group and map back to each term's id.
|
|
377
|
+
const groups = terms.map((t) => t.translationGroup ?? t.id);
|
|
378
|
+
const countsByGroup = await repo.countEntriesForTerms(groups);
|
|
280
379
|
|
|
281
380
|
const termData: TermWithCount[] = terms.map((term) => ({
|
|
282
381
|
id: term.id,
|
|
@@ -286,12 +385,13 @@ export async function handleTermList(
|
|
|
286
385
|
parentId: term.parentId,
|
|
287
386
|
description: typeof term.data?.description === "string" ? term.data.description : undefined,
|
|
288
387
|
children: [],
|
|
289
|
-
count:
|
|
388
|
+
count: countsByGroup.get(term.translationGroup ?? term.id) ?? 0,
|
|
389
|
+
locale: term.locale,
|
|
390
|
+
translationGroup: term.translationGroup,
|
|
290
391
|
}));
|
|
291
392
|
|
|
292
393
|
const isHierarchical = lookup.def.hierarchical === 1;
|
|
293
394
|
const result = isHierarchical ? buildTree(termData) : termData;
|
|
294
|
-
|
|
295
395
|
return { success: true, data: { terms: result } };
|
|
296
396
|
} catch {
|
|
297
397
|
return {
|
|
@@ -385,30 +485,60 @@ async function validateParentTerm(
|
|
|
385
485
|
export async function handleTermCreate(
|
|
386
486
|
db: Kysely<Database>,
|
|
387
487
|
taxonomyName: string,
|
|
388
|
-
input: {
|
|
488
|
+
input: {
|
|
489
|
+
slug: string;
|
|
490
|
+
label: string;
|
|
491
|
+
parentId?: string | null;
|
|
492
|
+
description?: string;
|
|
493
|
+
locale?: string;
|
|
494
|
+
translationOf?: string;
|
|
495
|
+
},
|
|
389
496
|
): Promise<ApiResult<TermResponse>> {
|
|
390
497
|
try {
|
|
498
|
+
// Taxonomy definitions are per-locale, but terms can exist in any locale
|
|
499
|
+
// regardless of whether the def has been translated there. Look up the
|
|
500
|
+
// def across all locales — we only care that it *exists*.
|
|
391
501
|
const lookup = await requireTaxonomyDef(db, taxonomyName);
|
|
392
502
|
if (!lookup.success) return lookup;
|
|
393
503
|
|
|
394
504
|
const repo = new TaxonomyRepository(db);
|
|
395
505
|
|
|
396
506
|
// Coerce empty-string parentId to undefined (treat as "no parent").
|
|
397
|
-
|
|
507
|
+
let parentId =
|
|
398
508
|
input.parentId === "" || input.parentId === undefined ? undefined : input.parentId;
|
|
399
509
|
|
|
400
|
-
//
|
|
401
|
-
const existing = await repo.findBySlug(taxonomyName, input.slug);
|
|
510
|
+
// Conflict check is scoped to locale (per-locale slugs are unique).
|
|
511
|
+
const existing = await repo.findBySlug(taxonomyName, input.slug, input.locale);
|
|
402
512
|
if (existing) {
|
|
403
513
|
return {
|
|
404
514
|
success: false,
|
|
405
515
|
error: {
|
|
406
516
|
code: "CONFLICT",
|
|
407
|
-
message:
|
|
517
|
+
message: input.locale
|
|
518
|
+
? `Term '${input.slug}' already exists in '${taxonomyName}' (${input.locale})`
|
|
519
|
+
: `Term with slug '${input.slug}' already exists in taxonomy '${taxonomyName}'`,
|
|
408
520
|
},
|
|
409
521
|
};
|
|
410
522
|
}
|
|
411
523
|
|
|
524
|
+
// If creating a translation whose parent is the translated sibling of
|
|
525
|
+
// the source's parent, try to resolve the parent in the same locale.
|
|
526
|
+
if (input.translationOf && parentId) {
|
|
527
|
+
const source = await repo.findById(input.translationOf);
|
|
528
|
+
if (source?.parentId === parentId && input.locale) {
|
|
529
|
+
const sourceParent = await repo.findById(parentId);
|
|
530
|
+
if (sourceParent?.translationGroup) {
|
|
531
|
+
const translatedParent = await db
|
|
532
|
+
.selectFrom("taxonomies")
|
|
533
|
+
.select("id")
|
|
534
|
+
.where("translation_group", "=", sourceParent.translationGroup)
|
|
535
|
+
.where("locale", "=", input.locale)
|
|
536
|
+
.executeTakeFirst();
|
|
537
|
+
if (translatedParent) parentId = translatedParent.id;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
412
542
|
// Validate parentId: must exist AND belong to the same taxonomy.
|
|
413
543
|
// (Cycle check is N/A on create — the term doesn't exist yet.)
|
|
414
544
|
const parentError = await validateParentTerm(repo, taxonomyName, undefined, parentId);
|
|
@@ -422,10 +552,10 @@ export async function handleTermCreate(
|
|
|
422
552
|
label: input.label,
|
|
423
553
|
parentId: parentId ?? undefined,
|
|
424
554
|
data: input.description ? { description: input.description } : undefined,
|
|
555
|
+
locale: input.locale,
|
|
556
|
+
translationOf: input.translationOf,
|
|
425
557
|
});
|
|
426
558
|
|
|
427
|
-
// New term means `hasAnyTermAssignments` may flip from false->true next
|
|
428
|
-
// time an entry is tagged. Clear the cache so the next read re-probes.
|
|
429
559
|
invalidateTermCache();
|
|
430
560
|
|
|
431
561
|
return {
|
|
@@ -439,6 +569,8 @@ export async function handleTermCreate(
|
|
|
439
569
|
parentId: term.parentId,
|
|
440
570
|
description:
|
|
441
571
|
typeof term.data?.description === "string" ? term.data.description : undefined,
|
|
572
|
+
locale: term.locale,
|
|
573
|
+
translationGroup: term.translationGroup,
|
|
442
574
|
},
|
|
443
575
|
},
|
|
444
576
|
};
|
|
@@ -457,10 +589,11 @@ export async function handleTermGet(
|
|
|
457
589
|
db: Kysely<Database>,
|
|
458
590
|
taxonomyName: string,
|
|
459
591
|
termSlug: string,
|
|
592
|
+
options: { locale?: string } = {},
|
|
460
593
|
): Promise<ApiResult<TermGetResponse>> {
|
|
461
594
|
try {
|
|
462
595
|
const repo = new TaxonomyRepository(db);
|
|
463
|
-
const term = await repo.findBySlug(taxonomyName, termSlug);
|
|
596
|
+
const term = await repo.findBySlug(taxonomyName, termSlug, options.locale);
|
|
464
597
|
|
|
465
598
|
if (!term) {
|
|
466
599
|
return {
|
|
@@ -487,11 +620,9 @@ export async function handleTermGet(
|
|
|
487
620
|
description:
|
|
488
621
|
typeof term.data?.description === "string" ? term.data.description : undefined,
|
|
489
622
|
count,
|
|
490
|
-
children: children.map((c) => ({
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
label: c.label,
|
|
494
|
-
})),
|
|
623
|
+
children: children.map((c) => ({ id: c.id, slug: c.slug, label: c.label })),
|
|
624
|
+
locale: term.locale,
|
|
625
|
+
translationGroup: term.translationGroup,
|
|
495
626
|
},
|
|
496
627
|
},
|
|
497
628
|
};
|
|
@@ -503,6 +634,50 @@ export async function handleTermGet(
|
|
|
503
634
|
}
|
|
504
635
|
}
|
|
505
636
|
|
|
637
|
+
/** List every translation of a term (by id or translation_group). */
|
|
638
|
+
export async function handleTermTranslations(
|
|
639
|
+
db: Kysely<Database>,
|
|
640
|
+
idOrGroup: string,
|
|
641
|
+
): Promise<ApiResult<TermTranslationsResponse>> {
|
|
642
|
+
try {
|
|
643
|
+
const anchor = await db
|
|
644
|
+
.selectFrom("taxonomies")
|
|
645
|
+
.selectAll()
|
|
646
|
+
.where((eb) => eb.or([eb("id", "=", idOrGroup), eb("translation_group", "=", idOrGroup)]))
|
|
647
|
+
.executeTakeFirst();
|
|
648
|
+
if (!anchor) {
|
|
649
|
+
return {
|
|
650
|
+
success: false,
|
|
651
|
+
error: { code: "NOT_FOUND", message: "Term not found" },
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
const group = anchor.translation_group ?? anchor.id;
|
|
655
|
+
const rows = await db
|
|
656
|
+
.selectFrom("taxonomies")
|
|
657
|
+
.selectAll()
|
|
658
|
+
.where("translation_group", "=", group)
|
|
659
|
+
.orderBy("locale", "asc")
|
|
660
|
+
.execute();
|
|
661
|
+
return {
|
|
662
|
+
success: true,
|
|
663
|
+
data: {
|
|
664
|
+
translationGroup: group,
|
|
665
|
+
translations: rows.map((r) => ({
|
|
666
|
+
id: r.id,
|
|
667
|
+
slug: r.slug,
|
|
668
|
+
label: r.label,
|
|
669
|
+
locale: r.locale,
|
|
670
|
+
})),
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
} catch {
|
|
674
|
+
return {
|
|
675
|
+
success: false,
|
|
676
|
+
error: { code: "TERM_TRANSLATIONS_ERROR", message: "Failed to list term translations" },
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
506
681
|
/**
|
|
507
682
|
* Update a term
|
|
508
683
|
*/
|
|
@@ -511,10 +686,11 @@ export async function handleTermUpdate(
|
|
|
511
686
|
taxonomyName: string,
|
|
512
687
|
termSlug: string,
|
|
513
688
|
input: { slug?: string; label?: string; parentId?: string | null; description?: string },
|
|
689
|
+
options: { locale?: string } = {},
|
|
514
690
|
): Promise<ApiResult<TermResponse>> {
|
|
515
691
|
try {
|
|
516
692
|
const repo = new TaxonomyRepository(db);
|
|
517
|
-
const term = await repo.findBySlug(taxonomyName, termSlug);
|
|
693
|
+
const term = await repo.findBySlug(taxonomyName, termSlug, options.locale);
|
|
518
694
|
|
|
519
695
|
if (!term) {
|
|
520
696
|
return {
|
|
@@ -532,9 +708,9 @@ export async function handleTermUpdate(
|
|
|
532
708
|
const newParentId =
|
|
533
709
|
input.parentId === "" || input.parentId === undefined ? undefined : input.parentId;
|
|
534
710
|
|
|
535
|
-
// Check if new slug conflicts
|
|
711
|
+
// Check if new slug conflicts (per-locale uniqueness).
|
|
536
712
|
if (newSlug !== undefined && newSlug !== termSlug) {
|
|
537
|
-
const existing = await repo.findBySlug(taxonomyName, newSlug);
|
|
713
|
+
const existing = await repo.findBySlug(taxonomyName, newSlug, options.locale);
|
|
538
714
|
if (existing && existing.id !== term.id) {
|
|
539
715
|
return {
|
|
540
716
|
success: false,
|
|
@@ -559,8 +735,6 @@ export async function handleTermUpdate(
|
|
|
559
735
|
data: input.description !== undefined ? { description: input.description } : undefined,
|
|
560
736
|
});
|
|
561
737
|
|
|
562
|
-
// Term label/slug changes are reflected in hydrated entry.data.terms —
|
|
563
|
-
// invalidate so the next read doesn't short-circuit on a stale probe.
|
|
564
738
|
invalidateTermCache();
|
|
565
739
|
|
|
566
740
|
if (!updated) {
|
|
@@ -581,6 +755,8 @@ export async function handleTermUpdate(
|
|
|
581
755
|
parentId: updated.parentId,
|
|
582
756
|
description:
|
|
583
757
|
typeof updated.data?.description === "string" ? updated.data.description : undefined,
|
|
758
|
+
locale: updated.locale,
|
|
759
|
+
translationGroup: updated.translationGroup,
|
|
584
760
|
},
|
|
585
761
|
},
|
|
586
762
|
};
|
|
@@ -599,10 +775,11 @@ export async function handleTermDelete(
|
|
|
599
775
|
db: Kysely<Database>,
|
|
600
776
|
taxonomyName: string,
|
|
601
777
|
termSlug: string,
|
|
778
|
+
options: { locale?: string } = {},
|
|
602
779
|
): Promise<ApiResult<{ deleted: true }>> {
|
|
603
780
|
try {
|
|
604
781
|
const repo = new TaxonomyRepository(db);
|
|
605
|
-
const term = await repo.findBySlug(taxonomyName, termSlug);
|
|
782
|
+
const term = await repo.findBySlug(taxonomyName, termSlug, options.locale);
|
|
606
783
|
|
|
607
784
|
if (!term) {
|
|
608
785
|
return {
|
|
@@ -614,7 +791,6 @@ export async function handleTermDelete(
|
|
|
614
791
|
};
|
|
615
792
|
}
|
|
616
793
|
|
|
617
|
-
// Prevent deletion of terms with children
|
|
618
794
|
const children = await repo.findChildren(term.id);
|
|
619
795
|
if (children.length > 0) {
|
|
620
796
|
return {
|
|
@@ -634,10 +810,7 @@ export async function handleTermDelete(
|
|
|
634
810
|
};
|
|
635
811
|
}
|
|
636
812
|
|
|
637
|
-
// Deleting a term cascades to content_taxonomies; invalidate so
|
|
638
|
-
// hydration no longer sees the stale assignments.
|
|
639
813
|
invalidateTermCache();
|
|
640
|
-
|
|
641
814
|
return { success: true, data: { deleted: true } };
|
|
642
815
|
} catch {
|
|
643
816
|
return {
|