emdash 0.9.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-DoNJiveC.d.mts → adapters-BktHA7EO.d.mts} +1 -1
- package/dist/{adapters-DoNJiveC.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
- package/dist/{apply-BzltprvY.mjs → apply-UsrFuO7l.mjs} +156 -254
- package/dist/apply-UsrFuO7l.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.mjs +10 -2
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.mjs +5 -5
- package/dist/astro/middleware/redirect.mjs +5 -5
- package/dist/astro/middleware/request-context.mjs +4 -4
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.mjs +35 -34
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +8 -9
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{base64-BRICGH2l.mjs → base64-MBPo9ozB.mjs} +1 -1
- package/dist/{base64-BRICGH2l.mjs.map → base64-MBPo9ozB.mjs.map} +1 -1
- package/dist/{byline-BSaNL1w7.mjs → byline-C3vnhIpU.mjs} +4 -4
- package/dist/{byline-BSaNL1w7.mjs.map → byline-C3vnhIpU.mjs.map} +1 -1
- package/dist/{bylines-CvJ3PYz2.mjs → bylines-esI7ioa9.mjs} +5 -5
- package/dist/{bylines-CvJ3PYz2.mjs.map → bylines-esI7ioa9.mjs.map} +1 -1
- package/dist/{cache-C6N_hhN7.mjs → cache-fTzxgMFJ.mjs} +3 -3
- package/dist/{cache-C6N_hhN7.mjs.map → cache-fTzxgMFJ.mjs.map} +1 -1
- package/dist/{chunks-NBQVDOci.mjs → chunks-Da2-b-oA.mjs} +2 -2
- package/dist/{chunks-NBQVDOci.mjs.map → chunks-Da2-b-oA.mjs.map} +1 -1
- package/dist/cli/index.mjs +251 -79
- 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 +1 -1
- package/dist/{config-BI0V3ICQ.mjs → config-CVssduLe.mjs} +1 -1
- package/dist/{config-BI0V3ICQ.mjs.map → config-CVssduLe.mjs.map} +1 -1
- package/dist/{content-8lOYF0pr.mjs → content-C7G4QXkK.mjs} +14 -3
- package/dist/content-C7G4QXkK.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{db-errors-WRezodiz.mjs → db-errors-B7P2pSCn.mjs} +1 -1
- package/dist/{db-errors-WRezodiz.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
- package/dist/{default-D8ksjWhO.mjs → default-pHuz9WF6.mjs} +1 -1
- package/dist/{default-D8ksjWhO.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
- package/dist/{error-D_-tqP-I.mjs → error-DqnRMM5z.mjs} +1 -1
- package/dist/{error-D_-tqP-I.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
- package/dist/{index-BFRaVcD6.d.mts → index-DjPMOfO0.d.mts} +82 -67
- package/dist/index-DjPMOfO0.d.mts.map +1 -0
- package/dist/index.d.mts +10 -10
- package/dist/index.mjs +28 -27
- package/dist/{load-DDqMMvZL.mjs → load-sXRuM7Us.mjs} +2 -2
- package/dist/{load-DDqMMvZL.mjs.map → load-sXRuM7Us.mjs.map} +1 -1
- package/dist/{loader-CKLbBnhK.mjs → loader-Bx2_9-5e.mjs} +31 -6
- package/dist/loader-Bx2_9-5e.mjs.map +1 -0
- package/dist/{manifest-schema-DqWNC3lM.mjs → manifest-schema-CXAbd1vH.mjs} +1 -1
- package/dist/{manifest-schema-DqWNC3lM.mjs.map → manifest-schema-CXAbd1vH.mjs.map} +1 -1
- 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/media/local-runtime.mjs +3 -3
- package/dist/{media-BW32b4gi.mjs → media-D8FbNsl0.mjs} +2 -2
- package/dist/{media-BW32b4gi.mjs.map → media-D8FbNsl0.mjs.map} +1 -1
- package/dist/{mode-ier8jbBk.mjs → mode-YhqNVef_.mjs} +1 -1
- package/dist/{mode-ier8jbBk.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
- package/dist/{options-BVp3UsTS.mjs → options-nPxWnrya.mjs} +1 -1
- package/dist/{options-BVp3UsTS.mjs.map → options-nPxWnrya.mjs.map} +1 -1
- 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-BE4o_2dc.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
- package/dist/{placeholder-BE4o_2dc.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
- package/dist/{placeholder-CIJejMlK.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
- package/dist/{placeholder-CIJejMlK.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
- package/dist/{public-url-DByxYjUw.mjs → public-url-B1AxbbbQ.mjs} +1 -1
- package/dist/{public-url-DByxYjUw.mjs.map → public-url-B1AxbbbQ.mjs.map} +1 -1
- package/dist/{query-Cg9ZKRQ0.mjs → query-Bo-msrmu.mjs} +13 -13
- package/dist/{query-Cg9ZKRQ0.mjs.map → query-Bo-msrmu.mjs.map} +1 -1
- package/dist/{redirect-BhUBKRc1.mjs → redirect-C5H7VGIX.mjs} +3 -3
- package/dist/{redirect-BhUBKRc1.mjs.map → redirect-C5H7VGIX.mjs.map} +1 -1
- package/dist/{registry-Dw70ChxB.mjs → registry-Beb7wxFc.mjs} +5 -5
- package/dist/{registry-Dw70ChxB.mjs.map → registry-Beb7wxFc.mjs.map} +1 -1
- package/dist/{request-cache-B-bmkipQ.mjs → request-cache-C-tIpYIw.mjs} +1 -1
- package/dist/{request-cache-B-bmkipQ.mjs.map → request-cache-C-tIpYIw.mjs.map} +1 -1
- package/dist/{runner-Bnoj7vjK.d.mts → runner-Clwe4Mme.d.mts} +2 -2
- package/dist/{runner-Bnoj7vjK.d.mts.map → runner-Clwe4Mme.d.mts.map} +1 -1
- package/dist/{runner-C7ADox5q.mjs → runner-DMnlIkh4.mjs} +433 -138
- package/dist/runner-DMnlIkh4.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +3 -3
- package/dist/{search-dOGEccMa.mjs → search-DkN-BqsS.mjs} +164 -92
- package/dist/search-DkN-BqsS.mjs.map +1 -0
- package/dist/{secrets-CW3reAnU.mjs → secrets-CZ8rxLX3.mjs} +3 -3
- package/dist/{secrets-CW3reAnU.mjs.map → secrets-CZ8rxLX3.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +15 -14
- 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-D7zMmWi2.mjs → tokens-CyRDPVW2.mjs} +2 -2
- package/dist/{tokens-D7zMmWi2.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-DNEfeMaU.d.mts → transport-DX_5rpsq.d.mts} +1 -1
- package/dist/{transport-DNEfeMaU.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
- package/dist/{transport-BeMCmin1.mjs → transport-xpzIjCIB.mjs} +1 -1
- package/dist/{transport-BeMCmin1.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
- package/dist/{types-CRxNbK-Z.mjs → types-BIgulNsW.mjs} +2 -2
- package/dist/{types-CRxNbK-Z.mjs.map → types-BIgulNsW.mjs.map} +1 -1
- package/dist/{types-CJsYGpco.d.mts → types-B_CXXnzh.d.mts} +1 -1
- package/dist/{types-CJsYGpco.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
- package/dist/{types-M78DQ1lx.d.mts → types-C-aFbqmA.d.mts} +1 -1
- package/dist/{types-M78DQ1lx.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
- package/dist/{types-4fVtCIm0.mjs → types-CoO6mpV3.mjs} +1 -1
- package/dist/{types-4fVtCIm0.mjs.map → types-CoO6mpV3.mjs.map} +1 -1
- package/dist/{types-BuBIptGk.d.mts → types-D19uBYWn.d.mts} +149 -4
- package/dist/types-D19uBYWn.d.mts.map +1 -0
- package/dist/{types-BSyXeCFW.d.mts → types-Dl1fgFjn.d.mts} +1 -1
- package/dist/{types-BSyXeCFW.d.mts.map → types-Dl1fgFjn.d.mts.map} +1 -1
- package/dist/{types-CrtWgIvl.d.mts → types-Dtx1mSMX.d.mts} +9 -1
- package/dist/types-Dtx1mSMX.d.mts.map +1 -0
- package/dist/{types-CIOg5AR8.mjs → types-Eg829jj9.mjs} +1 -1
- package/dist/{types-CIOg5AR8.mjs.map → types-Eg829jj9.mjs.map} +1 -1
- package/dist/{types-CDbKp7ND.mjs → types-K-EkEQCI.mjs} +1 -1
- package/dist/{types-CDbKp7ND.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
- package/dist/{validate-Baqf0slj.mjs → validate-CBIbxM3L.mjs} +14 -10
- package/dist/validate-CBIbxM3L.mjs.map +1 -0
- package/dist/{validate-BfQh_C_y.d.mts → validate-DHGwADqO.d.mts} +18 -5
- package/dist/validate-DHGwADqO.d.mts.map +1 -0
- package/dist/{validation-BfEI7tNe.mjs → validation-B1NYiEos.mjs} +5 -5
- package/dist/{validation-BfEI7tNe.mjs.map → validation-B1NYiEos.mjs.map} +1 -1
- package/dist/version-CMD42IRC.mjs +7 -0
- package/dist/{version-DoxrVdYf.mjs.map → version-CMD42IRC.mjs.map} +1 -1
- package/dist/{zod-generator-CC0xNe_K.mjs → zod-generator-BNJDQBSZ.mjs} +8 -3
- package/dist/zod-generator-BNJDQBSZ.mjs.map +1 -0
- package/package.json +6 -6
- package/src/api/handlers/content.ts +11 -0
- package/src/api/handlers/dashboard.ts +29 -36
- package/src/api/handlers/menus.ts +256 -75
- package/src/api/handlers/taxonomies.ts +273 -97
- package/src/api/schemas/common.ts +7 -0
- package/src/api/schemas/menus.ts +23 -0
- package/src/api/schemas/taxonomies.ts +39 -0
- package/src/astro/integration/routes.ts +10 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
- 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/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/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 -6
- package/src/cli/commands/export-seed.ts +82 -21
- package/src/cli/commands/plugin-init.ts +216 -90
- package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/content.ts +11 -0
- package/src/database/repositories/taxonomy.ts +193 -89
- package/src/database/types.ts +10 -2
- package/src/i18n/resolve.ts +37 -0
- package/src/loader.ts +49 -2
- package/src/mcp/server.ts +77 -18
- package/src/menus/index.ts +143 -124
- package/src/menus/types.ts +15 -1
- package/src/schema/zod-generator.ts +12 -2
- package/src/seed/apply.ts +140 -54
- package/src/seed/types.ts +14 -1
- package/src/seed/validate.ts +27 -13
- package/src/taxonomies/index.ts +230 -213
- package/src/taxonomies/types.ts +10 -0
- package/dist/apply-BzltprvY.mjs.map +0 -1
- package/dist/content-8lOYF0pr.mjs.map +0 -1
- package/dist/index-BFRaVcD6.d.mts.map +0 -1
- package/dist/loader-CKLbBnhK.mjs.map +0 -1
- package/dist/runner-C7ADox5q.mjs.map +0 -1
- package/dist/search-dOGEccMa.mjs.map +0 -1
- package/dist/taxonomies-ZlRtD6AG.mjs +0 -315
- package/dist/taxonomies-ZlRtD6AG.mjs.map +0 -1
- package/dist/types-BuBIptGk.d.mts.map +0 -1
- package/dist/types-CrtWgIvl.d.mts.map +0 -1
- package/dist/validate-Baqf0slj.mjs.map +0 -1
- package/dist/validate-BfQh_C_y.d.mts.map +0 -1
- package/dist/version-DoxrVdYf.mjs +0 -7
- package/dist/zod-generator-CC0xNe_K.mjs.map +0 -1
|
@@ -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,23 +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
|
-
// Batch count entries per term in a single query (replaces N+1 pattern)
|
|
275
|
-
|
|
276
|
-
|
|
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);
|
|
277
379
|
|
|
278
380
|
const termData: TermWithCount[] = terms.map((term) => ({
|
|
279
381
|
id: term.id,
|
|
@@ -283,12 +385,13 @@ export async function handleTermList(
|
|
|
283
385
|
parentId: term.parentId,
|
|
284
386
|
description: typeof term.data?.description === "string" ? term.data.description : undefined,
|
|
285
387
|
children: [],
|
|
286
|
-
count:
|
|
388
|
+
count: countsByGroup.get(term.translationGroup ?? term.id) ?? 0,
|
|
389
|
+
locale: term.locale,
|
|
390
|
+
translationGroup: term.translationGroup,
|
|
287
391
|
}));
|
|
288
392
|
|
|
289
393
|
const isHierarchical = lookup.def.hierarchical === 1;
|
|
290
394
|
const result = isHierarchical ? buildTree(termData) : termData;
|
|
291
|
-
|
|
292
395
|
return { success: true, data: { terms: result } };
|
|
293
396
|
} catch {
|
|
294
397
|
return {
|
|
@@ -382,30 +485,60 @@ async function validateParentTerm(
|
|
|
382
485
|
export async function handleTermCreate(
|
|
383
486
|
db: Kysely<Database>,
|
|
384
487
|
taxonomyName: string,
|
|
385
|
-
input: {
|
|
488
|
+
input: {
|
|
489
|
+
slug: string;
|
|
490
|
+
label: string;
|
|
491
|
+
parentId?: string | null;
|
|
492
|
+
description?: string;
|
|
493
|
+
locale?: string;
|
|
494
|
+
translationOf?: string;
|
|
495
|
+
},
|
|
386
496
|
): Promise<ApiResult<TermResponse>> {
|
|
387
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*.
|
|
388
501
|
const lookup = await requireTaxonomyDef(db, taxonomyName);
|
|
389
502
|
if (!lookup.success) return lookup;
|
|
390
503
|
|
|
391
504
|
const repo = new TaxonomyRepository(db);
|
|
392
505
|
|
|
393
506
|
// Coerce empty-string parentId to undefined (treat as "no parent").
|
|
394
|
-
|
|
507
|
+
let parentId =
|
|
395
508
|
input.parentId === "" || input.parentId === undefined ? undefined : input.parentId;
|
|
396
509
|
|
|
397
|
-
//
|
|
398
|
-
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);
|
|
399
512
|
if (existing) {
|
|
400
513
|
return {
|
|
401
514
|
success: false,
|
|
402
515
|
error: {
|
|
403
516
|
code: "CONFLICT",
|
|
404
|
-
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}'`,
|
|
405
520
|
},
|
|
406
521
|
};
|
|
407
522
|
}
|
|
408
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
|
+
|
|
409
542
|
// Validate parentId: must exist AND belong to the same taxonomy.
|
|
410
543
|
// (Cycle check is N/A on create — the term doesn't exist yet.)
|
|
411
544
|
const parentError = await validateParentTerm(repo, taxonomyName, undefined, parentId);
|
|
@@ -419,10 +552,10 @@ export async function handleTermCreate(
|
|
|
419
552
|
label: input.label,
|
|
420
553
|
parentId: parentId ?? undefined,
|
|
421
554
|
data: input.description ? { description: input.description } : undefined,
|
|
555
|
+
locale: input.locale,
|
|
556
|
+
translationOf: input.translationOf,
|
|
422
557
|
});
|
|
423
558
|
|
|
424
|
-
// New term means `hasAnyTermAssignments` may flip from false->true next
|
|
425
|
-
// time an entry is tagged. Clear the cache so the next read re-probes.
|
|
426
559
|
invalidateTermCache();
|
|
427
560
|
|
|
428
561
|
return {
|
|
@@ -436,6 +569,8 @@ export async function handleTermCreate(
|
|
|
436
569
|
parentId: term.parentId,
|
|
437
570
|
description:
|
|
438
571
|
typeof term.data?.description === "string" ? term.data.description : undefined,
|
|
572
|
+
locale: term.locale,
|
|
573
|
+
translationGroup: term.translationGroup,
|
|
439
574
|
},
|
|
440
575
|
},
|
|
441
576
|
};
|
|
@@ -454,10 +589,11 @@ export async function handleTermGet(
|
|
|
454
589
|
db: Kysely<Database>,
|
|
455
590
|
taxonomyName: string,
|
|
456
591
|
termSlug: string,
|
|
592
|
+
options: { locale?: string } = {},
|
|
457
593
|
): Promise<ApiResult<TermGetResponse>> {
|
|
458
594
|
try {
|
|
459
595
|
const repo = new TaxonomyRepository(db);
|
|
460
|
-
const term = await repo.findBySlug(taxonomyName, termSlug);
|
|
596
|
+
const term = await repo.findBySlug(taxonomyName, termSlug, options.locale);
|
|
461
597
|
|
|
462
598
|
if (!term) {
|
|
463
599
|
return {
|
|
@@ -484,11 +620,9 @@ export async function handleTermGet(
|
|
|
484
620
|
description:
|
|
485
621
|
typeof term.data?.description === "string" ? term.data.description : undefined,
|
|
486
622
|
count,
|
|
487
|
-
children: children.map((c) => ({
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
label: c.label,
|
|
491
|
-
})),
|
|
623
|
+
children: children.map((c) => ({ id: c.id, slug: c.slug, label: c.label })),
|
|
624
|
+
locale: term.locale,
|
|
625
|
+
translationGroup: term.translationGroup,
|
|
492
626
|
},
|
|
493
627
|
},
|
|
494
628
|
};
|
|
@@ -500,6 +634,50 @@ export async function handleTermGet(
|
|
|
500
634
|
}
|
|
501
635
|
}
|
|
502
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
|
+
|
|
503
681
|
/**
|
|
504
682
|
* Update a term
|
|
505
683
|
*/
|
|
@@ -508,10 +686,11 @@ export async function handleTermUpdate(
|
|
|
508
686
|
taxonomyName: string,
|
|
509
687
|
termSlug: string,
|
|
510
688
|
input: { slug?: string; label?: string; parentId?: string | null; description?: string },
|
|
689
|
+
options: { locale?: string } = {},
|
|
511
690
|
): Promise<ApiResult<TermResponse>> {
|
|
512
691
|
try {
|
|
513
692
|
const repo = new TaxonomyRepository(db);
|
|
514
|
-
const term = await repo.findBySlug(taxonomyName, termSlug);
|
|
693
|
+
const term = await repo.findBySlug(taxonomyName, termSlug, options.locale);
|
|
515
694
|
|
|
516
695
|
if (!term) {
|
|
517
696
|
return {
|
|
@@ -529,9 +708,9 @@ export async function handleTermUpdate(
|
|
|
529
708
|
const newParentId =
|
|
530
709
|
input.parentId === "" || input.parentId === undefined ? undefined : input.parentId;
|
|
531
710
|
|
|
532
|
-
// Check if new slug conflicts
|
|
711
|
+
// Check if new slug conflicts (per-locale uniqueness).
|
|
533
712
|
if (newSlug !== undefined && newSlug !== termSlug) {
|
|
534
|
-
const existing = await repo.findBySlug(taxonomyName, newSlug);
|
|
713
|
+
const existing = await repo.findBySlug(taxonomyName, newSlug, options.locale);
|
|
535
714
|
if (existing && existing.id !== term.id) {
|
|
536
715
|
return {
|
|
537
716
|
success: false,
|
|
@@ -556,8 +735,6 @@ export async function handleTermUpdate(
|
|
|
556
735
|
data: input.description !== undefined ? { description: input.description } : undefined,
|
|
557
736
|
});
|
|
558
737
|
|
|
559
|
-
// Term label/slug changes are reflected in hydrated entry.data.terms —
|
|
560
|
-
// invalidate so the next read doesn't short-circuit on a stale probe.
|
|
561
738
|
invalidateTermCache();
|
|
562
739
|
|
|
563
740
|
if (!updated) {
|
|
@@ -578,6 +755,8 @@ export async function handleTermUpdate(
|
|
|
578
755
|
parentId: updated.parentId,
|
|
579
756
|
description:
|
|
580
757
|
typeof updated.data?.description === "string" ? updated.data.description : undefined,
|
|
758
|
+
locale: updated.locale,
|
|
759
|
+
translationGroup: updated.translationGroup,
|
|
581
760
|
},
|
|
582
761
|
},
|
|
583
762
|
};
|
|
@@ -596,10 +775,11 @@ export async function handleTermDelete(
|
|
|
596
775
|
db: Kysely<Database>,
|
|
597
776
|
taxonomyName: string,
|
|
598
777
|
termSlug: string,
|
|
778
|
+
options: { locale?: string } = {},
|
|
599
779
|
): Promise<ApiResult<{ deleted: true }>> {
|
|
600
780
|
try {
|
|
601
781
|
const repo = new TaxonomyRepository(db);
|
|
602
|
-
const term = await repo.findBySlug(taxonomyName, termSlug);
|
|
782
|
+
const term = await repo.findBySlug(taxonomyName, termSlug, options.locale);
|
|
603
783
|
|
|
604
784
|
if (!term) {
|
|
605
785
|
return {
|
|
@@ -611,7 +791,6 @@ export async function handleTermDelete(
|
|
|
611
791
|
};
|
|
612
792
|
}
|
|
613
793
|
|
|
614
|
-
// Prevent deletion of terms with children
|
|
615
794
|
const children = await repo.findChildren(term.id);
|
|
616
795
|
if (children.length > 0) {
|
|
617
796
|
return {
|
|
@@ -631,10 +810,7 @@ export async function handleTermDelete(
|
|
|
631
810
|
};
|
|
632
811
|
}
|
|
633
812
|
|
|
634
|
-
// Deleting a term cascades to content_taxonomies; invalidate so
|
|
635
|
-
// hydration no longer sees the stale assignments.
|
|
636
813
|
invalidateTermCache();
|
|
637
|
-
|
|
638
814
|
return { success: true, data: { deleted: true } };
|
|
639
815
|
} catch {
|
|
640
816
|
return {
|
|
@@ -59,6 +59,13 @@ export const localeCode = z
|
|
|
59
59
|
.regex(/^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i, "Invalid locale code")
|
|
60
60
|
.transform((v) => v.toLowerCase());
|
|
61
61
|
|
|
62
|
+
/** Shared `?locale=xx` query shape for endpoints that filter by locale. */
|
|
63
|
+
export const localeFilterQuery = z
|
|
64
|
+
.object({
|
|
65
|
+
locale: z.string().min(1).optional(),
|
|
66
|
+
})
|
|
67
|
+
.meta({ id: "LocaleFilterQuery" });
|
|
68
|
+
|
|
62
69
|
// ---------------------------------------------------------------------------
|
|
63
70
|
// OpenAPI: Shared response schemas
|
|
64
71
|
// ---------------------------------------------------------------------------
|