emdash 0.9.0 → 0.11.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-Ded_1vng.mjs} +167 -254
- package/dist/apply-Ded_1vng.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.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +94 -43
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +12 -11
- 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-gFn1r0vA.mjs} +4 -4
- package/dist/{byline-BSaNL1w7.mjs.map → byline-gFn1r0vA.mjs.map} +1 -1
- package/dist/{bylines-CvJ3PYz2.mjs → bylines-DTFI8nDM.mjs} +5 -5
- package/dist/{bylines-CvJ3PYz2.mjs.map → bylines-DTFI8nDM.mjs.map} +1 -1
- package/dist/{cache-C6N_hhN7.mjs → cache-BAJbeoZ8.mjs} +3 -3
- package/dist/{cache-C6N_hhN7.mjs.map → cache-BAJbeoZ8.mjs.map} +1 -1
- package/dist/{chunks-NBQVDOci.mjs → chunks-BK1oZS-l.mjs} +2 -2
- package/dist/{chunks-NBQVDOci.mjs.map → chunks-BK1oZS-l.mjs.map} +1 -1
- package/dist/cli/index.mjs +342 -95
- 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-CERxPUN0.mjs} +14 -3
- package/dist/content-CERxPUN0.mjs.map +1 -0
- package/dist/database/instrumentation.d.mts +6 -4
- package/dist/database/instrumentation.d.mts.map +1 -1
- package/dist/database/instrumentation.mjs +19 -7
- package/dist/database/instrumentation.mjs.map +1 -1
- 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-Cg-rC4Gj.d.mts} +110 -87
- package/dist/index-Cg-rC4Gj.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +29 -28
- package/dist/{load-DDqMMvZL.mjs → load-DR1VwFXR.mjs} +2 -2
- package/dist/{load-DDqMMvZL.mjs.map → load-DR1VwFXR.mjs.map} +1 -1
- package/dist/{loader-CKLbBnhK.mjs → loader-ou_PXAjg.mjs} +31 -6
- package/dist/loader-ou_PXAjg.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-1fFhub9c.mjs} +22 -10
- package/dist/media-1fFhub9c.mjs.map +1 -0
- 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-8c_meo_K.mjs} +13 -13
- package/dist/{query-Cg9ZKRQ0.mjs.map → query-8c_meo_K.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-Do34mz_P.mjs} +7 -6
- package/dist/registry-Do34mz_P.mjs.map +1 -0
- package/dist/{request-cache-B-bmkipQ.mjs → request-cache-D4I69LeL.mjs} +6 -2
- package/dist/request-cache-D4I69LeL.mjs.map +1 -0
- package/dist/request-context.d.mts +27 -1
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs +16 -3
- package/dist/request-context.mjs.map +1 -1
- package/dist/{runner-C7ADox5q.mjs → runner-DIcU2UCC.mjs} +465 -148
- package/dist/runner-DIcU2UCC.mjs.map +1 -0
- package/dist/{runner-Bnoj7vjK.d.mts → runner-Iu3IZSDM.d.mts} +2 -2
- package/dist/{runner-Bnoj7vjK.d.mts.map → runner-Iu3IZSDM.d.mts.map} +1 -1
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +3 -3
- package/dist/{search-dOGEccMa.mjs → search-DuWhx4NG.mjs} +322 -108
- package/dist/search-DuWhx4NG.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-Bw76xAxo.mjs +407 -0
- package/dist/taxonomies-Bw76xAxo.mjs.map +1 -0
- package/dist/taxonomy-D6NvlKo8.mjs +218 -0
- package/dist/taxonomy-D6NvlKo8.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-CIOg5AR8.mjs → types-56BKbld_.mjs} +1 -1
- package/dist/types-56BKbld_.mjs.map +1 -0
- 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-CrtWgIvl.d.mts → types-BQx6ZXpR.d.mts} +10 -1
- package/dist/types-BQx6ZXpR.d.mts.map +1 -0
- 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-DiI8NOG_.mjs +16 -0
- package/dist/types-DiI8NOG_.mjs.map +1 -0
- package/dist/{types-BuBIptGk.d.mts → types-IN5z_S3P.d.mts} +158 -92
- package/dist/types-IN5z_S3P.d.mts.map +1 -0
- package/dist/{types-BSyXeCFW.d.mts → types-IZSZfEwv.d.mts} +4 -3
- package/dist/types-IZSZfEwv.d.mts.map +1 -0
- 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-BfQh_C_y.d.mts → validate-CO3JjFV5.d.mts} +22 -5
- package/dist/validate-CO3JjFV5.d.mts.map +1 -0
- package/dist/{validate-Baqf0slj.mjs → validate-UK4Ja1uo.mjs} +14 -10
- package/dist/validate-UK4Ja1uo.mjs.map +1 -0
- package/dist/{validation-BfEI7tNe.mjs → validation-Vc5DQkJa.mjs} +5 -5
- package/dist/{validation-BfEI7tNe.mjs.map → validation-Vc5DQkJa.mjs.map} +1 -1
- package/dist/version-Bg31I_Ff.mjs +7 -0
- package/dist/{version-DoxrVdYf.mjs.map → version-Bg31I_Ff.mjs.map} +1 -1
- package/dist/{zod-generator-CC0xNe_K.mjs → zod-generator-CHnJUP2l.mjs} +8 -3
- package/dist/zod-generator-CHnJUP2l.mjs.map +1 -0
- package/package.json +9 -8
- package/src/api/errors.ts +5 -0
- package/src/api/handlers/content.ts +20 -0
- package/src/api/handlers/dashboard.ts +29 -36
- package/src/api/handlers/media-allowlist.ts +40 -0
- package/src/api/handlers/media.ts +1 -1
- package/src/api/handlers/menus.ts +400 -89
- package/src/api/handlers/taxonomies.ts +273 -97
- package/src/api/handlers/validate-media-fields.ts +125 -0
- package/src/api/schemas/common.ts +7 -0
- package/src/api/schemas/media.ts +23 -3
- package/src/api/schemas/menus.ts +23 -0
- package/src/api/schemas/schema.ts +11 -2
- package/src/api/schemas/taxonomies.ts +39 -0
- package/src/astro/integration/routes.ts +10 -0
- package/src/astro/middleware.ts +46 -11
- 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/media/upload-url.ts +10 -4
- package/src/astro/routes/api/media.ts +12 -4
- 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/astro/types.ts +5 -1
- package/src/auth/rate-limit.ts +3 -3
- package/src/cli/commands/bundle-utils.ts +81 -6
- package/src/cli/commands/bundle.ts +18 -15
- package/src/cli/commands/export-seed.ts +139 -24
- package/src/cli/commands/plugin-init.ts +216 -90
- package/src/database/instrumentation.ts +22 -8
- package/src/database/migrations/016_api_tokens.ts +18 -3
- package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
- package/src/database/migrations/037_credential_algorithm.ts +18 -0
- package/src/database/migrations/runner.ts +4 -0
- package/src/database/repositories/content.ts +11 -0
- package/src/database/repositories/media.ts +40 -10
- package/src/database/repositories/taxonomy.ts +193 -89
- package/src/database/types.ts +12 -3
- package/src/emdash-runtime.ts +16 -3
- package/src/fields/file.ts +7 -6
- package/src/fields/image.ts +12 -11
- package/src/fields/types.ts +3 -0
- package/src/i18n/resolve.ts +37 -0
- package/src/index.ts +1 -1
- package/src/loader.ts +49 -2
- package/src/mcp/server.ts +114 -26
- package/src/media/mime.ts +75 -0
- package/src/menus/index.ts +143 -124
- package/src/menus/types.ts +15 -1
- package/src/plugins/types.ts +81 -191
- package/src/request-cache.ts +6 -2
- package/src/request-context.ts +42 -2
- package/src/schema/registry.ts +5 -5
- package/src/schema/types.ts +3 -2
- package/src/schema/zod-generator.ts +12 -2
- package/src/seed/apply.ts +157 -54
- package/src/seed/types.ts +18 -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/media-BW32b4gi.mjs.map +0 -1
- package/dist/registry-Dw70ChxB.mjs.map +0 -1
- package/dist/request-cache-B-bmkipQ.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-4fVtCIm0.mjs +0 -68
- package/dist/types-4fVtCIm0.mjs.map +0 -1
- package/dist/types-BSyXeCFW.d.mts.map +0 -1
- package/dist/types-BuBIptGk.d.mts.map +0 -1
- package/dist/types-CIOg5AR8.mjs.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/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,41 @@ 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
|
+
// Shared across menus: translated items reference anchor items in sibling menus.
|
|
516
|
+
const itemSeedIdMap = new Map<string, { id: string; translationGroup: string }>();
|
|
517
|
+
const fallbackLocale = getI18nConfig()?.defaultLocale ?? "en";
|
|
518
|
+
|
|
474
519
|
for (const menu of seed.menus) {
|
|
475
|
-
|
|
476
|
-
|
|
520
|
+
const locale = menu.locale ?? fallbackLocale;
|
|
521
|
+
let lookup = db
|
|
477
522
|
.selectFrom("_emdash_menus")
|
|
478
523
|
.selectAll()
|
|
479
524
|
.where("name", "=", menu.name)
|
|
480
|
-
.
|
|
525
|
+
.where("locale", "=", locale);
|
|
526
|
+
const existingMenu = await lookup.executeTakeFirst();
|
|
481
527
|
|
|
482
528
|
let menuId: string;
|
|
529
|
+
let translationGroup: string;
|
|
483
530
|
|
|
484
531
|
if (existingMenu) {
|
|
485
532
|
menuId = existingMenu.id;
|
|
533
|
+
translationGroup = existingMenu.translation_group ?? existingMenu.id;
|
|
486
534
|
// Clear existing items (menus are recreated)
|
|
487
535
|
await db.deleteFrom("_emdash_menu_items").where("menu_id", "=", menuId).execute();
|
|
488
536
|
} else {
|
|
489
|
-
// Create menu
|
|
490
537
|
menuId = ulid();
|
|
538
|
+
// Resolve translationOf to the source menu's translation_group.
|
|
539
|
+
translationGroup = menuId;
|
|
540
|
+
if (menu.translationOf) {
|
|
541
|
+
const source = menuSeedIdMap.get(menu.translationOf);
|
|
542
|
+
if (source) translationGroup = source.translationGroup;
|
|
543
|
+
else
|
|
544
|
+
console.warn(
|
|
545
|
+
`menu "${menu.name}" (${locale}): translationOf "${menu.translationOf}" not found yet; minting a fresh group.`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
491
548
|
await db
|
|
492
549
|
.insertInto("_emdash_menus")
|
|
493
550
|
.values({
|
|
@@ -496,19 +553,25 @@ export async function applySeed(
|
|
|
496
553
|
label: menu.label,
|
|
497
554
|
created_at: new Date().toISOString(),
|
|
498
555
|
updated_at: new Date().toISOString(),
|
|
556
|
+
locale,
|
|
557
|
+
translation_group: translationGroup,
|
|
499
558
|
})
|
|
500
559
|
.execute();
|
|
501
560
|
result.menus.created++;
|
|
502
561
|
}
|
|
503
562
|
|
|
563
|
+
if (menu.id) menuSeedIdMap.set(menu.id, { id: menuId, translationGroup });
|
|
564
|
+
|
|
504
565
|
// Create menu items
|
|
505
566
|
const itemCount = await applyMenuItems(
|
|
506
567
|
db,
|
|
507
568
|
menuId,
|
|
569
|
+
locale,
|
|
508
570
|
menu.items,
|
|
509
571
|
null, // parent_id
|
|
510
572
|
0, // sort_order
|
|
511
573
|
seedIdMap,
|
|
574
|
+
itemSeedIdMap,
|
|
512
575
|
);
|
|
513
576
|
result.menus.items += itemCount;
|
|
514
577
|
}
|
|
@@ -692,64 +755,75 @@ export async function applySeed(
|
|
|
692
755
|
async function applyHierarchicalTerms(
|
|
693
756
|
termRepo: TaxonomyRepository,
|
|
694
757
|
taxonomyName: string,
|
|
758
|
+
defLocale: string,
|
|
695
759
|
terms: SeedTaxonomyTerm[],
|
|
760
|
+
termSeedIdMap: Map<string, string>,
|
|
696
761
|
result: SeedApplyResult,
|
|
697
762
|
onConflict: "skip" | "update" | "error" = "skip",
|
|
698
763
|
): Promise<void> {
|
|
699
|
-
//
|
|
764
|
+
// "locale::slug" -> id, so the same slug can resolve per locale.
|
|
700
765
|
const slugToId = new Map<string, string>();
|
|
701
766
|
|
|
702
|
-
// Multiple passes
|
|
767
|
+
// Multiple passes — handles deep nesting and translationOf forward refs.
|
|
703
768
|
let remaining = [...terms];
|
|
704
|
-
let maxPasses = 10;
|
|
769
|
+
let maxPasses = 10;
|
|
705
770
|
|
|
706
771
|
while (remaining.length > 0 && maxPasses > 0) {
|
|
707
772
|
const processedThisPass: string[] = [];
|
|
708
773
|
|
|
709
774
|
for (const term of remaining) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
775
|
+
const termLocale = term.locale ?? defLocale;
|
|
776
|
+
const parentReady = !term.parent || slugToId.has(`${termLocale}::${term.parent}`);
|
|
777
|
+
const translationReady = !term.translationOf || termSeedIdMap.has(term.translationOf);
|
|
713
778
|
|
|
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,
|
|
779
|
+
if (!parentReady || !translationReady) continue;
|
|
780
|
+
|
|
781
|
+
const parentId = term.parent ? slugToId.get(`${termLocale}::${term.parent}`) : undefined;
|
|
782
|
+
const translationOf = term.translationOf ? termSeedIdMap.get(term.translationOf) : undefined;
|
|
783
|
+
|
|
784
|
+
const existing = await termRepo.findBySlug(taxonomyName, term.slug, termLocale);
|
|
785
|
+
if (existing) {
|
|
786
|
+
if (onConflict === "error") {
|
|
787
|
+
throw new Error(
|
|
788
|
+
`Conflict: taxonomy term "${term.slug}" in "${taxonomyName}" (${termLocale}) already exists`,
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
if (onConflict === "update") {
|
|
792
|
+
await termRepo.update(existing.id, {
|
|
734
793
|
label: term.label,
|
|
735
794
|
parentId,
|
|
736
|
-
data: term.description ? { description: term.description } :
|
|
795
|
+
data: term.description ? { description: term.description } : {},
|
|
737
796
|
});
|
|
738
|
-
slugToId.set(term.slug, created.id);
|
|
739
797
|
result.taxonomies.terms++;
|
|
740
798
|
}
|
|
741
|
-
|
|
742
|
-
|
|
799
|
+
slugToId.set(`${termLocale}::${term.slug}`, existing.id);
|
|
800
|
+
if (term.id) termSeedIdMap.set(term.id, existing.id);
|
|
801
|
+
} else {
|
|
802
|
+
const created = await termRepo.create({
|
|
803
|
+
name: taxonomyName,
|
|
804
|
+
slug: term.slug,
|
|
805
|
+
label: term.label,
|
|
806
|
+
parentId,
|
|
807
|
+
data: term.description ? { description: term.description } : undefined,
|
|
808
|
+
locale: termLocale,
|
|
809
|
+
translationOf,
|
|
810
|
+
});
|
|
811
|
+
slugToId.set(`${termLocale}::${term.slug}`, created.id);
|
|
812
|
+
if (term.id) termSeedIdMap.set(term.id, created.id);
|
|
813
|
+
result.taxonomies.terms++;
|
|
743
814
|
}
|
|
815
|
+
|
|
816
|
+
processedThisPass.push(term.slug + "::" + termLocale);
|
|
744
817
|
}
|
|
745
818
|
|
|
746
|
-
|
|
747
|
-
|
|
819
|
+
remaining = remaining.filter(
|
|
820
|
+
(t) => !processedThisPass.includes(t.slug + "::" + (t.locale ?? defLocale)),
|
|
821
|
+
);
|
|
748
822
|
maxPasses--;
|
|
749
823
|
}
|
|
750
824
|
|
|
751
825
|
if (remaining.length > 0) {
|
|
752
|
-
console.warn(`Could not process ${remaining.length} terms due to missing parents`);
|
|
826
|
+
console.warn(`Could not process ${remaining.length} terms due to missing parents/translations`);
|
|
753
827
|
}
|
|
754
828
|
}
|
|
755
829
|
|
|
@@ -847,21 +921,29 @@ async function applyContentTaxonomies(
|
|
|
847
921
|
}
|
|
848
922
|
|
|
849
923
|
/**
|
|
850
|
-
* Apply menu items recursively
|
|
924
|
+
* Apply menu items recursively.
|
|
925
|
+
*
|
|
926
|
+
* When a `SeedMenuItem` carries `id`/`translationOf`, the import resolves the
|
|
927
|
+
* source item's `translation_group` so cross-locale "same nav entry" links
|
|
928
|
+
* survive export → apply. Items without `translationOf` get a fresh group
|
|
929
|
+
* (= their own id).
|
|
851
930
|
*/
|
|
852
931
|
async function applyMenuItems(
|
|
853
932
|
db: Kysely<Database>,
|
|
854
933
|
menuId: string,
|
|
934
|
+
locale: string,
|
|
855
935
|
items: SeedMenuItem[],
|
|
856
936
|
parentId: string | null,
|
|
857
937
|
startOrder: number,
|
|
858
938
|
seedIdMap: Map<string, string>,
|
|
939
|
+
itemSeedIdMap: Map<string, { id: string; translationGroup: string }>,
|
|
859
940
|
): Promise<number> {
|
|
860
941
|
let count = 0;
|
|
861
942
|
let order = startOrder;
|
|
862
943
|
|
|
863
944
|
for (const item of items) {
|
|
864
945
|
const itemId = ulid();
|
|
946
|
+
const itemLocale = item.locale ?? locale;
|
|
865
947
|
|
|
866
948
|
// Resolve reference if needed
|
|
867
949
|
let referenceId: string | null = null;
|
|
@@ -877,7 +959,16 @@ async function applyMenuItems(
|
|
|
877
959
|
// If not in map, the content might not exist yet (will be broken link)
|
|
878
960
|
}
|
|
879
961
|
|
|
880
|
-
|
|
962
|
+
let translationGroup = itemId;
|
|
963
|
+
if (item.translationOf) {
|
|
964
|
+
const source = itemSeedIdMap.get(item.translationOf);
|
|
965
|
+
if (source) translationGroup = source.translationGroup;
|
|
966
|
+
else
|
|
967
|
+
console.warn(
|
|
968
|
+
`menu item "${item.label ?? item.url ?? item.ref ?? "(unlabeled)"}" (${itemLocale}): translationOf "${item.translationOf}" not found yet; minting a fresh group.`,
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
|
|
881
972
|
await db
|
|
882
973
|
.insertInto("_emdash_menu_items")
|
|
883
974
|
.values({
|
|
@@ -894,15 +985,27 @@ async function applyMenuItems(
|
|
|
894
985
|
target: item.target ?? null,
|
|
895
986
|
css_classes: item.cssClasses ?? null,
|
|
896
987
|
created_at: new Date().toISOString(),
|
|
988
|
+
locale: itemLocale,
|
|
989
|
+
translation_group: translationGroup,
|
|
897
990
|
})
|
|
898
991
|
.execute();
|
|
899
992
|
|
|
993
|
+
if (item.id) itemSeedIdMap.set(item.id, { id: itemId, translationGroup });
|
|
994
|
+
|
|
900
995
|
count++;
|
|
901
996
|
order++;
|
|
902
997
|
|
|
903
|
-
// Process children
|
|
904
998
|
if (item.children && item.children.length > 0) {
|
|
905
|
-
const childCount = await applyMenuItems(
|
|
999
|
+
const childCount = await applyMenuItems(
|
|
1000
|
+
db,
|
|
1001
|
+
menuId,
|
|
1002
|
+
itemLocale,
|
|
1003
|
+
item.children,
|
|
1004
|
+
itemId,
|
|
1005
|
+
0,
|
|
1006
|
+
seedIdMap,
|
|
1007
|
+
itemSeedIdMap,
|
|
1008
|
+
);
|
|
906
1009
|
count += childCount;
|
|
907
1010
|
}
|
|
908
1011
|
}
|
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
|
|
|
@@ -121,6 +134,8 @@ export interface SeedMenu {
|
|
|
121
134
|
* Menu item in seed
|
|
122
135
|
*/
|
|
123
136
|
export interface SeedMenuItem {
|
|
137
|
+
/** Optional seed-local id, e.g. "item:primary:home:en". */
|
|
138
|
+
id?: string;
|
|
124
139
|
type: string;
|
|
125
140
|
label?: string;
|
|
126
141
|
url?: string; // For custom type
|
|
@@ -129,6 +144,8 @@ export interface SeedMenuItem {
|
|
|
129
144
|
target?: "_blank" | "_self";
|
|
130
145
|
titleAttr?: string;
|
|
131
146
|
cssClasses?: string;
|
|
147
|
+
locale?: string;
|
|
148
|
+
translationOf?: string;
|
|
132
149
|
children?: SeedMenuItem[];
|
|
133
150
|
}
|
|
134
151
|
|
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) {
|