@murumets-ee/menus 0.37.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/LICENSE +94 -0
- package/dist/admin.d.mts +293 -0
- package/dist/admin.d.mts.map +1 -0
- package/dist/admin.mjs +2 -0
- package/dist/admin.mjs.map +1 -0
- package/dist/en-Cr9XUf6B.mjs +2 -0
- package/dist/en-Cr9XUf6B.mjs.map +1 -0
- package/dist/entity-BxLeqoBC.mjs +2 -0
- package/dist/entity-BxLeqoBC.mjs.map +1 -0
- package/dist/et-C_k3aK3T.mjs +2 -0
- package/dist/et-C_k3aK3T.mjs.map +1 -0
- package/dist/i18n.d.mts +16 -0
- package/dist/i18n.d.mts.map +1 -0
- package/dist/i18n.mjs +2 -0
- package/dist/i18n.mjs.map +1 -0
- package/dist/index.d.mts +75 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1 -0
- package/dist/menu-entity-types-BnKnKbif.mjs +2 -0
- package/dist/menu-entity-types-BnKnKbif.mjs.map +1 -0
- package/dist/menu-item-schema-xXWZKw7t.d.mts +296 -0
- package/dist/menu-item-schema-xXWZKw7t.d.mts.map +1 -0
- package/dist/messages/en.json +19 -0
- package/dist/messages/et.json +19 -0
- package/dist/messages/ru.json +19 -0
- package/dist/resolve.d.mts +205 -0
- package/dist/resolve.d.mts.map +1 -0
- package/dist/resolve.mjs +2 -0
- package/dist/resolve.mjs.map +1 -0
- package/dist/ru-CIJipJXJ.mjs +2 -0
- package/dist/ru-CIJipJXJ.mjs.map +1 -0
- package/dist/schema.d.mts +280 -0
- package/dist/schema.d.mts.map +1 -0
- package/dist/schema.mjs +2 -0
- package/dist/schema.mjs.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/menu-entity-types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Upper bound on how many entity types one menu's whitelist may name.
|
|
6
|
+
*
|
|
7
|
+
* A real app registers a few dozen entities, so this is a malformed-payload
|
|
8
|
+
* guard rather than a product limit — it keeps a hostile PATCH from parking an
|
|
9
|
+
* unbounded array in the column (and, through it, in every editor's RSC
|
|
10
|
+
* payload).
|
|
11
|
+
*
|
|
12
|
+
* **Not** `MAX_MENU_ENTITY_TYPES` (`./get-menu.ts`, 16). That one bounds RENDER
|
|
13
|
+
* fan-out — how many distinct types one menu render may issue batched lookups
|
|
14
|
+
* for — and applies to the types actually present in the stored tree. This one
|
|
15
|
+
* bounds the stored AUTHORING whitelist, which costs no I/O at all.
|
|
16
|
+
*/
|
|
17
|
+
declare const MAX_MENU_ENTITY_TYPE_WHITELIST = 64;
|
|
18
|
+
/**
|
|
19
|
+
* The write-boundary schema for `menu.entityTypes`.
|
|
20
|
+
*
|
|
21
|
+
* Strict where {@link normalizeMenuEntityTypes} is tolerant: a junk entry is a
|
|
22
|
+
* 400, not a silent drop, because silently discarding half an admin's selection
|
|
23
|
+
* and reporting success is worse than refusing the write. Duplicates ARE folded
|
|
24
|
+
* (an idempotent selection is not an error), and `[]` becomes `null` so the
|
|
25
|
+
* column only ever holds one representation of "unrestricted".
|
|
26
|
+
*/
|
|
27
|
+
declare const MenuEntityTypesSchema: z.ZodType<string[] | null, z.ZodTypeDef, unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* Narrow a raw `entityTypes` column value (untrusted shape) to a usable
|
|
30
|
+
* whitelist, or `null` for "no restriction".
|
|
31
|
+
*
|
|
32
|
+
* The read-side counterpart of {@link MenuEntityTypesSchema}, and tolerant for
|
|
33
|
+
* the same reason `normalizeMenuTree` is: rows written before this column
|
|
34
|
+
* existed, hand edits and restored dumps all reach readers, and a render must
|
|
35
|
+
* degrade rather than throw. Anything unusable — a non-array, a non-string
|
|
36
|
+
* entry, a whitelist that reduces to nothing — reads as `null`.
|
|
37
|
+
*/
|
|
38
|
+
declare function normalizeMenuEntityTypes(value: unknown): string[] | null;
|
|
39
|
+
/**
|
|
40
|
+
* Does this menu accept links to `entityType`?
|
|
41
|
+
*
|
|
42
|
+
* Takes the RAW column value so callers holding a `field.json()`-typed row can
|
|
43
|
+
* ask directly, without narrowing first. Exact string match — entity names are
|
|
44
|
+
* lowercase identifiers, and a case-insensitive or prefix match would silently
|
|
45
|
+
* widen a whitelist the admin wrote precisely.
|
|
46
|
+
*
|
|
47
|
+
* Fails OPEN (see the module header): absent, empty and malformed all mean "no
|
|
48
|
+
* restriction".
|
|
49
|
+
*/
|
|
50
|
+
declare function menuAcceptsEntityType(value: unknown, entityType: string): boolean;
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/menu-item-schema.d.ts
|
|
53
|
+
/**
|
|
54
|
+
* Absolute nesting cap for any menu, independent of a menu's own `maxDepth`
|
|
55
|
+
* (phase 07 §2.7 — "depth is capped per menu (default 3, max 6)"). Also the
|
|
56
|
+
* bound that keeps the recursive parse from being a DoS vector.
|
|
57
|
+
*/
|
|
58
|
+
declare const MAX_MENU_DEPTH = 6;
|
|
59
|
+
/** `maxDepth`'s default — a 3-level menu (phase 08 §2). */
|
|
60
|
+
declare const DEFAULT_MENU_DEPTH = 3;
|
|
61
|
+
/**
|
|
62
|
+
* Upper bound on the number of nodes in one menu tree. Navigation menus are
|
|
63
|
+
* hand-curated: a few dozen items is a big one. The cap exists so a single
|
|
64
|
+
* PATCH can't hand the validator (and `extractRefs`' ref walk, and every
|
|
65
|
+
* subsequent render) an unbounded amount of work — CLAUDE.md's
|
|
66
|
+
* "no unbounded fan-out" rule applied to a single request payload.
|
|
67
|
+
*/
|
|
68
|
+
declare const MAX_MENU_TREE_NODES = 500;
|
|
69
|
+
/**
|
|
70
|
+
* Max stored length of a `url`-kind link. Pinned to `entity`'s
|
|
71
|
+
* `MAX_EXTERNAL_URL_LENGTH` (the `external_link_usage.url_raw` column width)
|
|
72
|
+
* at COMPILE time via `satisfies` + a type-only import — so a longer URL is
|
|
73
|
+
* rejected at the boundary instead of being silently dropped from the
|
|
74
|
+
* reference graph. Same drift-proofing trick as content's
|
|
75
|
+
* `DEFAULT_LOCALE_SENTINEL`; `import type` has zero runtime cost, so the pure
|
|
76
|
+
* schema module keeps its (client-safe) dependency-free shape.
|
|
77
|
+
*/
|
|
78
|
+
declare const MAX_MENU_URL_LENGTH = 2048;
|
|
79
|
+
/**
|
|
80
|
+
* Max length of an entity-link fragment/anchor.
|
|
81
|
+
*
|
|
82
|
+
* Exported because this module is the SINGLE OWNER of the anchor bounds: the
|
|
83
|
+
* write-side schema below, the read-side anchor walk
|
|
84
|
+
* (`admin/anchors.ts`) and the editor's manual-entry fallback
|
|
85
|
+
* (`@murumets-ee/menus-ui`'s anchor picker, via the client-safe `./schema`
|
|
86
|
+
* subpath) all validate against exactly these two constants. Two of those three
|
|
87
|
+
* used to carry their own copy; a drift between them would let the picker offer
|
|
88
|
+
* — or the editor accept — a value the write boundary then rejects.
|
|
89
|
+
*/
|
|
90
|
+
declare const MAX_ANCHOR_LENGTH = 128;
|
|
91
|
+
/**
|
|
92
|
+
* Anchors are slugified by the block editor (phase 08 §5) and rendered into an
|
|
93
|
+
* `href` as `#<anchor>`; restricting them to slug characters keeps that
|
|
94
|
+
* interpolation inert.
|
|
95
|
+
*
|
|
96
|
+
* Exported for the same single-owner reason as {@link MAX_ANCHOR_LENGTH}.
|
|
97
|
+
*/
|
|
98
|
+
declare const ANCHOR_PATTERN: RegExp;
|
|
99
|
+
/**
|
|
100
|
+
* A menu item's link target — the item-type union phase 07 §2.2 and phase 08 §3
|
|
101
|
+
* define: `entity` (+ optional anchor) | `url` | `email`.
|
|
102
|
+
*
|
|
103
|
+
* **A NARROWED SUBSET of `@murumets-ee/blocks`' `LinkValue`, deliberately.**
|
|
104
|
+
* Blocks additionally carries a `media` kind; menus does NOT, because no phase
|
|
105
|
+
* gives a menu a media-href seam to resolve one with (phase 11 §2 specifies
|
|
106
|
+
* `href` as "canonical path from `toolkit_paths`; external URLs verbatim;
|
|
107
|
+
* header items null" and nothing else). Accepting a `media` link would let it
|
|
108
|
+
* validate, persist, and register an `entity_refs` row — arming delete
|
|
109
|
+
* protection for that file — while the renderer had no way to emit it: the item
|
|
110
|
+
* would silently never appear. Rejecting it at the write boundary turns that
|
|
111
|
+
* dead weight into a real 400. Widening later is additive (an arm here plus a
|
|
112
|
+
* `resolveMediaHref` dep on `MenuResolveDeps`) and needs no shape change.
|
|
113
|
+
*
|
|
114
|
+
* Every `MenuLinkValue` IS a valid blocks `LinkValue` (the subset direction, and
|
|
115
|
+
* the one `resolveLinkHref` needs) — pinned at compile time by an assignment in
|
|
116
|
+
* `menu-item-schema.test.ts`, so a drift in blocks' union is a build error
|
|
117
|
+
* rather than a silent divergence.
|
|
118
|
+
*
|
|
119
|
+
* The `anchor?: string | undefined` is forced by the combination of Zod and this
|
|
120
|
+
* repo's `exactOptionalPropertyTypes`: `.optional()` always widens to
|
|
121
|
+
* `T | undefined`, which is NOT assignable to blocks' `anchor?: string` under
|
|
122
|
+
* that flag.
|
|
123
|
+
*/
|
|
124
|
+
type MenuLinkValue = {
|
|
125
|
+
kind: 'entity';
|
|
126
|
+
entity: string;
|
|
127
|
+
id: string;
|
|
128
|
+
anchor?: string | undefined;
|
|
129
|
+
} | {
|
|
130
|
+
kind: 'url';
|
|
131
|
+
url: string;
|
|
132
|
+
} | {
|
|
133
|
+
kind: 'email';
|
|
134
|
+
email: string;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* A menu tree node (phase 08 §3 / D009).
|
|
138
|
+
*
|
|
139
|
+
* `label: null` means "use the LIVE title of the linked entity, per locale";
|
|
140
|
+
* a map is an explicit per-locale override. `enabled` is per-locale visibility
|
|
141
|
+
* in BOTH directions — absent/`true` shows the item, `false` hides it in that
|
|
142
|
+
* locale — so one tree serves every locale (no `menu_translations` table).
|
|
143
|
+
*
|
|
144
|
+
* Declared as a `type`, NOT an `interface`, on purpose: only type aliases get
|
|
145
|
+
* TypeScript's implicit index signature, and without it `MenuItem[]` is not
|
|
146
|
+
* assignable to the `Record<string, unknown>[]` arm of `AdminClient.create/
|
|
147
|
+
* update`'s json-field value type — every caller storing a typed tree would
|
|
148
|
+
* need a cast.
|
|
149
|
+
*/
|
|
150
|
+
type MenuItem = {
|
|
151
|
+
/** Stable id — DND identity + anchor for audit diffs. */id: string; /** `header` = a text-only dropdown parent with no link of its own. */
|
|
152
|
+
kind: 'link' | 'header';
|
|
153
|
+
/**
|
|
154
|
+
* Present iff `kind === 'link'`.
|
|
155
|
+
*
|
|
156
|
+
* The explicit `| undefined` is required by the repo's
|
|
157
|
+
* `exactOptionalPropertyTypes` — Zod's `.optional()` emits
|
|
158
|
+
* `LinkValue | undefined`, and without it the schema wouldn't typecheck
|
|
159
|
+
* against this interface.
|
|
160
|
+
*/
|
|
161
|
+
link?: MenuLinkValue | undefined; /** `null` = live entity title per locale; a map = explicit override. */
|
|
162
|
+
label: Record<string, string> | null; /** Per-locale visibility. Absent/`true` = visible; `false` = hidden. */
|
|
163
|
+
enabled?: Record<string, boolean> | undefined;
|
|
164
|
+
newTab?: boolean | undefined;
|
|
165
|
+
children: MenuItem[];
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* One menu item's target, as the editor needs to render its row
|
|
169
|
+
* (`@murumets-ee/menus/admin`'s `resolveMenuItemSummaries`).
|
|
170
|
+
*
|
|
171
|
+
* Declared here — not in `admin/summaries.ts` — for the same reason
|
|
172
|
+
* {@link BlockAnchor} is: it is a pure data shape with no server dependency,
|
|
173
|
+
* so the client-safe `./schema` subpath can re-export it without pulling the
|
|
174
|
+
* server-only resolver along. `import type` is erased at compile time either
|
|
175
|
+
* way, but a single canonical home (this leaf module) is simpler to reason
|
|
176
|
+
* about than "which of two server-only files owns the type".
|
|
177
|
+
*/
|
|
178
|
+
interface MenuItemSummary {
|
|
179
|
+
/**
|
|
180
|
+
* The target row's live title, or `null` when the entity declares no
|
|
181
|
+
* display-able field. Unlike the public render path — which DROPS such an
|
|
182
|
+
* item rather than show a raw uuid as navigation — the admin editor still
|
|
183
|
+
* reports the target as present, just unlabelled.
|
|
184
|
+
*/
|
|
185
|
+
title: string | null;
|
|
186
|
+
/** `false` = the target row is gone (deleted) → a BROKEN item. */
|
|
187
|
+
exists: boolean;
|
|
188
|
+
/** `null` when the entity isn't publishable — no draft/published distinction applies. */
|
|
189
|
+
published: boolean | null;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* One addressable block anchor on a document (`@murumets-ee/menus/admin`'s
|
|
193
|
+
* `collectBlockAnchors` / the anchors endpoint). See {@link MenuItemSummary}'s
|
|
194
|
+
* doc for why this pure shape lives here rather than in `admin/anchors.ts`.
|
|
195
|
+
*/
|
|
196
|
+
interface BlockAnchor {
|
|
197
|
+
/** The stable id rendered as the block element's `id` attribute. */
|
|
198
|
+
anchor: string;
|
|
199
|
+
/** Display text — `anchorLabel` when usable, else the anchor value itself. */
|
|
200
|
+
label: string;
|
|
201
|
+
}
|
|
202
|
+
/** Options for {@link buildMenuItemSchema} / {@link buildMenuTreeSchema}. */
|
|
203
|
+
interface MenuSchemaOptions {
|
|
204
|
+
/**
|
|
205
|
+
* Locale codes accepted as keys in `label` / `enabled` maps. Anything else
|
|
206
|
+
* is a validation error — a typo'd locale would produce a label no renderer
|
|
207
|
+
* ever reads (the same failure mode content's `validateLocale` guards).
|
|
208
|
+
*/
|
|
209
|
+
allowedLocales: readonly string[];
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* {@link MenuLinkValue} as Zod. Blocks models its own `LinkValue` as a plain TS
|
|
213
|
+
* type + structural guards (zod-free by design), so this is the first Zod
|
|
214
|
+
* encoding of it — narrowed to the three kinds a menu item may carry.
|
|
215
|
+
*
|
|
216
|
+
* There is deliberately NO `media` arm (see {@link MenuLinkValue}): a media
|
|
217
|
+
* link has no href seam at render time, so accepting one would store a
|
|
218
|
+
* permanently invisible item. `z.discriminatedUnion` rejects it with an
|
|
219
|
+
* `Invalid discriminator value. Expected 'entity' | 'url' | 'email'` issue —
|
|
220
|
+
* which names the allowed set without echoing the caller's own value back.
|
|
221
|
+
*
|
|
222
|
+
* `url` kinds run through `sanitizeHref`, the single source of truth for the
|
|
223
|
+
* href protocol allowlist (`http:`/`https:`/`mailto:`/`tel:` + relative
|
|
224
|
+
* paths); it returns `''` for anything it rejects, including
|
|
225
|
+
* protocol-relative `//evil.com` and obfuscated `java\nscript:`.
|
|
226
|
+
*/
|
|
227
|
+
declare const LinkValueSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
|
|
228
|
+
kind: z.ZodLiteral<"entity">;
|
|
229
|
+
entity: z.ZodString;
|
|
230
|
+
id: z.ZodString;
|
|
231
|
+
anchor: z.ZodOptional<z.ZodString>;
|
|
232
|
+
}, "strip", z.ZodTypeAny, {
|
|
233
|
+
id: string;
|
|
234
|
+
kind: "entity";
|
|
235
|
+
entity: string;
|
|
236
|
+
anchor?: string | undefined;
|
|
237
|
+
}, {
|
|
238
|
+
id: string;
|
|
239
|
+
kind: "entity";
|
|
240
|
+
entity: string;
|
|
241
|
+
anchor?: string | undefined;
|
|
242
|
+
}>, z.ZodObject<{
|
|
243
|
+
kind: z.ZodLiteral<"url">;
|
|
244
|
+
url: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
|
|
245
|
+
}, "strip", z.ZodTypeAny, {
|
|
246
|
+
kind: "url";
|
|
247
|
+
url: string;
|
|
248
|
+
}, {
|
|
249
|
+
kind: "url";
|
|
250
|
+
url: string;
|
|
251
|
+
}>, z.ZodObject<{
|
|
252
|
+
kind: z.ZodLiteral<"email">;
|
|
253
|
+
email: z.ZodString;
|
|
254
|
+
}, "strip", z.ZodTypeAny, {
|
|
255
|
+
kind: "email";
|
|
256
|
+
email: string;
|
|
257
|
+
}, {
|
|
258
|
+
kind: "email";
|
|
259
|
+
email: string;
|
|
260
|
+
}>]>;
|
|
261
|
+
/**
|
|
262
|
+
* The recursive per-node schema. Input is `unknown` (this parses untrusted
|
|
263
|
+
* JSONB), output is a fully-narrowed {@link MenuItem}. Unknown properties are
|
|
264
|
+
* STRIPPED by Zod's default object behavior, so writing the parse result back
|
|
265
|
+
* onto the payload also sanitizes it — nothing a caller invents gets stored.
|
|
266
|
+
*/
|
|
267
|
+
declare function buildMenuItemSchema(options: MenuSchemaOptions): z.ZodType<MenuItem, z.ZodTypeDef, unknown>;
|
|
268
|
+
/** The whole tree — an array of {@link buildMenuItemSchema} nodes. */
|
|
269
|
+
declare function buildMenuTreeSchema(options: MenuSchemaOptions): z.ZodType<MenuItem[], z.ZodTypeDef, unknown>;
|
|
270
|
+
/** Result of {@link measureMenuTree}. */
|
|
271
|
+
interface MenuTreeMeasurement {
|
|
272
|
+
/** Deepest nesting level; `0` for an empty tree, `1` for flat items. */
|
|
273
|
+
depth: number;
|
|
274
|
+
/** Total node count across every level. */
|
|
275
|
+
nodeCount: number;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Measure a (possibly untrusted) tree's depth + node count **iteratively**, so
|
|
279
|
+
* measuring can never blow the stack the way a recursive walk over a hostile
|
|
280
|
+
* payload would. Stops early once either hard bound is exceeded and reports
|
|
281
|
+
* the over-limit value — the caller turns that into a validation error.
|
|
282
|
+
*
|
|
283
|
+
* Accepts `unknown` on purpose: this runs BEFORE the Zod parse (bounding the
|
|
284
|
+
* work that parse will do), so it can't assume a well-formed shape.
|
|
285
|
+
*/
|
|
286
|
+
declare function measureMenuTree(tree: unknown): MenuTreeMeasurement;
|
|
287
|
+
/**
|
|
288
|
+
* Cross-field check: does `tree` nest no deeper than the menu's own
|
|
289
|
+
* `maxDepth`? Lives outside the node schema because a per-node Zod refinement
|
|
290
|
+
* has no visibility of the sibling `maxDepth` COLUMN — the behavior calls this
|
|
291
|
+
* after the recursive parse.
|
|
292
|
+
*/
|
|
293
|
+
declare function validateTreeDepth(tree: unknown, maxDepth: number): boolean;
|
|
294
|
+
//#endregion
|
|
295
|
+
export { validateTreeDepth as _, MAX_ANCHOR_LENGTH as a, menuAcceptsEntityType as b, MAX_MENU_URL_LENGTH as c, MenuLinkValue as d, MenuSchemaOptions as f, measureMenuTree as g, buildMenuTreeSchema as h, LinkValueSchema as i, MenuItem as l, buildMenuItemSchema as m, BlockAnchor as n, MAX_MENU_DEPTH as o, MenuTreeMeasurement as p, DEFAULT_MENU_DEPTH as r, MAX_MENU_TREE_NODES as s, ANCHOR_PATTERN as t, MenuItemSummary as u, MAX_MENU_ENTITY_TYPE_WHITELIST as v, normalizeMenuEntityTypes as x, MenuEntityTypesSchema as y };
|
|
296
|
+
//# sourceMappingURL=menu-item-schema-xXWZKw7t.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu-item-schema-xXWZKw7t.d.mts","names":[],"sources":["../src/menu-entity-types.ts","../src/menu-item-schema.ts"],"mappings":";;;;;ACqC+B;AAS/B;;;;AAAgC;AAWhC;;;;AAAgC;cDHnB,8BAAA;;;;ACuBiB;AAmB9B;;;;AAAgD;cD5BnC,qBAAA,EAAuB,CAAA,CAAE,OAAO,kBAAkB,CAAA,CAAE,UAAA;;;;;;;;;;;iBAwBjD,wBAAA,CAAyB,KAAc;;ACkC7B;AAgB1B;;;;;;;;;iBDpBgB,qBAAA,CAAsB,KAAA,WAAgB,UAAkB;;;;AAAA;;;;cCxF3D,cAAA;;cAGA,kBAAA;;AAHc;AAG3B;;;;AAA+B;cASlB,mBAAA;;;;AAAmB;AAWhC;;;;AAAgC;cAAnB,mBAAA;;;;AAoBiB;AAmB9B;;;;AAAgD;AA2BhD;;cA9Ca,iBAAA;;AAiDa;AAgB1B;;;;;cA9Ca,cAAA,EAAc,MAAqB;;;;;;;;;;;;;;;;AAiE5B;AAcpB;;;;;;;;AAWW;KA/DC,aAAA;EACN,IAAA;EAAgB,MAAA;EAAgB,EAAA;EAAY,MAAA;AAAA;EAC5C,IAAA;EAAa,GAAA;AAAA;EACb,IAAA;EAAe,KAAA;AAAA;;;;;;;;;;;;;;;KAgBT,QAAA;2DAEV,EAAA;EAEA,IAAA;EAiF0B;;;;;;;;EAxE1B,IAAA,GAAO,aAAA;EAEP,KAAA,EAAO,MAAA;EAEP,OAAA,GAAU,MAAA;EACV,MAAA;EACA,QAAA,EAAU,QAAA;AAAA;;;;;;;;;;;;UAcK,eAAA;;;;;;;EAOf,KAAA;;EAEA,MAAA;;EAEA,SAAA;AAAA;;;;;;UAQe,WAAA;;EAEf,MAAA;;EAEA,KAAK;AAAA;;UAIU,iBAAA;;;;;;EAMf,cAAc;AAAA;;;;;AAmHhB;;;;;;;;;;;;cAhGa,eAAA,EAAe,CAAA,CAAA,qBAAA,UAAA,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqN5B;;;;AAAiE;;AAAjE,iBArHgB,mBAAA,CACd,OAAA,EAAS,iBAAA,GACR,CAAA,CAAE,OAAA,CAAQ,QAAA,EAAU,CAAA,CAAE,UAAA;;iBAkDT,mBAAA,CACd,OAAA,EAAS,iBAAA,GACR,CAAA,CAAE,OAAA,CAAQ,QAAA,IAAY,CAAA,CAAE,UAAA;;UAKV,mBAAA;;EAEf,KAAA;;EAEA,SAAS;AAAA;;;;;;;;;;iBAYK,eAAA,CAAgB,IAAA,YAAgB,mBAAmB;;;;;;;iBA0CnD,iBAAA,CAAkB,IAAA,WAAe,QAAgB"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Menus": {
|
|
3
|
+
"nav": {
|
|
4
|
+
"menus": "Menus"
|
|
5
|
+
},
|
|
6
|
+
"entity": {
|
|
7
|
+
"menu": "Menu",
|
|
8
|
+
"menus": "Menus",
|
|
9
|
+
"handle": "Handle",
|
|
10
|
+
"title": "Title",
|
|
11
|
+
"maxDepth": "Maximum depth",
|
|
12
|
+
"tree": "Items"
|
|
13
|
+
},
|
|
14
|
+
"itemKind": {
|
|
15
|
+
"link": "Link",
|
|
16
|
+
"header": "Header"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Menus": {
|
|
3
|
+
"nav": {
|
|
4
|
+
"menus": "Menüüd"
|
|
5
|
+
},
|
|
6
|
+
"entity": {
|
|
7
|
+
"menu": "Menüü",
|
|
8
|
+
"menus": "Menüüd",
|
|
9
|
+
"handle": "Tunnus",
|
|
10
|
+
"title": "Pealkiri",
|
|
11
|
+
"maxDepth": "Maksimaalne sügavus",
|
|
12
|
+
"tree": "Elemendid"
|
|
13
|
+
},
|
|
14
|
+
"itemKind": {
|
|
15
|
+
"link": "Link",
|
|
16
|
+
"header": "Pealkiri"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Menus": {
|
|
3
|
+
"nav": {
|
|
4
|
+
"menus": "Меню"
|
|
5
|
+
},
|
|
6
|
+
"entity": {
|
|
7
|
+
"menu": "Меню",
|
|
8
|
+
"menus": "Меню",
|
|
9
|
+
"handle": "Идентификатор",
|
|
10
|
+
"title": "Название",
|
|
11
|
+
"maxDepth": "Максимальная глубина",
|
|
12
|
+
"tree": "Элементы"
|
|
13
|
+
},
|
|
14
|
+
"itemKind": {
|
|
15
|
+
"link": "Ссылка",
|
|
16
|
+
"header": "Заголовок"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { ToolkitApp } from "@murumets-ee/core";
|
|
2
|
+
|
|
3
|
+
//#region src/get-menu.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* `getMenu()` — turn a stored menu tree into a render-ready structure for one
|
|
6
|
+
* locale (plan/routing phase 11 §2), plus the pure `markActiveTrail()` helper
|
|
7
|
+
* (phase 11 §3).
|
|
8
|
+
*
|
|
9
|
+
* **Leaf module.** Every cross-package capability (the menu row read, the
|
|
10
|
+
* `toolkit_paths` forward index, the publish gate, live entity titles) arrives
|
|
11
|
+
* through the injected {@link MenuResolveDeps}; nothing here imports
|
|
12
|
+
* `@murumets-ee/core` or `@murumets-ee/content`. Concrete deps are built from a
|
|
13
|
+
* live app in `./menu-resolve-context.ts` — the same leaf/seam split
|
|
14
|
+
* `@murumets-ee/content`'s `paths/enumerate.ts` ↔ `paths/resolve-context.ts`
|
|
15
|
+
* uses, and the reason the deps' function types are declared structurally here
|
|
16
|
+
* rather than imported (assignability is still checked, at the seam).
|
|
17
|
+
*
|
|
18
|
+
* **Security.** The resolved output feeds PUBLIC page navigation, so it must
|
|
19
|
+
* never surface an unpublished or deleted document:
|
|
20
|
+
* - the menu itself is read through a publish-filtered client (an unpublished
|
|
21
|
+
* menu resolves to `null` — nothing renders),
|
|
22
|
+
* - every entity-linked item must have BOTH a canonical path in
|
|
23
|
+
* `toolkit_paths` for the requested locale AND survive the batched publish
|
|
24
|
+
* gate; missing either → the item (and its subtree) is dropped,
|
|
25
|
+
* - `url` / `email` targets are re-gated through the same `sanitizeHref`
|
|
26
|
+
* protocol allowlist the write path uses, so a value that reached the column
|
|
27
|
+
* by some route other than the entity API (a direct DB edit, a restored
|
|
28
|
+
* dump) still cannot become a `javascript:` href,
|
|
29
|
+
* - every filtering decision fails CLOSED — anything unresolvable is dropped,
|
|
30
|
+
* never rendered with a placeholder.
|
|
31
|
+
*
|
|
32
|
+
* **No unbounded fan-out** (CLAUDE.md). Backend work is bounded by the number
|
|
33
|
+
* of distinct ENTITY TYPES in the tree, never by the number of items: the walk
|
|
34
|
+
* collects ids grouped by type, then issues at most THREE batched calls per
|
|
35
|
+
* group (forward index → publish gate → titles). The group count is itself hard-
|
|
36
|
+
* capped ({@link MAX_MENU_ENTITY_TYPES}) and the walk carries a node budget
|
|
37
|
+
* ({@link MAX_MENU_TREE_NODES}), so one render costs a fixed, small number of
|
|
38
|
+
* round-trips no matter what the stored tree looks like.
|
|
39
|
+
*
|
|
40
|
+
* **Read-time validation is per-node, not whole-tree.** The write path already
|
|
41
|
+
* runs `buildMenuTreeSchema()` (`menuTreeValidation()`), and this module
|
|
42
|
+
* deliberately does NOT re-run it: that schema whitelists locale KEYS against
|
|
43
|
+
* the app's configured locales, so re-parsing at read time would fail the WHOLE
|
|
44
|
+
* menu — site navigation gone — the moment an operator removes a locale from
|
|
45
|
+
* config after a menu was saved. The locale whitelist is a write-time authoring
|
|
46
|
+
* check with no read-time security value. What DOES matter at read time (node
|
|
47
|
+
* shape, the `LinkValue` protocol gate) is re-checked per node here, and a
|
|
48
|
+
* malformed node is dropped on its own without taking the menu down with it.
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* Hard cap on the number of DISTINCT entity types one menu may fan out over.
|
|
52
|
+
*
|
|
53
|
+
* The batched pipeline costs three DB round-trips per type group, so this — not
|
|
54
|
+
* the item count — is what bounds a render's backend work. A navigation menu
|
|
55
|
+
* pointing at more than a handful of routable types is malformed by
|
|
56
|
+
* construction; types past the cap are dropped (their items disappear) and
|
|
57
|
+
* logged, never silently truncated.
|
|
58
|
+
*/
|
|
59
|
+
declare const MAX_MENU_ENTITY_TYPES = 16;
|
|
60
|
+
/** One render-ready menu item (phase 11 §2 — exact shape). */
|
|
61
|
+
interface ResolvedMenuItem {
|
|
62
|
+
/** The stored item's stable id (DND identity; also a stable React key). */
|
|
63
|
+
id: string;
|
|
64
|
+
kind: 'link' | 'header';
|
|
65
|
+
/** Resolved label: the per-locale override, else the live entity title. */
|
|
66
|
+
label: string;
|
|
67
|
+
/**
|
|
68
|
+
* Locale-relative canonical path from `toolkit_paths` (+ `#anchor` when set),
|
|
69
|
+
* external `url` / `email` targets verbatim (protocol-gated), `null` for
|
|
70
|
+
* `header` items.
|
|
71
|
+
*/
|
|
72
|
+
href: string | null;
|
|
73
|
+
newTab: boolean;
|
|
74
|
+
/** Exact `currentPath` match on the path part (any `#anchor` ignored). */
|
|
75
|
+
isActive: boolean;
|
|
76
|
+
/** An ancestor BY TREE of the active item — never a path-prefix guess. */
|
|
77
|
+
inActiveTrail: boolean;
|
|
78
|
+
children: ResolvedMenuItem[];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A resolved menu.
|
|
82
|
+
*
|
|
83
|
+
* Phase 11 §2 names `ResolvedMenuItem` and uses `ResolvedMenu` as `getMenu`'s
|
|
84
|
+
* return type without spelling its fields out; this is the minimal wrapper that
|
|
85
|
+
* satisfies it — the requested `handle` (so a caller holding several resolved
|
|
86
|
+
* menus can tell them apart, and so `markActiveTrail` can round-trip a menu
|
|
87
|
+
* without losing its identity) plus the item forest. Deliberately carries no
|
|
88
|
+
* `maxDepth` / `title`: the depth clamp is already APPLIED in `items`, and a
|
|
89
|
+
* menu's admin-facing title is not a rendering concern.
|
|
90
|
+
*/
|
|
91
|
+
interface ResolvedMenu {
|
|
92
|
+
handle: string;
|
|
93
|
+
items: ResolvedMenuItem[];
|
|
94
|
+
}
|
|
95
|
+
/** Options for {@link getMenu} (phase 11 §2). */
|
|
96
|
+
interface GetMenuOptions {
|
|
97
|
+
/** The locale to resolve labels, visibility and canonical paths for. */
|
|
98
|
+
locale: string;
|
|
99
|
+
/**
|
|
100
|
+
* The page being rendered, for active-trail marking. SSG frontends usually
|
|
101
|
+
* OMIT this (one build-time fetch) and call {@link markActiveTrail} per page.
|
|
102
|
+
*/
|
|
103
|
+
currentPath?: string | undefined;
|
|
104
|
+
}
|
|
105
|
+
/** The projected `menu` row {@link getMenu} reads. */
|
|
106
|
+
interface MenuRow {
|
|
107
|
+
/**
|
|
108
|
+
* The menu's own depth cap. Absent / out of range falls back to the entity
|
|
109
|
+
* default — the clamp is renderer-enforced independently of the editor
|
|
110
|
+
* (phase 07 §2.7), so a bad stored value can never widen it.
|
|
111
|
+
*/
|
|
112
|
+
maxDepth?: number | null | undefined;
|
|
113
|
+
/** Raw `menu.tree` JSONB. Untrusted shape — validated per node during the walk. */
|
|
114
|
+
tree: unknown;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Structural minimum of a pino/toolkit logger. Declared here (rather than
|
|
118
|
+
* imported) so this module stays import-free; `app.logger.child(...)` satisfies
|
|
119
|
+
* it structurally.
|
|
120
|
+
*/
|
|
121
|
+
interface MenuResolveLogger {
|
|
122
|
+
warn(obj: object, msg: string): void;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Everything {@link getMenu} needs from the outside world.
|
|
126
|
+
*
|
|
127
|
+
* Function-shaped (not reader-shaped) on purpose: the concrete implementations
|
|
128
|
+
* live in `./menu-resolve-context.ts`, where assignability against
|
|
129
|
+
* `@murumets-ee/content`'s real `getCanonicalPaths` / `PublishFilter` is
|
|
130
|
+
* type-checked at the seam — so this module needs no `@murumets-ee/content`
|
|
131
|
+
* import and drift is still a build error.
|
|
132
|
+
*/
|
|
133
|
+
interface MenuResolveDeps {
|
|
134
|
+
/**
|
|
135
|
+
* Fetch the PUBLISHED menu row by handle, or `null`.
|
|
136
|
+
*
|
|
137
|
+
* The publish filter is the implementation's responsibility (the app-side
|
|
138
|
+
* factory reads through a publish-filtering `QueryClient`) — a draft menu
|
|
139
|
+
* must never resolve, per phase 11 §2.
|
|
140
|
+
*/
|
|
141
|
+
getMenuRow: (handle: string) => Promise<MenuRow | null>;
|
|
142
|
+
/**
|
|
143
|
+
* Batched forward index read: which of `ids` have a canonical path in
|
|
144
|
+
* `locale`? Does NOT publish-filter — {@link MenuResolveDeps.filterPublished}
|
|
145
|
+
* is the required second step.
|
|
146
|
+
*/
|
|
147
|
+
resolveCanonicalPaths: (entityType: string, ids: readonly string[], locale: string) => Promise<ReadonlyMap<string, string>>;
|
|
148
|
+
/** Batched publish gate: which of `ids` are published in `locale`? */
|
|
149
|
+
filterPublished: (entityType: string, ids: readonly string[], locale: string) => Promise<ReadonlySet<string>>;
|
|
150
|
+
/**
|
|
151
|
+
* Batched live-title read: `id → display title` for `locale`. Only called for
|
|
152
|
+
* the ids that actually need one (no per-locale label override), and never
|
|
153
|
+
* for `header` items (they have no linked entity to borrow a title from).
|
|
154
|
+
*/
|
|
155
|
+
resolveTitles: (entityType: string, ids: readonly string[], locale: string) => Promise<ReadonlyMap<string, string>>;
|
|
156
|
+
/**
|
|
157
|
+
* Fallback locale for label overrides — the app's configured content
|
|
158
|
+
* `defaultLocale`. A label present only in the default locale still renders
|
|
159
|
+
* (the same locale → default fallback `mergeTranslations` gives entity data),
|
|
160
|
+
* instead of silently dropping a partially-translated item.
|
|
161
|
+
*/
|
|
162
|
+
defaultLocale: string;
|
|
163
|
+
/** Optional — used to report DROPPED work (never silent truncation). */
|
|
164
|
+
logger?: MenuResolveLogger | undefined;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Mark `isActive` / `inActiveTrail` on a resolved menu for one page.
|
|
168
|
+
*
|
|
169
|
+
* PURE — no I/O, and the input menu is not mutated (a new tree is returned), so
|
|
170
|
+
* an SSG build can fetch a menu ONCE and mark it per page without the pages
|
|
171
|
+
* contaminating each other. `getMenu` calls this internally when
|
|
172
|
+
* `opts.currentPath` is given, so there is exactly one implementation of the
|
|
173
|
+
* ancestry walk.
|
|
174
|
+
*
|
|
175
|
+
* `isActive` is an EXACT match on the path part; `inActiveTrail` marks strict
|
|
176
|
+
* tree ancestors of an active item (an active item is not its own ancestor).
|
|
177
|
+
*/
|
|
178
|
+
declare function markActiveTrail(menu: ResolvedMenu, currentPath: string): ResolvedMenu;
|
|
179
|
+
/**
|
|
180
|
+
* Resolve a stored menu into a render-ready tree for one locale (phase 11 §2).
|
|
181
|
+
*
|
|
182
|
+
* Returns `null` when the menu does not exist or is not published — a draft
|
|
183
|
+
* menu renders nowhere public. A published menu whose every item was filtered
|
|
184
|
+
* out returns an EMPTY item list, not `null`: "the menu exists but has nothing
|
|
185
|
+
* to show here" is a different fact from "there is no such menu".
|
|
186
|
+
*
|
|
187
|
+
* Filtering order (phase 11 §2, not reorderable): menu unpublished → `null`;
|
|
188
|
+
* `enabled[locale] === false` → dropped with its subtree; entity target deleted
|
|
189
|
+
* or unpublished-in-locale → dropped; header with no surviving children →
|
|
190
|
+
* dropped; depth clamped to the menu's own `maxDepth`.
|
|
191
|
+
*/
|
|
192
|
+
declare function getMenu(handle: string, opts: GetMenuOptions, deps: MenuResolveDeps): Promise<ResolvedMenu | null>;
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/menu-resolve-context.d.ts
|
|
195
|
+
/**
|
|
196
|
+
* Build the concrete {@link MenuResolveDeps} for an app.
|
|
197
|
+
*
|
|
198
|
+
* Cheap to call per request (it constructs no clients eagerly — each read
|
|
199
|
+
* builds its own `QueryClient`, matching `buildEnumerateDeps`'s per-call shape,
|
|
200
|
+
* so this never depends on plugin boot ORDER).
|
|
201
|
+
*/
|
|
202
|
+
declare function buildMenuResolveDeps(app: ToolkitApp): MenuResolveDeps;
|
|
203
|
+
//#endregion
|
|
204
|
+
export { type GetMenuOptions, MAX_MENU_ENTITY_TYPES, type MenuResolveDeps, type MenuResolveLogger, type MenuRow, type ResolvedMenu, type ResolvedMenuItem, buildMenuResolveDeps, getMenu, markActiveTrail };
|
|
205
|
+
//# sourceMappingURL=resolve.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.mts","names":[],"sources":["../src/get-menu.ts","../src/menu-resolve-context.ts"],"mappings":";;;;;;AAqEA;;;;AAAkC;AAelC;;;;;;;;;;;;;;AAiB4B;AAc5B;;;;;;;;AAEyB;AAIzB;;;;AAOa;AAQb;;;;AAQM;AAQN;;;;;;;;AAC+B;AAY/B;;;;cAhGa,qBAAA;;UAeI,gBAAA;EAyGF;EAvGb,EAAA;EACA,IAAA;EAgHK;EA9GL,KAAA;EAuH0B;;;;;EAjH1B,IAAA;EACA,MAAA;EAoFE;EAlFF,QAAA;EAoFE;EAlFF,aAAA;EACA,QAAA,EAAU,gBAAgB;AAAA;;;;;;;;;;;;UAcX,YAAA;EACf,MAAA;EACA,KAAA,EAAO,gBAAgB;AAAA;AA2FG;AAAA,UAvFX,cAAA;EA+cc;EA7c7B,MAAA;EA6coF;;;;EAxcpF,WAAW;AAAA;AAwcyE;AAAA,UAhcrE,OAAA;EAqdY;;;;;EA/c3B,QAAA;EAmdQ;EAjdR,IAAI;AAAA;;;;;;UAQW,iBAAA;EACf,IAAA,CAAK,GAAA,UAAa,GAAA;AAAA;;;;AC7FpB;;;;;;UDyGiB,eAAA;ECzGqD;AAAA;;;;;;EDiHpE,UAAA,GAAa,MAAA,aAAmB,OAAA,CAAQ,OAAA;;;;;;EAMxC,qBAAA,GACE,UAAA,UACA,GAAA,qBACA,MAAA,aACG,OAAA,CAAQ,WAAA;;EAEb,eAAA,GACE,UAAA,UACA,GAAA,qBACA,MAAA,aACG,OAAA,CAAQ,WAAA;;;;;;EAMb,aAAA,GACE,UAAA,UACA,GAAA,qBACA,MAAA,aACG,OAAA,CAAQ,WAAA;;;;;;;EAOb,aAAA;;EAEA,MAAA,GAAS,iBAAA;AAAA;;;;;;;;;;;;;iBAwXK,eAAA,CAAgB,IAAA,EAAM,YAAA,EAAc,WAAA,WAAsB,YAAY;;;;;;;;;;;;;;iBAqBhE,OAAA,CACpB,MAAA,UACA,IAAA,EAAM,cAAA,EACN,IAAA,EAAM,eAAA,GACL,OAAA,CAAQ,YAAA;;;;;;;;AA5ec;AAIzB;iBC7DgB,oBAAA,CAAqB,GAAA,EAAK,UAAA,GAAa,eAAe"}
|
package/dist/resolve.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{s as e}from"./menu-entity-types-BnKnKbif.mjs";import{t}from"./entity-BxLeqoBC.mjs";import{resolveLinkHref as n}from"@murumets-ee/blocks/links";import{getContentConfig as r}from"@murumets-ee/content/plugin";import{getEntityTitleField as i}from"@murumets-ee/entity";import{buildCanonicalPathsBatchDeps as a,buildEnumerateDeps as o,getCanonicalPaths as s}from"@murumets-ee/content/resolve";import{QueryClient as c}from"@murumets-ee/entity/query";import{eq as l,inArray as u}from"drizzle-orm";const d=16;function f(e,t,n){if(typeof e!=`object`||!e||Array.isArray(e))return;let r=e;for(let e of t===n?[t]:[t,n]){if(!Object.hasOwn(r,e))continue;let t=r[e];if(typeof t==`string`&&t.length>0)return t}}function p(e,t){if(typeof e!=`object`||!e||Array.isArray(e))return;let n=e;if(!Object.hasOwn(n,t))return;let r=n[t];return typeof r==`boolean`?r:void 0}function m(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function h(t,n,r,i){let a=new Map,o=new Map,s=500,c=0;function l(e,t,n){let r=e.get(t);r?r.add(n):e.set(t,new Set([n]))}function u(t,d){if(d>i||!Array.isArray(t))return[];let h=t,g=[];for(let t of h){if(s<=0){c++;continue}if(!m(t))continue;let i=t.id,h=t.kind;if(typeof i!=`string`||i.length===0||h!==`link`&&h!==`header`||p(t.enabled,n)===!1)continue;s--;let _;if(h===`link`){let n=e.safeParse(t.link);if(!n.success)continue;_=n.data}else if(t.link!==void 0)continue;let v=u(t.children,d+1);_?.kind===`entity`&&(l(a,_.entity,_.id),f(t.label,n,r)===void 0&&l(o,_.entity,_.id)),g.push({id:i,kind:h,link:_,label:t.label,newTab:t.newTab===!0,children:v})}return g}return{nodes:u(t,1),idsByType:a,titleIdsByType:o,droppedForBudget:c}}async function g(e,t,n,r){let i=new Map,a=new Map,o=new Map,s=[...e.idsByType.entries()];return s.length>16&&n.logger?.warn({handle:r,locale:t,entityTypes:s.length,cap:16},`menu resolution dropped entity-type groups over the fan-out cap`),await Promise.all(s.slice(0,16).map(async([r,s])=>{let c=[...s],l=await n.resolveCanonicalPaths(r,c,t);if(i.set(r,l),l.size===0)return;let u=[...l.keys()],d=await n.filterPublished(r,u,t);a.set(r,d);let f=e.titleIdsByType.get(r);if(f===void 0)return;let p=u.filter(e=>f.has(e)&&d.has(e));p.length!==0&&o.set(r,await n.resolveTitles(r,p,t))})),{paths:i,published:a,titles:o}}function _(e,t,r,i){let a=[];for(let o of e){let e=_(o.children,t,r,i),s=f(o.label,t,i.defaultLocale);if(o.kind===`header`){if(e.length===0||s===void 0)continue;a.push({id:o.id,kind:`header`,label:s,href:null,newTab:!1,isActive:!1,inActiveTrail:!1,children:e});continue}let c=o.link;if(c===void 0)continue;let l,u=s;if(c.kind===`entity`){let e=r.paths.get(c.entity)?.get(c.id);if(e===void 0||r.published.get(c.entity)?.has(c.id)!==!0)continue;l=c.anchor===void 0?e:`${e}#${c.anchor}`,u??=r.titles.get(c.entity)?.get(c.id)}else l=n(c);l===null||l.length===0||u===void 0||u.length===0||a.push({id:o.id,kind:`link`,label:u,href:l,newTab:o.newTab,isActive:!1,inActiveTrail:!1,children:e})}return a}function v(e){let t=e.indexOf(`#`),n=t===-1?e:e.slice(0,t);return n.length>1&&n.endsWith(`/`)?n.slice(0,-1):n}function y(e,t){let n=!1;return{items:e.map(e=>{let r=y(e.children,t),i=e.href!==null&&v(e.href)===t,a=r.containsActive;return(i||a)&&(n=!0),{...e,isActive:i,inActiveTrail:a,children:r.items}}),containsActive:n}}function b(e,t){return{...e,items:y(e.items,v(t)).items}}async function x(e,t,n){if(e.length===0||e.length>128)return null;let r=await n.getMenuRow(e);if(r===null)return null;let i=r.maxDepth,a=typeof i==`number`&&Number.isInteger(i)&&i>=1?Math.min(i,6):3,o=h(r.tree,t.locale,n.defaultLocale,a);o.droppedForBudget>0&&n.logger?.warn({handle:e,locale:t.locale,dropped:o.droppedForBudget,cap:500},`menu resolution dropped items over the node budget`);let s=await g(o,t.locale,n,e),c={handle:e,items:_(o.nodes,t.locale,s,n)};return t.currentPath===void 0?c:b(c,t.currentPath)}function S(e,t){return e[t]}const C=()=>({user:{id:`system`,groups:[`admin`]},checker:()=>!0});function w(e){let n=e.db.readOnly,d=a(e),{filterPublished:f}=o(e);return{getMenuRow:async r=>{let i=new c({entity:t,db:n,logger:e.logger.child({resolve:`menu`}),contextResolver:C}),a=(await i.findMany({where:l(S(i.getTable(),`handle`),r),limit:1}))[0];return a===void 0?null:{maxDepth:a.maxDepth,tree:a.tree}},resolveCanonicalPaths:(e,t,n)=>s(e,t,n,d),filterPublished:f,resolveTitles:async(t,r,a)=>{let o=new Map;if(r.length===0)return o;let s=e.entities.get(t);if(s===void 0)return o;let l=i(s);if(l===`id`)return o;let d=new c({entity:s,db:n,logger:e.logger.child({resolve:`menu-title`,entity:t}),contextResolver:C}),f=S(d.getTable(),`id`);for(let e=0;e<r.length;e+=500){let t=r.slice(e,e+500),n=await d.findMany({where:u(f,[...t]),locale:a,limit:t.length});for(let e of n){let t=e,n=t.id,r=t[l];typeof n==`string`&&typeof r==`string`&&r.length>0&&o.set(n,r)}}return o},defaultLocale:r().defaultLocale,logger:e.logger.child({resolve:`menu`})}}export{d as MAX_MENU_ENTITY_TYPES,w as buildMenuResolveDeps,x as getMenu,b as markActiveTrail};
|
|
2
|
+
//# sourceMappingURL=resolve.mjs.map
|