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
package/src/menus/types.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface Menu {
|
|
|
24
24
|
name: string;
|
|
25
25
|
label: string;
|
|
26
26
|
items: MenuItem[];
|
|
27
|
+
locale: string;
|
|
28
|
+
translationGroup: string | null;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
@@ -36,13 +38,15 @@ export interface MenuItemRow {
|
|
|
36
38
|
sort_order: number;
|
|
37
39
|
type: MenuItemType;
|
|
38
40
|
reference_collection: string | null;
|
|
39
|
-
reference_id: string | null;
|
|
41
|
+
reference_id: string | null; // translation_group of referenced content/term
|
|
40
42
|
custom_url: string | null;
|
|
41
43
|
label: string;
|
|
42
44
|
title_attr: string | null;
|
|
43
45
|
target: string | null;
|
|
44
46
|
css_classes: string | null;
|
|
45
47
|
created_at: string;
|
|
48
|
+
locale: string;
|
|
49
|
+
translation_group: string | null;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
/**
|
|
@@ -54,6 +58,8 @@ export interface MenuRow {
|
|
|
54
58
|
label: string;
|
|
55
59
|
created_at: string;
|
|
56
60
|
updated_at: string;
|
|
61
|
+
locale: string;
|
|
62
|
+
translation_group: string | null;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
/**
|
|
@@ -62,6 +68,11 @@ export interface MenuRow {
|
|
|
62
68
|
export interface CreateMenuItemInput {
|
|
63
69
|
type: MenuItemType;
|
|
64
70
|
label: string;
|
|
71
|
+
/**
|
|
72
|
+
* Identifier of the referenced entity. For `reference_collection` items it is
|
|
73
|
+
* the content's translation_group (locale-agnostic); for `taxonomy` items it
|
|
74
|
+
* is the term's translation_group.
|
|
75
|
+
*/
|
|
65
76
|
referenceCollection?: string;
|
|
66
77
|
referenceId?: string;
|
|
67
78
|
customUrl?: string;
|
|
@@ -91,6 +102,9 @@ export interface UpdateMenuItemInput {
|
|
|
91
102
|
export interface CreateMenuInput {
|
|
92
103
|
name: string;
|
|
93
104
|
label: string;
|
|
105
|
+
locale?: string;
|
|
106
|
+
/** When set, links the new menu into an existing translation_group. */
|
|
107
|
+
translationOf?: string;
|
|
94
108
|
}
|
|
95
109
|
|
|
96
110
|
/**
|
|
@@ -131,6 +131,12 @@ function getBaseSchema(type: FieldType, field: Field): ZodTypeAny {
|
|
|
131
131
|
alt: z.string().optional(),
|
|
132
132
|
width: z.number().optional(),
|
|
133
133
|
height: z.number().optional(),
|
|
134
|
+
/** Provider ID (e.g. "local", "cloudflare-images") */
|
|
135
|
+
provider: z.string().optional(),
|
|
136
|
+
/** Admin-side preview URL for external providers (not persisted by plugins) */
|
|
137
|
+
previewUrl: z.string().optional(),
|
|
138
|
+
/** Provider-specific metadata; for local media this carries storageKey */
|
|
139
|
+
meta: z.record(z.string(), z.unknown()).optional(),
|
|
134
140
|
});
|
|
135
141
|
|
|
136
142
|
case "file":
|
|
@@ -140,6 +146,10 @@ function getBaseSchema(type: FieldType, field: Field): ZodTypeAny {
|
|
|
140
146
|
filename: z.string().optional(),
|
|
141
147
|
mimeType: z.string().optional(),
|
|
142
148
|
size: z.number().optional(),
|
|
149
|
+
/** Provider ID (e.g. "local", "s3") */
|
|
150
|
+
provider: z.string().optional(),
|
|
151
|
+
/** Provider-specific metadata; for local media this carries storageKey */
|
|
152
|
+
meta: z.record(z.string(), z.unknown()).optional(),
|
|
143
153
|
});
|
|
144
154
|
|
|
145
155
|
case "reference":
|
|
@@ -384,10 +394,10 @@ function fieldTypeToTypeScript(field: Field): string {
|
|
|
384
394
|
return "PortableTextBlock[]";
|
|
385
395
|
|
|
386
396
|
case "image":
|
|
387
|
-
return "{ id: string; src?: string; alt?: string; width?: number; height?: number }";
|
|
397
|
+
return "{ id: string; src?: string; alt?: string; width?: number; height?: number; provider?: string; previewUrl?: string; meta?: Record<string, unknown> }";
|
|
388
398
|
|
|
389
399
|
case "file":
|
|
390
|
-
return "{ id: string; src?: string; filename?: string; mimeType?: string; size?: number }";
|
|
400
|
+
return "{ id: string; src?: string; filename?: string; mimeType?: string; size?: number; provider?: string; meta?: Record<string, unknown> }";
|
|
391
401
|
|
|
392
402
|
case "reference":
|
|
393
403
|
// Could be enhanced to include the referenced collection type
|
package/src/seed/apply.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { TaxonomyRepository } from "../database/repositories/taxonomy.js";
|
|
|
19
19
|
import { withTransaction } from "../database/transaction.js";
|
|
20
20
|
import type { Database } from "../database/types.js";
|
|
21
21
|
import type { MediaValue } from "../fields/types.js";
|
|
22
|
+
import { getI18nConfig } from "../i18n/config.js";
|
|
22
23
|
import { ssrfSafeFetch, validateExternalUrl } from "../import/ssrf.js";
|
|
23
24
|
import { SchemaRegistry } from "../schema/registry.js";
|
|
24
25
|
import { FTSManager } from "../search/fts-manager.js";
|
|
@@ -219,17 +220,30 @@ export async function applySeed(
|
|
|
219
220
|
|
|
220
221
|
// 4-5. Taxonomies
|
|
221
222
|
if (seed.taxonomies) {
|
|
223
|
+
// seed-local id -> resolved info, used to wire `translationOf` refs.
|
|
224
|
+
const defSeedIdMap = new Map<string, { id: string; translationGroup: string }>();
|
|
225
|
+
const termSeedIdMap = new Map<string, string>();
|
|
226
|
+
const fallbackLocale = getI18nConfig()?.defaultLocale ?? "en";
|
|
227
|
+
|
|
222
228
|
for (const taxonomy of seed.taxonomies) {
|
|
223
|
-
|
|
229
|
+
const defLocale = taxonomy.locale ?? fallbackLocale;
|
|
230
|
+
|
|
231
|
+
// (name, locale) is the UNIQUE key after migration 036.
|
|
224
232
|
const existingDef = await db
|
|
225
233
|
.selectFrom("_emdash_taxonomy_defs")
|
|
226
234
|
.selectAll()
|
|
227
235
|
.where("name", "=", taxonomy.name)
|
|
236
|
+
.where("locale", "=", defLocale)
|
|
228
237
|
.executeTakeFirst();
|
|
229
238
|
|
|
239
|
+
let defId: string;
|
|
240
|
+
let defTranslationGroup: string;
|
|
241
|
+
|
|
230
242
|
if (existingDef) {
|
|
243
|
+
defId = existingDef.id;
|
|
244
|
+
defTranslationGroup = existingDef.translation_group ?? existingDef.id;
|
|
231
245
|
if (onConflict === "error") {
|
|
232
|
-
throw new Error(`Conflict: taxonomy "${taxonomy.name}" already exists`);
|
|
246
|
+
throw new Error(`Conflict: taxonomy "${taxonomy.name}" (${defLocale}) already exists`);
|
|
233
247
|
}
|
|
234
248
|
if (onConflict === "update") {
|
|
235
249
|
await db
|
|
@@ -242,40 +256,59 @@ export async function applySeed(
|
|
|
242
256
|
})
|
|
243
257
|
.where("id", "=", existingDef.id)
|
|
244
258
|
.execute();
|
|
245
|
-
// Taxonomy defs don't track an "updated" counter -- just the definition is updated
|
|
246
259
|
}
|
|
247
|
-
// skip: do nothing for the definition
|
|
248
260
|
} else {
|
|
249
|
-
|
|
261
|
+
defId = ulid();
|
|
262
|
+
defTranslationGroup = defId;
|
|
263
|
+
if (taxonomy.translationOf) {
|
|
264
|
+
const source = defSeedIdMap.get(taxonomy.translationOf);
|
|
265
|
+
if (source) defTranslationGroup = source.translationGroup;
|
|
266
|
+
else
|
|
267
|
+
console.warn(
|
|
268
|
+
`taxonomy "${taxonomy.name}" (${defLocale}): translationOf "${taxonomy.translationOf}" not found yet; minting a fresh group.`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
250
271
|
await db
|
|
251
272
|
.insertInto("_emdash_taxonomy_defs")
|
|
252
273
|
.values({
|
|
253
|
-
id:
|
|
274
|
+
id: defId,
|
|
254
275
|
name: taxonomy.name,
|
|
255
276
|
label: taxonomy.label,
|
|
256
277
|
label_singular: taxonomy.labelSingular ?? null,
|
|
257
278
|
hierarchical: taxonomy.hierarchical ? 1 : 0,
|
|
258
279
|
collections: JSON.stringify(taxonomy.collections),
|
|
280
|
+
locale: defLocale,
|
|
281
|
+
translation_group: defTranslationGroup,
|
|
259
282
|
})
|
|
260
283
|
.execute();
|
|
261
284
|
result.taxonomies.created++;
|
|
262
285
|
}
|
|
263
286
|
|
|
287
|
+
if (taxonomy.id)
|
|
288
|
+
defSeedIdMap.set(taxonomy.id, { id: defId, translationGroup: defTranslationGroup });
|
|
289
|
+
|
|
264
290
|
// Create terms (if provided)
|
|
265
291
|
if (taxonomy.terms && taxonomy.terms.length > 0) {
|
|
266
292
|
const termRepo = new TaxonomyRepository(db);
|
|
267
293
|
|
|
268
|
-
// For hierarchical taxonomies, we need to create parents before children
|
|
269
294
|
if (taxonomy.hierarchical) {
|
|
270
|
-
await applyHierarchicalTerms(
|
|
295
|
+
await applyHierarchicalTerms(
|
|
296
|
+
termRepo,
|
|
297
|
+
taxonomy.name,
|
|
298
|
+
defLocale,
|
|
299
|
+
taxonomy.terms,
|
|
300
|
+
termSeedIdMap,
|
|
301
|
+
result,
|
|
302
|
+
onConflict,
|
|
303
|
+
);
|
|
271
304
|
} else {
|
|
272
|
-
// Flat taxonomy - create all terms
|
|
273
305
|
for (const term of taxonomy.terms) {
|
|
274
|
-
const
|
|
306
|
+
const termLocale = term.locale ?? defLocale;
|
|
307
|
+
const existing = await termRepo.findBySlug(taxonomy.name, term.slug, termLocale);
|
|
275
308
|
if (existing) {
|
|
276
309
|
if (onConflict === "error") {
|
|
277
310
|
throw new Error(
|
|
278
|
-
`Conflict: taxonomy term "${term.slug}" in "${taxonomy.name}" already exists`,
|
|
311
|
+
`Conflict: taxonomy term "${term.slug}" in "${taxonomy.name}" (${termLocale}) already exists`,
|
|
279
312
|
);
|
|
280
313
|
}
|
|
281
314
|
if (onConflict === "update") {
|
|
@@ -285,14 +318,20 @@ export async function applySeed(
|
|
|
285
318
|
});
|
|
286
319
|
result.taxonomies.terms++;
|
|
287
320
|
}
|
|
288
|
-
|
|
321
|
+
if (term.id) termSeedIdMap.set(term.id, existing.id);
|
|
289
322
|
} else {
|
|
290
|
-
|
|
323
|
+
const translationOf = term.translationOf
|
|
324
|
+
? termSeedIdMap.get(term.translationOf)
|
|
325
|
+
: undefined;
|
|
326
|
+
const created = await termRepo.create({
|
|
291
327
|
name: taxonomy.name,
|
|
292
328
|
slug: term.slug,
|
|
293
329
|
label: term.label,
|
|
294
330
|
data: term.description ? { description: term.description } : undefined,
|
|
331
|
+
locale: termLocale,
|
|
332
|
+
translationOf,
|
|
295
333
|
});
|
|
334
|
+
if (term.id) termSeedIdMap.set(term.id, created.id);
|
|
296
335
|
result.taxonomies.terms++;
|
|
297
336
|
}
|
|
298
337
|
}
|
|
@@ -471,23 +510,39 @@ export async function applySeed(
|
|
|
471
510
|
|
|
472
511
|
// 8. Menus and Menu Items (after content so refs can resolve)
|
|
473
512
|
if (seed.menus) {
|
|
513
|
+
// seed-local id -> resolved info, used to wire `translationOf` refs.
|
|
514
|
+
const menuSeedIdMap = new Map<string, { id: string; translationGroup: string }>();
|
|
515
|
+
const fallbackLocale = getI18nConfig()?.defaultLocale ?? "en";
|
|
516
|
+
|
|
474
517
|
for (const menu of seed.menus) {
|
|
475
|
-
|
|
476
|
-
|
|
518
|
+
const locale = menu.locale ?? fallbackLocale;
|
|
519
|
+
let lookup = db
|
|
477
520
|
.selectFrom("_emdash_menus")
|
|
478
521
|
.selectAll()
|
|
479
522
|
.where("name", "=", menu.name)
|
|
480
|
-
.
|
|
523
|
+
.where("locale", "=", locale);
|
|
524
|
+
const existingMenu = await lookup.executeTakeFirst();
|
|
481
525
|
|
|
482
526
|
let menuId: string;
|
|
527
|
+
let translationGroup: string;
|
|
483
528
|
|
|
484
529
|
if (existingMenu) {
|
|
485
530
|
menuId = existingMenu.id;
|
|
531
|
+
translationGroup = existingMenu.translation_group ?? existingMenu.id;
|
|
486
532
|
// Clear existing items (menus are recreated)
|
|
487
533
|
await db.deleteFrom("_emdash_menu_items").where("menu_id", "=", menuId).execute();
|
|
488
534
|
} else {
|
|
489
|
-
// Create menu
|
|
490
535
|
menuId = ulid();
|
|
536
|
+
// Resolve translationOf to the source menu's translation_group.
|
|
537
|
+
translationGroup = menuId;
|
|
538
|
+
if (menu.translationOf) {
|
|
539
|
+
const source = menuSeedIdMap.get(menu.translationOf);
|
|
540
|
+
if (source) translationGroup = source.translationGroup;
|
|
541
|
+
else
|
|
542
|
+
console.warn(
|
|
543
|
+
`menu "${menu.name}" (${locale}): translationOf "${menu.translationOf}" not found yet; minting a fresh group.`,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
491
546
|
await db
|
|
492
547
|
.insertInto("_emdash_menus")
|
|
493
548
|
.values({
|
|
@@ -496,15 +551,20 @@ export async function applySeed(
|
|
|
496
551
|
label: menu.label,
|
|
497
552
|
created_at: new Date().toISOString(),
|
|
498
553
|
updated_at: new Date().toISOString(),
|
|
554
|
+
locale,
|
|
555
|
+
translation_group: translationGroup,
|
|
499
556
|
})
|
|
500
557
|
.execute();
|
|
501
558
|
result.menus.created++;
|
|
502
559
|
}
|
|
503
560
|
|
|
561
|
+
if (menu.id) menuSeedIdMap.set(menu.id, { id: menuId, translationGroup });
|
|
562
|
+
|
|
504
563
|
// Create menu items
|
|
505
564
|
const itemCount = await applyMenuItems(
|
|
506
565
|
db,
|
|
507
566
|
menuId,
|
|
567
|
+
locale,
|
|
508
568
|
menu.items,
|
|
509
569
|
null, // parent_id
|
|
510
570
|
0, // sort_order
|
|
@@ -692,64 +752,75 @@ export async function applySeed(
|
|
|
692
752
|
async function applyHierarchicalTerms(
|
|
693
753
|
termRepo: TaxonomyRepository,
|
|
694
754
|
taxonomyName: string,
|
|
755
|
+
defLocale: string,
|
|
695
756
|
terms: SeedTaxonomyTerm[],
|
|
757
|
+
termSeedIdMap: Map<string, string>,
|
|
696
758
|
result: SeedApplyResult,
|
|
697
759
|
onConflict: "skip" | "update" | "error" = "skip",
|
|
698
760
|
): Promise<void> {
|
|
699
|
-
//
|
|
761
|
+
// "locale::slug" -> id, so the same slug can resolve per locale.
|
|
700
762
|
const slugToId = new Map<string, string>();
|
|
701
763
|
|
|
702
|
-
// Multiple passes
|
|
764
|
+
// Multiple passes — handles deep nesting and translationOf forward refs.
|
|
703
765
|
let remaining = [...terms];
|
|
704
|
-
let maxPasses = 10;
|
|
766
|
+
let maxPasses = 10;
|
|
705
767
|
|
|
706
768
|
while (remaining.length > 0 && maxPasses > 0) {
|
|
707
769
|
const processedThisPass: string[] = [];
|
|
708
770
|
|
|
709
771
|
for (const term of remaining) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
772
|
+
const termLocale = term.locale ?? defLocale;
|
|
773
|
+
const parentReady = !term.parent || slugToId.has(`${termLocale}::${term.parent}`);
|
|
774
|
+
const translationReady = !term.translationOf || termSeedIdMap.has(term.translationOf);
|
|
713
775
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
slugToId.set(term.slug, existing.id);
|
|
730
|
-
} else {
|
|
731
|
-
const created = await termRepo.create({
|
|
732
|
-
name: taxonomyName,
|
|
733
|
-
slug: term.slug,
|
|
776
|
+
if (!parentReady || !translationReady) continue;
|
|
777
|
+
|
|
778
|
+
const parentId = term.parent ? slugToId.get(`${termLocale}::${term.parent}`) : undefined;
|
|
779
|
+
const translationOf = term.translationOf ? termSeedIdMap.get(term.translationOf) : undefined;
|
|
780
|
+
|
|
781
|
+
const existing = await termRepo.findBySlug(taxonomyName, term.slug, termLocale);
|
|
782
|
+
if (existing) {
|
|
783
|
+
if (onConflict === "error") {
|
|
784
|
+
throw new Error(
|
|
785
|
+
`Conflict: taxonomy term "${term.slug}" in "${taxonomyName}" (${termLocale}) already exists`,
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
if (onConflict === "update") {
|
|
789
|
+
await termRepo.update(existing.id, {
|
|
734
790
|
label: term.label,
|
|
735
791
|
parentId,
|
|
736
|
-
data: term.description ? { description: term.description } :
|
|
792
|
+
data: term.description ? { description: term.description } : {},
|
|
737
793
|
});
|
|
738
|
-
slugToId.set(term.slug, created.id);
|
|
739
794
|
result.taxonomies.terms++;
|
|
740
795
|
}
|
|
741
|
-
|
|
742
|
-
|
|
796
|
+
slugToId.set(`${termLocale}::${term.slug}`, existing.id);
|
|
797
|
+
if (term.id) termSeedIdMap.set(term.id, existing.id);
|
|
798
|
+
} else {
|
|
799
|
+
const created = await termRepo.create({
|
|
800
|
+
name: taxonomyName,
|
|
801
|
+
slug: term.slug,
|
|
802
|
+
label: term.label,
|
|
803
|
+
parentId,
|
|
804
|
+
data: term.description ? { description: term.description } : undefined,
|
|
805
|
+
locale: termLocale,
|
|
806
|
+
translationOf,
|
|
807
|
+
});
|
|
808
|
+
slugToId.set(`${termLocale}::${term.slug}`, created.id);
|
|
809
|
+
if (term.id) termSeedIdMap.set(term.id, created.id);
|
|
810
|
+
result.taxonomies.terms++;
|
|
743
811
|
}
|
|
812
|
+
|
|
813
|
+
processedThisPass.push(term.slug + "::" + termLocale);
|
|
744
814
|
}
|
|
745
815
|
|
|
746
|
-
|
|
747
|
-
|
|
816
|
+
remaining = remaining.filter(
|
|
817
|
+
(t) => !processedThisPass.includes(t.slug + "::" + (t.locale ?? defLocale)),
|
|
818
|
+
);
|
|
748
819
|
maxPasses--;
|
|
749
820
|
}
|
|
750
821
|
|
|
751
822
|
if (remaining.length > 0) {
|
|
752
|
-
console.warn(`Could not process ${remaining.length} terms due to missing parents`);
|
|
823
|
+
console.warn(`Could not process ${remaining.length} terms due to missing parents/translations`);
|
|
753
824
|
}
|
|
754
825
|
}
|
|
755
826
|
|
|
@@ -847,11 +918,18 @@ async function applyContentTaxonomies(
|
|
|
847
918
|
}
|
|
848
919
|
|
|
849
920
|
/**
|
|
850
|
-
* Apply menu items recursively
|
|
921
|
+
* Apply menu items recursively.
|
|
922
|
+
*
|
|
923
|
+
* Each item gets a fresh `translation_group` (= its own id). The seed format's
|
|
924
|
+
* `SeedMenuItem` has no `id`/`translationOf` fields, so we can't express the
|
|
925
|
+
* cross-locale "same nav entry" link here — items diverge across locales on
|
|
926
|
+
* re-apply. Runtime navigation still resolves correctly because `reference_id`
|
|
927
|
+
* already holds the content's translation_group.
|
|
851
928
|
*/
|
|
852
929
|
async function applyMenuItems(
|
|
853
930
|
db: Kysely<Database>,
|
|
854
931
|
menuId: string,
|
|
932
|
+
locale: string,
|
|
855
933
|
items: SeedMenuItem[],
|
|
856
934
|
parentId: string | null,
|
|
857
935
|
startOrder: number,
|
|
@@ -877,7 +955,6 @@ async function applyMenuItems(
|
|
|
877
955
|
// If not in map, the content might not exist yet (will be broken link)
|
|
878
956
|
}
|
|
879
957
|
|
|
880
|
-
// Insert menu item
|
|
881
958
|
await db
|
|
882
959
|
.insertInto("_emdash_menu_items")
|
|
883
960
|
.values({
|
|
@@ -894,15 +971,24 @@ async function applyMenuItems(
|
|
|
894
971
|
target: item.target ?? null,
|
|
895
972
|
css_classes: item.cssClasses ?? null,
|
|
896
973
|
created_at: new Date().toISOString(),
|
|
974
|
+
locale,
|
|
975
|
+
translation_group: itemId,
|
|
897
976
|
})
|
|
898
977
|
.execute();
|
|
899
978
|
|
|
900
979
|
count++;
|
|
901
980
|
order++;
|
|
902
981
|
|
|
903
|
-
// Process children
|
|
904
982
|
if (item.children && item.children.length > 0) {
|
|
905
|
-
const childCount = await applyMenuItems(
|
|
983
|
+
const childCount = await applyMenuItems(
|
|
984
|
+
db,
|
|
985
|
+
menuId,
|
|
986
|
+
locale,
|
|
987
|
+
item.children,
|
|
988
|
+
itemId,
|
|
989
|
+
0,
|
|
990
|
+
seedIdMap,
|
|
991
|
+
);
|
|
906
992
|
count += childCount;
|
|
907
993
|
}
|
|
908
994
|
}
|
package/src/seed/types.ts
CHANGED
|
@@ -87,14 +87,19 @@ export interface SeedField {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* Taxonomy definition in seed
|
|
90
|
+
* Taxonomy definition in seed. For multi-locale exports each locale variant
|
|
91
|
+
* is its own entry, linked via `translationOf` (referencing another entry's `id`).
|
|
91
92
|
*/
|
|
92
93
|
export interface SeedTaxonomy {
|
|
94
|
+
/** Optional seed-local id, e.g. "tax:category:en". Target of `translationOf`. */
|
|
95
|
+
id?: string;
|
|
93
96
|
name: string;
|
|
94
97
|
label: string;
|
|
95
98
|
labelSingular?: string;
|
|
96
99
|
hierarchical: boolean;
|
|
97
100
|
collections: string[];
|
|
101
|
+
locale?: string;
|
|
102
|
+
translationOf?: string;
|
|
98
103
|
terms?: SeedTaxonomyTerm[];
|
|
99
104
|
}
|
|
100
105
|
|
|
@@ -102,18 +107,26 @@ export interface SeedTaxonomy {
|
|
|
102
107
|
* Taxonomy term in seed
|
|
103
108
|
*/
|
|
104
109
|
export interface SeedTaxonomyTerm {
|
|
110
|
+
/** Optional seed-local id, e.g. "term:category:news:en". */
|
|
111
|
+
id?: string;
|
|
105
112
|
slug: string;
|
|
106
113
|
label: string;
|
|
107
114
|
description?: string;
|
|
108
115
|
parent?: string; // Slug of parent term (for hierarchical taxonomies)
|
|
116
|
+
locale?: string;
|
|
117
|
+
translationOf?: string;
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
/**
|
|
112
121
|
* Menu definition in seed
|
|
113
122
|
*/
|
|
114
123
|
export interface SeedMenu {
|
|
124
|
+
/** Optional seed-local id, e.g. "menu:primary:en". */
|
|
125
|
+
id?: string;
|
|
115
126
|
name: string;
|
|
116
127
|
label: string;
|
|
128
|
+
locale?: string;
|
|
129
|
+
translationOf?: string;
|
|
117
130
|
items: SeedMenuItem[];
|
|
118
131
|
}
|
|
119
132
|
|
package/src/seed/validate.ts
CHANGED
|
@@ -147,11 +147,16 @@ export function validateSeed(data: unknown): ValidationResult {
|
|
|
147
147
|
if (!taxonomy.name) {
|
|
148
148
|
errors.push(`${prefix}: name is required`);
|
|
149
149
|
} else {
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
// Uniqueness is per (name, locale).
|
|
151
|
+
const key = `${taxonomy.name}::${taxonomy.locale ?? ""}`;
|
|
152
|
+
if (taxonomyNames.has(key)) {
|
|
153
|
+
errors.push(
|
|
154
|
+
taxonomy.locale
|
|
155
|
+
? `${prefix}.name: duplicate taxonomy "${taxonomy.name}" in locale "${taxonomy.locale}"`
|
|
156
|
+
: `${prefix}.name: duplicate taxonomy name "${taxonomy.name}"`,
|
|
157
|
+
);
|
|
153
158
|
}
|
|
154
|
-
taxonomyNames.add(
|
|
159
|
+
taxonomyNames.add(key);
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
if (!taxonomy.label) {
|
|
@@ -184,13 +189,15 @@ export function validateSeed(data: unknown): ValidationResult {
|
|
|
184
189
|
if (!term.slug) {
|
|
185
190
|
errors.push(`${termPrefix}: slug is required`);
|
|
186
191
|
} else {
|
|
187
|
-
//
|
|
188
|
-
|
|
192
|
+
// Uniqueness is per (slug, locale) so the same slug can repeat
|
|
193
|
+
// across locale variants of the def.
|
|
194
|
+
const key = `${term.slug}::${term.locale ?? taxonomy.locale ?? ""}`;
|
|
195
|
+
if (termSlugs.has(key)) {
|
|
189
196
|
errors.push(
|
|
190
197
|
`${termPrefix}.slug: duplicate term slug "${term.slug}" in taxonomy "${taxonomy.name}"`,
|
|
191
198
|
);
|
|
192
199
|
}
|
|
193
|
-
termSlugs.add(
|
|
200
|
+
termSlugs.add(key);
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
if (!term.label) {
|
|
@@ -207,11 +214,12 @@ export function validateSeed(data: unknown): ValidationResult {
|
|
|
207
214
|
}
|
|
208
215
|
}
|
|
209
216
|
|
|
210
|
-
// Second pass: validate parent references
|
|
217
|
+
// Second pass: validate parent references (within the same locale).
|
|
211
218
|
if (taxonomy.hierarchical && taxonomy.terms) {
|
|
212
219
|
for (let j = 0; j < taxonomy.terms.length; j++) {
|
|
213
220
|
const term = taxonomy.terms[j];
|
|
214
|
-
|
|
221
|
+
const termLocale = term.locale ?? taxonomy.locale ?? "";
|
|
222
|
+
if (term.parent && !termSlugs.has(`${term.parent}::${termLocale}`)) {
|
|
215
223
|
errors.push(
|
|
216
224
|
`${prefix}.terms[${j}].parent: parent term "${term.parent}" not found in taxonomy`,
|
|
217
225
|
);
|
|
@@ -243,11 +251,17 @@ export function validateSeed(data: unknown): ValidationResult {
|
|
|
243
251
|
if (!menu.name) {
|
|
244
252
|
errors.push(`${prefix}: name is required`);
|
|
245
253
|
} else {
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
254
|
+
// Uniqueness is per (name, locale) — siblings of a translation
|
|
255
|
+
// group share name but differ in locale.
|
|
256
|
+
const key = `${menu.name}::${menu.locale ?? ""}`;
|
|
257
|
+
if (menuNames.has(key)) {
|
|
258
|
+
errors.push(
|
|
259
|
+
menu.locale
|
|
260
|
+
? `${prefix}.name: duplicate menu "${menu.name}" in locale "${menu.locale}"`
|
|
261
|
+
: `${prefix}.name: duplicate menu name "${menu.name}"`,
|
|
262
|
+
);
|
|
249
263
|
}
|
|
250
|
-
menuNames.add(
|
|
264
|
+
menuNames.add(key);
|
|
251
265
|
}
|
|
252
266
|
|
|
253
267
|
if (!menu.label) {
|