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/menus/index.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Navigation menu runtime functions
|
|
2
|
+
* Navigation menu runtime functions.
|
|
3
3
|
*
|
|
4
|
-
* These are called from templates to query menus and resolve URLs.
|
|
4
|
+
* These are called from templates to query menus and resolve URLs. All queries
|
|
5
|
+
* are locale-aware: when a locale is configured (or passed explicitly) items
|
|
6
|
+
* are filtered to that locale, and menu item references resolve against the
|
|
7
|
+
* referenced content's translation_group so the URL points at the right
|
|
8
|
+
* per-locale row.
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
11
|
import type { Kysely } from "kysely";
|
|
@@ -9,50 +13,61 @@ import { sql } from "kysely";
|
|
|
9
13
|
|
|
10
14
|
import type { Database } from "../database/types.js";
|
|
11
15
|
import { validateIdentifier } from "../database/validate.js";
|
|
16
|
+
import { resolveLocale, resolveLocaleChain } from "../i18n/resolve.js";
|
|
12
17
|
import { getDb } from "../loader.js";
|
|
13
18
|
import { requestCached } from "../request-cache.js";
|
|
14
19
|
import { sanitizeHref } from "../utils/url.js";
|
|
15
20
|
import type { Menu, MenuItem, MenuItemRow } from "./types.js";
|
|
16
21
|
|
|
22
|
+
export interface MenuQueryOptions {
|
|
23
|
+
/** Override the locale used for the lookup. When omitted, the locale comes
|
|
24
|
+
* from the request context or the configured defaultLocale. */
|
|
25
|
+
locale?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
/**
|
|
18
|
-
* Get menu by name with resolved URLs
|
|
29
|
+
* Get a menu by name with resolved URLs.
|
|
19
30
|
*
|
|
20
31
|
* @example
|
|
21
32
|
* ```ts
|
|
22
|
-
* import { getMenu } from "emdash";
|
|
23
|
-
*
|
|
24
33
|
* const menu = await getMenu("primary");
|
|
25
|
-
*
|
|
26
|
-
* console.log(menu.items); // Array of MenuItem with resolved URLs
|
|
27
|
-
* }
|
|
34
|
+
* const menuEs = await getMenu("primary", { locale: "es" });
|
|
28
35
|
* ```
|
|
29
36
|
*/
|
|
30
|
-
export function getMenu(name: string): Promise<Menu | null> {
|
|
31
|
-
|
|
37
|
+
export function getMenu(name: string, options: MenuQueryOptions = {}): Promise<Menu | null> {
|
|
38
|
+
const locale = resolveLocale(options.locale);
|
|
39
|
+
return requestCached(`menu:${name}:${locale ?? "*"}`, async () => {
|
|
32
40
|
const db = await getDb();
|
|
33
|
-
return getMenuWithDb(name, db);
|
|
41
|
+
return getMenuWithDb(name, db, { locale });
|
|
34
42
|
});
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/**
|
|
38
|
-
* Get menu by name with resolved URLs (with explicit db)
|
|
39
|
-
*
|
|
40
|
-
* @internal Use `getMenu()` in templates. This variant is for admin routes
|
|
41
|
-
* that already have a database handle.
|
|
46
|
+
* Get menu by name with resolved URLs (with explicit db). Internal helper for
|
|
47
|
+
* admin routes that already have a database handle.
|
|
42
48
|
*/
|
|
43
|
-
export async function getMenuWithDb(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
export async function getMenuWithDb(
|
|
50
|
+
name: string,
|
|
51
|
+
db: Kysely<Database>,
|
|
52
|
+
options: MenuQueryOptions = {},
|
|
53
|
+
): Promise<Menu | null> {
|
|
54
|
+
const chain = resolveLocaleChain(options.locale);
|
|
55
|
+
|
|
56
|
+
const selectMenu = () => db.selectFrom("_emdash_menus").selectAll().where("name", "=", name);
|
|
57
|
+
|
|
58
|
+
let menuRow: Awaited<ReturnType<ReturnType<typeof selectMenu>["executeTakeFirst"]>>;
|
|
59
|
+
if (chain.length === 0) {
|
|
60
|
+
menuRow = await selectMenu().orderBy("locale", "asc").executeTakeFirst();
|
|
61
|
+
} else {
|
|
62
|
+
menuRow = undefined;
|
|
63
|
+
for (const locale of chain) {
|
|
64
|
+
menuRow = await selectMenu().where("locale", "=", locale).executeTakeFirst();
|
|
65
|
+
if (menuRow) break;
|
|
66
|
+
}
|
|
53
67
|
}
|
|
54
68
|
|
|
55
|
-
|
|
69
|
+
if (!menuRow) return null;
|
|
70
|
+
|
|
56
71
|
const itemRows = await db
|
|
57
72
|
.selectFrom("_emdash_menu_items")
|
|
58
73
|
.selectAll()
|
|
@@ -61,31 +76,27 @@ export async function getMenuWithDb(name: string, db: Kysely<Database>): Promise
|
|
|
61
76
|
.orderBy("sort_order", "asc")
|
|
62
77
|
.execute();
|
|
63
78
|
|
|
64
|
-
|
|
65
|
-
const items = await buildMenuTree(itemRows, db);
|
|
79
|
+
const items = await buildMenuTree(itemRows, db, menuRow.locale);
|
|
66
80
|
|
|
67
81
|
return {
|
|
68
82
|
id: menuRow.id,
|
|
69
83
|
name: menuRow.name,
|
|
70
84
|
label: menuRow.label,
|
|
71
85
|
items,
|
|
86
|
+
locale: menuRow.locale,
|
|
87
|
+
translationGroup: menuRow.translation_group,
|
|
72
88
|
};
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
/**
|
|
76
|
-
* Get all menus (without items - for admin list
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* ```ts
|
|
80
|
-
* import { getMenus } from "emdash";
|
|
81
|
-
*
|
|
82
|
-
* const menus = await getMenus();
|
|
83
|
-
* console.log(menus); // [{ id, name, label }]
|
|
84
|
-
* ```
|
|
92
|
+
* Get all menus (without items, locale-filtered — for admin list / site nav
|
|
93
|
+
* summaries). When no locale is configured, returns menus across all locales.
|
|
85
94
|
*/
|
|
86
|
-
export async function getMenus(
|
|
95
|
+
export async function getMenus(
|
|
96
|
+
options: MenuQueryOptions = {},
|
|
97
|
+
): Promise<Array<{ id: string; name: string; label: string; locale: string }>> {
|
|
87
98
|
const db = await getDb();
|
|
88
|
-
return getMenusWithDb(db);
|
|
99
|
+
return getMenusWithDb(db, options);
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
/**
|
|
@@ -96,26 +107,30 @@ export async function getMenus(): Promise<Array<{ id: string; name: string; labe
|
|
|
96
107
|
*/
|
|
97
108
|
export async function getMenusWithDb(
|
|
98
109
|
db: Kysely<Database>,
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
options: MenuQueryOptions = {},
|
|
111
|
+
): Promise<Array<{ id: string; name: string; label: string; locale: string }>> {
|
|
112
|
+
const locale = resolveLocale(options.locale);
|
|
113
|
+
let query = db
|
|
101
114
|
.selectFrom("_emdash_menus")
|
|
102
|
-
.select(["id", "name", "label"])
|
|
103
|
-
.orderBy("name", "asc")
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return rows;
|
|
115
|
+
.select(["id", "name", "label", "locale"])
|
|
116
|
+
.orderBy("name", "asc");
|
|
117
|
+
if (locale !== undefined) query = query.where("locale", "=", locale);
|
|
118
|
+
return query.execute();
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
/**
|
|
110
|
-
* Build hierarchical menu tree from flat
|
|
122
|
+
* Build a hierarchical menu tree from a flat list of items. Items are
|
|
123
|
+
* resolved against the given `locale` so references land on the right
|
|
124
|
+
* per-locale content rows.
|
|
111
125
|
*/
|
|
112
|
-
async function buildMenuTree(
|
|
113
|
-
|
|
126
|
+
async function buildMenuTree(
|
|
127
|
+
items: MenuItemRow[],
|
|
128
|
+
db: Kysely<Database>,
|
|
129
|
+
locale: string,
|
|
130
|
+
): Promise<MenuItem[]> {
|
|
114
131
|
const collectionSlugs = new Set<string>();
|
|
115
132
|
for (const item of items) {
|
|
116
|
-
if (item.reference_collection)
|
|
117
|
-
collectionSlugs.add(item.reference_collection);
|
|
118
|
-
}
|
|
133
|
+
if (item.reference_collection) collectionSlugs.add(item.reference_collection);
|
|
119
134
|
if (item.type === "page" || item.type === "post") {
|
|
120
135
|
collectionSlugs.add(item.reference_collection || `${item.type}s`);
|
|
121
136
|
}
|
|
@@ -128,41 +143,28 @@ async function buildMenuTree(items: MenuItemRow[], db: Kysely<Database>): Promis
|
|
|
128
143
|
.select(["slug", "url_pattern"])
|
|
129
144
|
.where("slug", "in", [...collectionSlugs])
|
|
130
145
|
.execute();
|
|
131
|
-
for (const row of rows)
|
|
132
|
-
urlPatterns.set(row.slug, row.url_pattern);
|
|
133
|
-
}
|
|
146
|
+
for (const row of rows) urlPatterns.set(row.slug, row.url_pattern);
|
|
134
147
|
}
|
|
135
148
|
|
|
136
|
-
// Resolve all URLs first
|
|
137
149
|
const resolvedItems = await Promise.all(
|
|
138
|
-
items.map((item) => resolveMenuItem(item, db, urlPatterns)),
|
|
150
|
+
items.map((item) => resolveMenuItem(item, db, urlPatterns, locale)),
|
|
139
151
|
);
|
|
152
|
+
const validItems = resolvedItems.filter((item): item is MenuItem => item !== null);
|
|
140
153
|
|
|
141
|
-
// Filter out items that couldn't be resolved (e.g., deleted content)
|
|
142
|
-
const validItems = resolvedItems.filter((item) => item !== null);
|
|
143
|
-
|
|
144
|
-
// Build tree structure
|
|
145
154
|
const itemMap = new Map<string, MenuItem & { children: MenuItem[] }>();
|
|
146
155
|
const rootItems: MenuItem[] = [];
|
|
147
156
|
|
|
148
|
-
// First pass: create all items
|
|
149
157
|
for (const item of validItems) {
|
|
150
158
|
itemMap.set(item.id, { ...item, children: [] });
|
|
151
159
|
}
|
|
152
160
|
|
|
153
|
-
// Second pass: build parent-child relationships
|
|
154
161
|
for (const item of items) {
|
|
155
162
|
const menuItem = itemMap.get(item.id);
|
|
156
163
|
if (!menuItem) continue;
|
|
157
|
-
|
|
158
164
|
if (item.parent_id) {
|
|
159
165
|
const parent = itemMap.get(item.parent_id);
|
|
160
|
-
if (parent)
|
|
161
|
-
|
|
162
|
-
} else {
|
|
163
|
-
// Parent not found, treat as root
|
|
164
|
-
rootItems.push(menuItem);
|
|
165
|
-
}
|
|
166
|
+
if (parent) parent.children.push(menuItem);
|
|
167
|
+
else rootItems.push(menuItem);
|
|
166
168
|
} else {
|
|
167
169
|
rootItems.push(menuItem);
|
|
168
170
|
}
|
|
@@ -172,14 +174,15 @@ async function buildMenuTree(items: MenuItemRow[], db: Kysely<Database>): Promis
|
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
/**
|
|
175
|
-
* Resolve a single menu item's URL
|
|
176
|
-
*
|
|
177
|
-
*
|
|
177
|
+
* Resolve a single menu item's URL. `reference_id` is a translation_group
|
|
178
|
+
* (migration 036 remapped all existing references); we join it against
|
|
179
|
+
* the per-locale ec_* row or per-locale taxonomy row.
|
|
178
180
|
*/
|
|
179
181
|
async function resolveMenuItem(
|
|
180
182
|
item: MenuItemRow,
|
|
181
183
|
db: Kysely<Database>,
|
|
182
184
|
urlPatterns: Map<string, string | null>,
|
|
185
|
+
locale: string,
|
|
183
186
|
): Promise<MenuItem | null> {
|
|
184
187
|
let url: string | null;
|
|
185
188
|
|
|
@@ -192,24 +195,18 @@ async function resolveMenuItem(
|
|
|
192
195
|
case "page":
|
|
193
196
|
case "post":
|
|
194
197
|
url = await resolveContentUrl(
|
|
195
|
-
// Default to plural collection name (pages/posts) if not specified
|
|
196
198
|
item.reference_collection || `${item.type}s`,
|
|
197
199
|
item.reference_id,
|
|
198
200
|
db,
|
|
199
201
|
urlPatterns,
|
|
202
|
+
locale,
|
|
200
203
|
);
|
|
201
|
-
|
|
202
|
-
if (url === null) {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
204
|
+
if (url === null) return null;
|
|
205
205
|
break;
|
|
206
206
|
|
|
207
207
|
case "taxonomy":
|
|
208
|
-
url = await resolveTaxonomyUrl(item.reference_id, db);
|
|
209
|
-
|
|
210
|
-
if (url === null) {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
208
|
+
url = await resolveTaxonomyUrl(item.reference_id, db, locale);
|
|
209
|
+
if (url === null) return null;
|
|
213
210
|
break;
|
|
214
211
|
|
|
215
212
|
case "collection":
|
|
@@ -223,16 +220,14 @@ async function resolveMenuItem(
|
|
|
223
220
|
item.reference_id,
|
|
224
221
|
db,
|
|
225
222
|
urlPatterns,
|
|
223
|
+
locale,
|
|
226
224
|
);
|
|
227
|
-
if (url === null)
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
225
|
+
if (url === null) return null;
|
|
230
226
|
} else {
|
|
231
227
|
url = "#";
|
|
232
228
|
}
|
|
233
229
|
}
|
|
234
230
|
} catch (error) {
|
|
235
|
-
// If resolution fails, skip this item
|
|
236
231
|
console.error(`Failed to resolve menu item ${item.id}:`, error);
|
|
237
232
|
return null;
|
|
238
233
|
}
|
|
@@ -244,7 +239,7 @@ async function resolveMenuItem(
|
|
|
244
239
|
target: item.target || undefined,
|
|
245
240
|
titleAttr: item.title_attr || undefined,
|
|
246
241
|
cssClasses: item.css_classes || undefined,
|
|
247
|
-
children: [],
|
|
242
|
+
children: [],
|
|
248
243
|
};
|
|
249
244
|
}
|
|
250
245
|
|
|
@@ -261,72 +256,96 @@ function interpolateUrlPattern(pattern: string, slug: string, id: string): strin
|
|
|
261
256
|
}
|
|
262
257
|
|
|
263
258
|
/**
|
|
264
|
-
* Resolve URL for a content
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
259
|
+
* Resolve the URL for a content reference. `referenceGroup` is the content
|
|
260
|
+
* row's translation_group; we look up the row in the requested locale
|
|
261
|
+
* (falling back to the source if no translation exists so the menu link is
|
|
262
|
+
* still clickable).
|
|
268
263
|
*/
|
|
269
264
|
async function resolveContentUrl(
|
|
270
265
|
collection: string,
|
|
271
|
-
|
|
266
|
+
referenceGroup: string | null,
|
|
272
267
|
db: Kysely<Database>,
|
|
273
268
|
urlPatterns: Map<string, string | null>,
|
|
269
|
+
locale: string,
|
|
274
270
|
): Promise<string | null> {
|
|
275
|
-
if (!
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
271
|
+
if (!referenceGroup) return null;
|
|
278
272
|
|
|
279
273
|
try {
|
|
280
|
-
// Validate collection name before interpolating into table reference
|
|
281
274
|
validateIdentifier(collection, "menu item collection");
|
|
282
275
|
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
SELECT slug FROM ${sql.ref(`ec_${collection}`)}
|
|
276
|
+
// Try the requested locale first, then any locale (deterministic).
|
|
277
|
+
let result = await sql<{ id: string; slug: string }>`
|
|
278
|
+
SELECT id, slug FROM ${sql.ref(`ec_${collection}`)}
|
|
279
|
+
WHERE translation_group = ${referenceGroup} AND locale = ${locale}
|
|
280
|
+
LIMIT 1
|
|
286
281
|
`.execute(db);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
282
|
+
let row = result.rows[0];
|
|
283
|
+
if (!row) {
|
|
284
|
+
result = await sql<{ id: string; slug: string }>`
|
|
285
|
+
SELECT id, slug FROM ${sql.ref(`ec_${collection}`)}
|
|
286
|
+
WHERE translation_group = ${referenceGroup}
|
|
287
|
+
ORDER BY locale ASC LIMIT 1
|
|
288
|
+
`.execute(db);
|
|
289
|
+
row = result.rows[0];
|
|
295
290
|
}
|
|
291
|
+
if (!row) {
|
|
292
|
+
// Legacy rows whose reference_id still points at an id directly
|
|
293
|
+
// (defensive — migration 036 normalised these, but a row inserted
|
|
294
|
+
// between migrations could predate the remap).
|
|
295
|
+
const legacy = await sql<{ id: string; slug: string }>`
|
|
296
|
+
SELECT id, slug FROM ${sql.ref(`ec_${collection}`)}
|
|
297
|
+
WHERE id = ${referenceGroup} LIMIT 1
|
|
298
|
+
`.execute(db);
|
|
299
|
+
row = legacy.rows[0];
|
|
300
|
+
}
|
|
301
|
+
if (!row) return null;
|
|
296
302
|
|
|
297
|
-
|
|
298
|
-
return
|
|
303
|
+
const pattern = urlPatterns.get(collection);
|
|
304
|
+
if (pattern) return interpolateUrlPattern(pattern, row.slug, row.id);
|
|
305
|
+
return `/${collection}/${row.slug}`;
|
|
299
306
|
} catch (error) {
|
|
300
|
-
|
|
301
|
-
console.error(`Failed to resolve content URL for ${collection}/${entryId}:`, error);
|
|
307
|
+
console.error(`Failed to resolve content URL for ${collection}/${referenceGroup}:`, error);
|
|
302
308
|
return null;
|
|
303
309
|
}
|
|
304
310
|
}
|
|
305
311
|
|
|
306
312
|
/**
|
|
307
|
-
* Resolve URL for a taxonomy term
|
|
308
|
-
*
|
|
309
|
-
* Returns null if taxonomy not found (item should be skipped)
|
|
313
|
+
* Resolve URL for a taxonomy term reference. `referenceGroup` is the term's
|
|
314
|
+
* translation_group; we pick the row in the active locale (or fall back).
|
|
310
315
|
*/
|
|
311
316
|
async function resolveTaxonomyUrl(
|
|
312
|
-
|
|
317
|
+
referenceGroup: string | null,
|
|
313
318
|
db: Kysely<Database>,
|
|
319
|
+
locale: string,
|
|
314
320
|
): Promise<string | null> {
|
|
315
|
-
if (!
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
321
|
+
if (!referenceGroup) return null;
|
|
318
322
|
|
|
319
|
-
|
|
323
|
+
let taxonomy = await db
|
|
320
324
|
.selectFrom("taxonomies")
|
|
321
325
|
.select(["name", "slug"])
|
|
322
|
-
.where("
|
|
326
|
+
.where("translation_group", "=", referenceGroup)
|
|
327
|
+
.where("locale", "=", locale)
|
|
323
328
|
.executeTakeFirst();
|
|
324
329
|
|
|
325
330
|
if (!taxonomy) {
|
|
326
|
-
|
|
327
|
-
|
|
331
|
+
taxonomy = await db
|
|
332
|
+
.selectFrom("taxonomies")
|
|
333
|
+
.select(["name", "slug"])
|
|
334
|
+
.where("translation_group", "=", referenceGroup)
|
|
335
|
+
.orderBy("locale", "asc")
|
|
336
|
+
.executeTakeFirst();
|
|
328
337
|
}
|
|
329
338
|
|
|
330
|
-
|
|
339
|
+
if (!taxonomy) {
|
|
340
|
+
// Legacy: id-based reference that predates the migration remap.
|
|
341
|
+
taxonomy = await db
|
|
342
|
+
.selectFrom("taxonomies")
|
|
343
|
+
.select(["name", "slug"])
|
|
344
|
+
.where("id", "=", referenceGroup)
|
|
345
|
+
.executeTakeFirst();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!taxonomy) return null;
|
|
349
|
+
|
|
331
350
|
return `/${taxonomy.name}/${taxonomy.slug}`;
|
|
332
351
|
}
|
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
|
/**
|