@murumets-ee/menus 0.0.0-preview-20261014110621

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.
Files changed (44) hide show
  1. package/LICENSE +94 -0
  2. package/dist/admin.d.mts +363 -0
  3. package/dist/admin.d.mts.map +1 -0
  4. package/dist/admin.mjs +2 -0
  5. package/dist/admin.mjs.map +1 -0
  6. package/dist/columns-DwDukx_r.mjs +2 -0
  7. package/dist/columns-DwDukx_r.mjs.map +1 -0
  8. package/dist/en-BcniJZiB.mjs +2 -0
  9. package/dist/en-BcniJZiB.mjs.map +1 -0
  10. package/dist/entity-BC8r_5xy.mjs +2 -0
  11. package/dist/entity-BC8r_5xy.mjs.map +1 -0
  12. package/dist/et-BL3ZuuFV.mjs +2 -0
  13. package/dist/et-BL3ZuuFV.mjs.map +1 -0
  14. package/dist/i18n.d.mts +16 -0
  15. package/dist/i18n.d.mts.map +1 -0
  16. package/dist/i18n.mjs +2 -0
  17. package/dist/i18n.mjs.map +1 -0
  18. package/dist/index.d.mts +75 -0
  19. package/dist/index.d.mts.map +1 -0
  20. package/dist/index.mjs +1 -0
  21. package/dist/menu-entity-types-BnKnKbif.mjs +2 -0
  22. package/dist/menu-entity-types-BnKnKbif.mjs.map +1 -0
  23. package/dist/menu-item-schema-_Nj03ytp.d.mts +296 -0
  24. package/dist/menu-item-schema-_Nj03ytp.d.mts.map +1 -0
  25. package/dist/menu-resolve-context-DLIUP8RY.mjs +2 -0
  26. package/dist/menu-resolve-context-DLIUP8RY.mjs.map +1 -0
  27. package/dist/messages/en.json +22 -0
  28. package/dist/messages/et.json +22 -0
  29. package/dist/messages/ru.json +22 -0
  30. package/dist/resolve.d.mts +205 -0
  31. package/dist/resolve.d.mts.map +1 -0
  32. package/dist/resolve.mjs +2 -0
  33. package/dist/resolve.mjs.map +1 -0
  34. package/dist/ru-D8QNW6XE.mjs +2 -0
  35. package/dist/ru-D8QNW6XE.mjs.map +1 -0
  36. package/dist/schema.d.mts +280 -0
  37. package/dist/schema.d.mts.map +1 -0
  38. package/dist/schema.mjs +2 -0
  39. package/dist/schema.mjs.map +1 -0
  40. package/dist/seed.d.mts +67 -0
  41. package/dist/seed.d.mts.map +1 -0
  42. package/dist/seed.mjs +2 -0
  43. package/dist/seed.mjs.map +1 -0
  44. package/package.json +62 -0
package/LICENSE ADDED
@@ -0,0 +1,94 @@
1
+ Elastic License 2.0 (ELv2)
2
+
3
+ URL: https://www.elastic.co/licensing/elastic-license
4
+
5
+ ## Acceptance
6
+
7
+ By using the software, you agree to all of the terms and conditions below.
8
+
9
+ ## Copyright License
10
+
11
+ The licensor grants you a non-exclusive, royalty-free, worldwide,
12
+ non-sublicensable, non-transferable license to use, copy, distribute, make
13
+ available, and prepare derivative works of the software, in each case subject
14
+ to the limitations and conditions below.
15
+
16
+ ## Limitations
17
+
18
+ You may not provide the software to third parties as a hosted or managed
19
+ service, where the service provides users with access to any substantial set
20
+ of the features or functionality of the software.
21
+
22
+ You may not move, change, disable, or circumvent the license key functionality
23
+ in the software, and you may not remove or obscure any functionality in the
24
+ software that is protected by the license key.
25
+
26
+ You may not alter, remove, or obscure any licensing, copyright, or other
27
+ notices of the licensor in the software. Any use of the licensor's trademarks
28
+ is subject to applicable law.
29
+
30
+ ## Patents
31
+
32
+ The licensor grants you a license, under any patent claims the licensor can
33
+ license, or becomes able to license, to make, have made, use, sell, offer for
34
+ sale, import and have imported the software, in each case subject to the
35
+ limitations and conditions in this license. This license does not cover any
36
+ patent claims that you cause to be infringed by modifications or additions to
37
+ the software. If you or your company make any written claim that the software
38
+ infringes or contributes to infringement of any patent, your patent license
39
+ for the software granted under these terms ends immediately. If your company
40
+ makes such a claim, your patent license ends immediately for work on behalf
41
+ of your company.
42
+
43
+ ## Notices
44
+
45
+ You must ensure that anyone who gets a copy of any part of the software from
46
+ you also gets a copy of these terms.
47
+
48
+ If you modify the software, you must include in any modified copies of the
49
+ software prominent notices stating that you have modified the software.
50
+
51
+ ## No Other Rights
52
+
53
+ These terms do not imply any licenses other than those expressly granted in
54
+ these terms.
55
+
56
+ ## Termination
57
+
58
+ If you use the software in violation of these terms, such use is not licensed,
59
+ and your licenses will automatically terminate. If the licensor provides you
60
+ with a notice of your violation, and you cease all violation of this license
61
+ no later than 30 days after you receive that notice, your licenses will be
62
+ reinstated retroactively. However, if you violate these terms after such
63
+ reinstatement, any additional violation of these terms will cause your
64
+ licenses to terminate automatically and permanently.
65
+
66
+ ## No Liability
67
+
68
+ As far as the law allows, the software comes as is, without any warranty or
69
+ condition, and the licensor will not be liable to you for any damages arising
70
+ out of these terms or the use or nature of the software, under any kind of
71
+ legal claim.
72
+
73
+ ## Definitions
74
+
75
+ The **licensor** is the entity offering these terms, and the **software** is
76
+ the software the licensor makes available under these terms, including any
77
+ portion of it.
78
+
79
+ **you** refers to the individual or entity agreeing to these terms.
80
+
81
+ **your company** is any legal entity, sole proprietorship, or other kind of
82
+ organization that you work for, plus all organizations that have control over,
83
+ are under the control of, or are under common control with that organization.
84
+ **control** means ownership of substantially all the assets of an entity, or
85
+ the power to direct the management and policies of an entity (for example, by
86
+ voting right, contract, or otherwise). Control can be direct or indirect.
87
+
88
+ **your licenses** are all the licenses granted to you for the software under
89
+ these terms.
90
+
91
+ **use** means anything you do with the software requiring one of your
92
+ licenses.
93
+
94
+ **trademark** means trademarks, service marks, and similar rights.
@@ -0,0 +1,363 @@
1
+ import { z } from "zod";
2
+ import { AdminRoute, ToolkitApp } from "@murumets-ee/core";
3
+ import { TranslatableSource, TranslatableSourceText } from "@murumets-ee/content/translation";
4
+ import { ResolvedMenuNavItem } from "@murumets-ee/blocks";
5
+
6
+ //#region src/menu-item-schema.d.ts
7
+ /**
8
+ * Absolute nesting cap for any menu, independent of a menu's own `maxDepth`
9
+ * (phase 07 §2.7 — "depth is capped per menu (default 3, max 6)"). Also the
10
+ * bound that keeps the recursive parse from being a DoS vector.
11
+ */
12
+ declare const MAX_MENU_DEPTH = 6;
13
+ /**
14
+ * Upper bound on the number of nodes in one menu tree. Navigation menus are
15
+ * hand-curated: a few dozen items is a big one. The cap exists so a single
16
+ * PATCH can't hand the validator (and `extractRefs`' ref walk, and every
17
+ * subsequent render) an unbounded amount of work — CLAUDE.md's
18
+ * "no unbounded fan-out" rule applied to a single request payload.
19
+ */
20
+ declare const MAX_MENU_TREE_NODES = 500;
21
+ /**
22
+ * Max length of an entity-link fragment/anchor.
23
+ *
24
+ * Exported because this module is the SINGLE OWNER of the anchor bounds: the
25
+ * write-side schema below, the read-side anchor walk
26
+ * (`admin/anchors.ts`) and the editor's manual-entry fallback
27
+ * (`@murumets-ee/menus-ui`'s anchor picker, via the client-safe `./schema`
28
+ * subpath) all validate against exactly these two constants. Two of those three
29
+ * used to carry their own copy; a drift between them would let the picker offer
30
+ * — or the editor accept — a value the write boundary then rejects.
31
+ */
32
+ declare const MAX_ANCHOR_LENGTH = 128;
33
+ /**
34
+ * Anchors are slugified by the block editor (phase 08 §5) and rendered into an
35
+ * `href` as `#<anchor>`; restricting them to slug characters keeps that
36
+ * interpolation inert.
37
+ *
38
+ * Exported for the same single-owner reason as {@link MAX_ANCHOR_LENGTH}.
39
+ */
40
+ declare const ANCHOR_PATTERN: RegExp;
41
+ /**
42
+ * A menu item's link target — the item-type union phase 07 §2.2 and phase 08 §3
43
+ * define: `entity` (+ optional anchor) | `url` | `email`.
44
+ *
45
+ * **A NARROWED SUBSET of `@murumets-ee/blocks`' `LinkValue`, deliberately.**
46
+ * Blocks additionally carries a `media` kind; menus does NOT, because no phase
47
+ * gives a menu a media-href seam to resolve one with (phase 11 §2 specifies
48
+ * `href` as "canonical path from `toolkit_paths`; external URLs verbatim;
49
+ * header items null" and nothing else). Accepting a `media` link would let it
50
+ * validate, persist, and register an `entity_refs` row — arming delete
51
+ * protection for that file — while the renderer had no way to emit it: the item
52
+ * would silently never appear. Rejecting it at the write boundary turns that
53
+ * dead weight into a real 400. Widening later is additive (an arm here plus a
54
+ * `resolveMediaHref` dep on `MenuResolveDeps`) and needs no shape change.
55
+ *
56
+ * Every `MenuLinkValue` IS a valid blocks `LinkValue` (the subset direction, and
57
+ * the one `resolveLinkHref` needs) — pinned at compile time by an assignment in
58
+ * `menu-item-schema.test.ts`, so a drift in blocks' union is a build error
59
+ * rather than a silent divergence.
60
+ *
61
+ * The `anchor?: string | undefined` is forced by the combination of Zod and this
62
+ * repo's `exactOptionalPropertyTypes`: `.optional()` always widens to
63
+ * `T | undefined`, which is NOT assignable to blocks' `anchor?: string` under
64
+ * that flag.
65
+ */
66
+ type MenuLinkValue = {
67
+ kind: 'entity';
68
+ entity: string;
69
+ id: string;
70
+ anchor?: string | undefined;
71
+ } | {
72
+ kind: 'url';
73
+ url: string;
74
+ } | {
75
+ kind: 'email';
76
+ email: string;
77
+ };
78
+ /**
79
+ * A menu tree node (phase 08 §3 / D009).
80
+ *
81
+ * `label: null` means "use the LIVE title of the linked entity, per locale";
82
+ * a map is an explicit per-locale override. `enabled` is per-locale visibility
83
+ * in BOTH directions — absent/`true` shows the item, `false` hides it in that
84
+ * locale — so one tree serves every locale (no `menu_translations` table).
85
+ *
86
+ * Declared as a `type`, NOT an `interface`, on purpose: only type aliases get
87
+ * TypeScript's implicit index signature, and without it `MenuItem[]` is not
88
+ * assignable to the `Record<string, unknown>[]` arm of `AdminClient.create/
89
+ * update`'s json-field value type — every caller storing a typed tree would
90
+ * need a cast.
91
+ */
92
+ type MenuItem = {
93
+ /** Stable id — DND identity + anchor for audit diffs. */id: string; /** `header` = a text-only dropdown parent with no link of its own. */
94
+ kind: 'link' | 'header';
95
+ /**
96
+ * Present iff `kind === 'link'`.
97
+ *
98
+ * The explicit `| undefined` is required by the repo's
99
+ * `exactOptionalPropertyTypes` — Zod's `.optional()` emits
100
+ * `LinkValue | undefined`, and without it the schema wouldn't typecheck
101
+ * against this interface.
102
+ */
103
+ link?: MenuLinkValue | undefined; /** `null` = live entity title per locale; a map = explicit override. */
104
+ label: Record<string, string> | null; /** Per-locale visibility. Absent/`true` = visible; `false` = hidden. */
105
+ enabled?: Record<string, boolean> | undefined;
106
+ newTab?: boolean | undefined;
107
+ children: MenuItem[];
108
+ };
109
+ /**
110
+ * One menu item's target, as the editor needs to render its row
111
+ * (`@murumets-ee/menus/admin`'s `resolveMenuItemSummaries`).
112
+ *
113
+ * Declared here — not in `admin/summaries.ts` — for the same reason
114
+ * {@link BlockAnchor} is: it is a pure data shape with no server dependency,
115
+ * so the client-safe `./schema` subpath can re-export it without pulling the
116
+ * server-only resolver along. `import type` is erased at compile time either
117
+ * way, but a single canonical home (this leaf module) is simpler to reason
118
+ * about than "which of two server-only files owns the type".
119
+ */
120
+ interface MenuItemSummary {
121
+ /**
122
+ * The target row's live title, or `null` when the entity declares no
123
+ * display-able field. Unlike the public render path — which DROPS such an
124
+ * item rather than show a raw uuid as navigation — the admin editor still
125
+ * reports the target as present, just unlabelled.
126
+ */
127
+ title: string | null;
128
+ /** `false` = the target row is gone (deleted) → a BROKEN item. */
129
+ exists: boolean;
130
+ /** `null` when the entity isn't publishable — no draft/published distinction applies. */
131
+ published: boolean | null;
132
+ }
133
+ /**
134
+ * One addressable block anchor on a document (`@murumets-ee/menus/admin`'s
135
+ * `collectBlockAnchors` / the anchors endpoint). See {@link MenuItemSummary}'s
136
+ * doc for why this pure shape lives here rather than in `admin/anchors.ts`.
137
+ */
138
+ interface BlockAnchor {
139
+ /** The stable id rendered as the block element's `id` attribute. */
140
+ anchor: string;
141
+ /** Display text — `anchorLabel` when usable, else the anchor value itself. */
142
+ label: string;
143
+ }
144
+ /** Options for {@link buildMenuItemSchema} / {@link buildMenuTreeSchema}. */
145
+ interface MenuSchemaOptions {
146
+ /**
147
+ * Locale codes accepted as keys in `label` / `enabled` maps. Anything else
148
+ * is a validation error — a typo'd locale would produce a label no renderer
149
+ * ever reads (the same failure mode content's `validateLocale` guards).
150
+ */
151
+ allowedLocales: readonly string[];
152
+ }
153
+ /**
154
+ * The recursive per-node schema. Input is `unknown` (this parses untrusted
155
+ * JSONB), output is a fully-narrowed {@link MenuItem}. Unknown properties are
156
+ * STRIPPED by Zod's default object behavior, so writing the parse result back
157
+ * onto the payload also sanitizes it — nothing a caller invents gets stored.
158
+ */
159
+ declare function buildMenuItemSchema(options: MenuSchemaOptions): z.ZodType<MenuItem, z.ZodTypeDef, unknown>;
160
+ /** The whole tree — an array of {@link buildMenuItemSchema} nodes. */
161
+ declare function buildMenuTreeSchema(options: MenuSchemaOptions): z.ZodType<MenuItem[], z.ZodTypeDef, unknown>;
162
+ /** Result of {@link measureMenuTree}. */
163
+ interface MenuTreeMeasurement {
164
+ /** Deepest nesting level; `0` for an empty tree, `1` for flat items. */
165
+ depth: number;
166
+ /** Total node count across every level. */
167
+ nodeCount: number;
168
+ }
169
+ /**
170
+ * Measure a (possibly untrusted) tree's depth + node count **iteratively**, so
171
+ * measuring can never blow the stack the way a recursive walk over a hostile
172
+ * payload would. Stops early once either hard bound is exceeded and reports
173
+ * the over-limit value — the caller turns that into a validation error.
174
+ *
175
+ * Accepts `unknown` on purpose: this runs BEFORE the Zod parse (bounding the
176
+ * work that parse will do), so it can't assume a well-formed shape.
177
+ */
178
+ declare function measureMenuTree(tree: unknown): MenuTreeMeasurement;
179
+ /**
180
+ * Cross-field check: does `tree` nest no deeper than the menu's own
181
+ * `maxDepth`? Lives outside the node schema because a per-node Zod refinement
182
+ * has no visibility of the sibling `maxDepth` COLUMN — the behavior calls this
183
+ * after the recursive parse.
184
+ */
185
+ declare function validateTreeDepth(tree: unknown, maxDepth: number): boolean;
186
+ //#endregion
187
+ //#region src/translatable-source.d.ts
188
+ /** The largest page `list` returns, whatever the caller asks for. */
189
+ declare const MAX_MENU_SOURCE_PAGE = 200;
190
+ /**
191
+ * What identifies the tree a set of texts was read from: sha256 of its JSON.
192
+ *
193
+ * Any change to the tree — a label, an order, a new item — is a new revision,
194
+ * so `write` can refuse to lay translations over a tree that moved under them.
195
+ */
196
+ declare function menuTreeRevision(tree: unknown): string;
197
+ /**
198
+ * The labels of a stored tree that want a translation.
199
+ *
200
+ * An item contributes when its `label` is a MAP with a non-empty string for
201
+ * `sourceLocale` and — in `'missing'` mode — no non-empty string yet for
202
+ * `targetLocale`. `label: null` means "use the linked entity's live title",
203
+ * which is that entity's text, not the menu's, and is never offered.
204
+ */
205
+ declare function collectMenuLabelTexts(tree: unknown, input: {
206
+ sourceLocale: string;
207
+ targetLocale: string;
208
+ mode: 'missing' | 'all';
209
+ }): TranslatableSourceText[];
210
+ /**
211
+ * A copy of `tree` with `label[targetLocale]` set on each item named in
212
+ * `texts`. The input is not modified.
213
+ *
214
+ * Only an item whose label is already a map takes a translation: a `null`
215
+ * label stays `null` (it follows the linked entity's title in every locale),
216
+ * and an id in `texts` that matches no such item is ignored. A text is trimmed;
217
+ * an empty one is skipped; one longer than {@link MAX_LABEL_LENGTH} throws,
218
+ * naming the item.
219
+ */
220
+ declare function applyMenuLabelTranslations(tree: unknown, targetLocale: string, texts: Readonly<Record<string, string>>): {
221
+ tree: unknown[];
222
+ applied: number;
223
+ };
224
+ /**
225
+ * Menu labels, for a whole-site translation. Contributed on the menus plugin's
226
+ * `contributes['content.translatableSources']` via {@link menusContributions}.
227
+ */
228
+ declare const menuTranslatableSource: TranslatableSource;
229
+ /**
230
+ * Everything `@murumets-ee/menus` contributes to other plugins' slots, as the
231
+ * value of the menus plugin's `contributes` field.
232
+ *
233
+ * Declared HERE rather than inline in the factory because the factory lives in
234
+ * `@murumets-ee/menus-ui/plugin` (this package stays react-free) while what is
235
+ * contributed is server behaviour this package owns.
236
+ */
237
+ declare const menusContributions: {
238
+ 'content.translatableSources': TranslatableSource[];
239
+ };
240
+ //#endregion
241
+ //#region src/admin/editor-state.d.ts
242
+ /** The menu row projected to exactly what the embedded editor needs. */
243
+ interface MenuEditorStateMenu {
244
+ id: string;
245
+ handle: string;
246
+ title: string;
247
+ maxDepth: number;
248
+ status: 'draft' | 'published';
249
+ tree: MenuItem[];
250
+ /**
251
+ * Which content types this menu accepts links to (routing PR12), narrowed by
252
+ * `normalizeMenuEntityTypes`. `null` = accepts every type. Read with
253
+ * draft-then-live precedence, same as `tree`.
254
+ */
255
+ entityTypes: string[] | null;
256
+ }
257
+ /** One content type offered in the "Add link" picker's Content tab. */
258
+ interface MenuEditorStateEntityType {
259
+ name: string;
260
+ label: string;
261
+ }
262
+ /** Full response body for `GET /api/admin/menus/editor-state?handle=`. */
263
+ interface MenuEditorStateResponse {
264
+ menu: MenuEditorStateMenu;
265
+ /** Keyed `` `${entity}:${id}` `` — see `resolveMenuItemSummaries`. */
266
+ summaries: Readonly<Record<string, MenuItemSummary>>;
267
+ locales: ReadonlyArray<{
268
+ code: string;
269
+ label: string;
270
+ }>;
271
+ defaultLocale: string;
272
+ /**
273
+ * How many stored nodes the tolerant `normalizeMenuTree` could NOT read.
274
+ *
275
+ * Every menu write is whole-tree, so `> 0` means a save from this surface
276
+ * would PERMANENTLY delete those items — `<MenuTreeEditor>` refuses to save
277
+ * past it without an explicit confirm. Counted against the very read being
278
+ * handed back (not a separate projection), so the number always describes
279
+ * the tree the editor is about to write.
280
+ */
281
+ droppedItemCount: number;
282
+ entityTypes: readonly MenuEditorStateEntityType[];
283
+ canPublish: boolean;
284
+ /**
285
+ * The pending draft for this menu, when one exists.
286
+ *
287
+ * Structurally identical to `@murumets-ee/admin-ui`'s `DraftInfo` (declared
288
+ * here rather than imported — `menus` must not depend on a UI package), and
289
+ * assignability is checked at the seam where `menus-ui` passes it into
290
+ * `MenuTreeEditorProps.draft`.
291
+ *
292
+ * **Load-bearing, not cosmetic.** `useContentEditor.saveDraft` REPLACES the
293
+ * draft's `data` rather than merging, so the editor must know the existing
294
+ * payload to spread it back — otherwise saving the tree from this surface
295
+ * silently drops a `title` / `handle` / `maxDepth` edit staged in the same
296
+ * draft from the entity form. `<MenuTreeEditor>` does that spread; this
297
+ * field is what makes it possible here as well as on the standalone page.
298
+ */
299
+ draft?: {
300
+ data: Record<string, unknown>;
301
+ createdBy: string;
302
+ createdByName: string | null;
303
+ updatedAt: string;
304
+ };
305
+ }
306
+ //#endregion
307
+ //#region src/admin/routes.d.ts
308
+ /**
309
+ * Most distinct handles one request resolves — the amplification bound.
310
+ *
311
+ * The number is `MAX_RENDERED_MENU_HANDLES` in `@murumets-ee/blocks/library`:
312
+ * what one canvas resolves when it opens, so what one canvas can come to need.
313
+ * Restated rather than imported because that entry is the whole block library
314
+ * and this module is on the `lumi.config.ts` load path; `routes.test.ts` holds
315
+ * the two equal.
316
+ */
317
+ declare const MAX_RESOLVED_HANDLES = 8;
318
+ /** `GET /api/admin/menus/resolved` — `null` for a handle with no published menu. */
319
+ interface ResolvedMenusResponse {
320
+ menus: Record<string, ResolvedMenuNavItem[] | null>;
321
+ }
322
+ /**
323
+ * Every admin route this package contributes, in the `AdminRoute[]` shape
324
+ * `createAdminApiHandler` consumes.
325
+ *
326
+ * Wired declaratively via the plugin's `server.routes` slot — see
327
+ * `@murumets-ee/menus-ui`.
328
+ *
329
+ * @example
330
+ * ```ts
331
+ * import { menusRoutes } from '@murumets-ee/menus/admin'
332
+ *
333
+ * definePlugin({ name: '…', server: { routes: menusRoutes() } })
334
+ * ```
335
+ */
336
+ declare function menusRoutes(): AdminRoute[];
337
+ //#endregion
338
+ //#region src/admin/summaries.d.ts
339
+ /**
340
+ * Batch-resolve the live title + existence + publish state of every
341
+ * `{ entity, id }` target, keyed `` `${entity}:${id}` `` (the same key shape
342
+ * `resolveRowTitles` uses).
343
+ *
344
+ * **A MISSING map entry means "not resolvable here", NOT "broken."** It happens
345
+ * when the entity type isn't registered in this app, when the caller holds no
346
+ * `view` grant on it, when that group's read failed, or (for a `team`/`user`-
347
+ * scoped entity only) when the row belongs to a different scope than this
348
+ * request's — a scope filter excludes it the same way a delete would, and
349
+ * this function cannot tell the two apart. Callers should render such items
350
+ * as indeterminate — only an explicit `exists: false` (reported ONLY for a
351
+ * `global`-scoped entity, where no scope filter can ever apply) means the
352
+ * target was really deleted.
353
+ *
354
+ * Best-effort per group: one group's failure never takes down the others (and
355
+ * therefore never takes down the editor page).
356
+ */
357
+ declare function resolveMenuItemSummaries(app: ToolkitApp, refs: ReadonlyArray<{
358
+ entity: string;
359
+ id: string;
360
+ }>): Promise<Map<string, MenuItemSummary>>;
361
+ //#endregion
362
+ export { ANCHOR_PATTERN, type BlockAnchor, MAX_ANCHOR_LENGTH, MAX_MENU_DEPTH, MAX_MENU_SOURCE_PAGE, MAX_MENU_TREE_NODES, MAX_RESOLVED_HANDLES, type MenuEditorStateEntityType, type MenuEditorStateMenu, type MenuEditorStateResponse, type MenuItem, type MenuItemSummary, type MenuSchemaOptions, type ResolvedMenusResponse, applyMenuLabelTranslations, buildMenuItemSchema, buildMenuTreeSchema, collectMenuLabelTexts, measureMenuTree, menuTranslatableSource, menuTreeRevision, menusContributions, menusRoutes, resolveMenuItemSummaries, validateTreeDepth };
363
+ //# sourceMappingURL=admin.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.d.mts","names":[],"sources":["../src/menu-item-schema.ts","../src/translatable-source.ts","../src/admin/editor-state.ts","../src/admin/routes.ts","../src/admin/summaries.ts"],"mappings":"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"}
package/dist/admin.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import{a as e,c as t,d as n,f as r,i,l as a,n as o,o as s,r as c,s as l,t as u,u as d}from"./columns-DwDukx_r.mjs";import{resolveLinkHref as f}from"@murumets-ee/blocks/links";import{z as p}from"zod";import{createHash as m}from"node:crypto";import{getContentConfig as h}from"@murumets-ee/content/plugin";import{combineAdminRoutes as g,defineAdminRoute as _,getContext as v,getCurrentApp as y,getCurrentScope as b,siteSecurityFields as x}from"@murumets-ee/core";import{asc as S,eq as ee,gt as C,inArray as w}from"drizzle-orm";import{VIEW_UNPUBLISHED_ACTION as T,getBlockTreeFieldName as E,getEntityTitleField as D,getSearchableConfig as te}from"@murumets-ee/entity";import{QueryClient as O}from"@murumets-ee/entity/query";const ne=200,k=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function A(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function j(e){return typeof e==`string`&&e.trim()!==``}function M(e){let t=[],n=Array.isArray(e)?[...e].reverse():[];for(;n.length>0;){let e=n.pop();if(A(e)){if(t.push(e),t.length>500)throw Error(`menu tree has more than 500 items`);if(Array.isArray(e.children))for(let t=e.children.length-1;t>=0;t--)n.push(e.children[t])}}return t}function N(e){return m(`sha256`).update(JSON.stringify(Array.isArray(e)?e:[])).digest(`hex`)}function P(e,t){let{sourceLocale:n,targetLocale:r,mode:i}=t;if(n===r)return[];let a=[],o=new Set;for(let t of M(e)){let{id:e,label:s}=t;if(!j(e)||o.has(e)||!A(s))continue;let c=Object.hasOwn(s,n)?s[n]:void 0;if(!j(c))continue;let l=Object.hasOwn(s,r)?s[r]:void 0;i===`missing`&&j(l)||(o.add(e),a.push({key:e,text:c,maxLength:200}))}return a}function F(e,t,n){let r=Array.isArray(e)?structuredClone(e):[],i=0;for(let e of M(r)){let{id:r,label:a}=e;if(typeof r!=`string`||!Object.hasOwn(n,r)||!A(a))continue;let o=n[r];if(typeof o!=`string`)continue;let s=o.trim();if(s!==``){if(s.length>200)throw Error(`menu item ${r}: the translated label is longer than 200 characters`);e.label={...a,[t]:s},i++}}return{tree:r,applied:i}}function I(e){if(e===null||typeof e==`string`||typeof e==`boolean`)return e;if(typeof e==`number`)return Number.isFinite(e)?e:null;if(Array.isArray(e))return e.map(I);if(A(e)){let t={};for(let[n,r]of Object.entries(e))r!==void 0&&(t[n]=I(r));return t}return null}async function re(e){let{ContentClient:t}=await import(`@murumets-ee/content/client`);return new t({admin:e.getClient(o),entity:o})}async function L(e,t){if(!k.test(t))return null;let n=await e.getClient(o).findById(t);if(!n)return null;let r=await re(e),i=await r.getDraft(t,`_`),a=A(i?.data)?i.data:void 0;return{tree:a!==void 0&&`tree`in a?a.tree:n.tree,draftData:a,content:r}}async function ie(e,t){if(t.after!==void 0&&!k.test(t.after))throw Error("menu translatable source: `after` is not a menu id");let n=Number.isFinite(t.limit)?Math.min(Math.max(Math.trunc(t.limit),1),200):200,r=e.getClient(o),i=u(r.getTable(),`id`);return(await r.findMany({...t.after!==void 0&&{where:C(i,t.after)},orderBy:S(i),limit:n})).map(e=>{let t=e;return{id:t.id,label:typeof t.title==`string`?t.title:``}})}async function ae(e,t){let n=await L(e,t.id);return n===null?null:{revision:N(n.tree),texts:P(n.tree,t)}}function oe(){let e=v(),t=e?.user?.groups[0];if(t===void 0||e?.checker?.(t,o.name,`update`)!==!0)throw Error(`menu translatable source: the caller may not update menus`)}async function se(e,t,n){oe();let r=await L(e,t.id);if(r===null)return`not_found`;if(N(r.tree)!==t.revision)return`changed`;if(!n.includes(t.targetLocale))throw Error(`menu ${t.id}: the target locale is not one of the site's content languages (${n.join(`, `)})`);let{tree:i,applied:a}=F(r.tree,t.targetLocale,t.texts);if(a===0)return`written`;let s=d({allowedLocales:n}).safeParse(i);if(!s.success){let e=s.error.issues.slice(0,5).map(e=>`${e.path.join(`.`)}: ${e.message}`).join(`; `);throw Error(`menu ${t.id}: the translated tree is not valid — ${e}`)}let{LockService:c}=await import(`@murumets-ee/content/lock`),l=await new c({app:e}).getLock(o.name,t.id,`_`);if(l&&l.lockedBy!==t.user.id)throw Error(`menu ${t.id}: is being edited by ${l.lockedByName??l.lockedBy} — try again when they have finished`);return await r.content.saveDraft(t.id,{...r.draftData,tree:I(s.data)},`_`,t.user),`written`}function R(){let e=y();if(e===void 0)throw Error(`menu translatable source: called outside a request context`);return e}function ce(){let e=v()?.availableLocales;return e!==void 0&&e.length>0?e:h().locales.map(e=>e.code)}const z={kind:`menu`,label:`Menus`,labelKey:`Menus.translatableSource.menu`,permission:{resource:`menu`,action:`update`},draft:!0,list:e=>ie(R(),e),read:e=>ae(R(),e),write:e=>se(R(),e,ce())},le={"content.translatableSources":[z]};function B(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 ue(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 de(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function fe(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 f=t,p=[];for(let t of f){if(s<=0){c++;continue}if(!de(t))continue;let i=t.id,f=t.kind;if(typeof i!=`string`||i.length===0||f!==`link`&&f!==`header`||ue(t.enabled,n)===!1)continue;s--;let m;if(f===`link`){let n=e.safeParse(t.link);if(!n.success)continue;m=n.data}else if(t.link!==void 0)continue;let h=u(t.children,d+1);m?.kind===`entity`&&(l(a,m.entity,m.id),B(t.label,n,r)===void 0&&l(o,m.entity,m.id)),p.push({id:i,kind:f,link:m,label:t.label,newTab:t.newTab===!0,children:h})}return p}return{nodes:u(t,1),idsByType:a,titleIdsByType:o,droppedForBudget:c}}async function pe(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 V(e,t,n,r){let i=[];for(let a of e){let e=V(a.children,t,n,r),o=B(a.label,t,r.defaultLocale);if(a.kind===`header`){if(e.length===0||o===void 0)continue;i.push({id:a.id,kind:`header`,label:o,href:null,newTab:!1,isActive:!1,inActiveTrail:!1,children:e});continue}let s=a.link;if(s===void 0)continue;let c,l=o;if(s.kind===`entity`){let e=n.paths.get(s.entity)?.get(s.id);if(e===void 0||n.published.get(s.entity)?.has(s.id)!==!0)continue;c=s.anchor===void 0?e:`${e}#${s.anchor}`,l??=n.titles.get(s.entity)?.get(s.id)}else c=f(s);c===null||c.length===0||l===void 0||l.length===0||i.push({id:a.id,kind:`link`,label:l,href:c,newTab:a.newTab,isActive:!1,inActiveTrail:!1,children:e})}return i}function H(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 U(e,t){let n=!1;return{items:e.map(e=>{let r=U(e.children,t),i=e.href!==null&&H(e.href)===t,a=r.containsActive;return(i||a)&&(n=!0),{...e,isActive:i,inActiveTrail:a,children:r.items}}),containsActive:n}}function me(e,t){return{...e,items:U(e.items,H(t)).items}}async function he(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=fe(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 pe(o,t.locale,n,e),c={handle:e,items:V(o.nodes,t.locale,s,n)};return t.currentPath===void 0?c:me(c,t.currentPath)}function W(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function G(e,t){if(!W(e))return;let n={};for(let[r,i]of Object.entries(e))t(i)&&(n[r]=i);return n}const ge=e=>typeof e==`string`,_e=e=>typeof e==`boolean`;function ve(t){let n=500,r=0;function i(t,a){if(!Array.isArray(t))return[];let o=t;if(a>6)return r+=o.length,[];let s=[];for(let t of o){if(!W(t)){r++;continue}let o=t.id,c=t.kind;if(typeof o!=`string`||o.length===0){r++;continue}if(c!==`link`&&c!==`header`){r++;continue}let l;if(c===`link`){let n=e.safeParse(t.link);if(!n.success){r++;continue}l=n.data}else if(t.link!==void 0){r++;continue}if(n<=0){r++;continue}n--;let u=G(t.label,ge),d=u!==void 0&&Object.keys(u).length>0?u:void 0,f=G(t.enabled,_e),p=f!==void 0&&Object.keys(f).length>0?f:void 0;s.push({id:o,kind:c,link:l,label:d??null,enabled:p,newTab:typeof t.newTab==`boolean`?t.newTab:void 0,children:i(t.children,a+1)})}return s}return{items:i(t,1),dropped:r}}function ye(e){let t=new Set,n=[];function r(e){for(let i of e){let e=i.link;if(e?.kind===`entity`){let r=`${e.entity}:${e.id}`;t.has(r)||(t.add(r),n.push({entity:e.entity,id:e.id}))}r(i.children)}}return r(e),n}const K=1e3;function q(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function be(e){let t=e.props;if(!q(t))return;let n=t.anchor;if(typeof n!=`string`||n.length===0||n.length>128||!i.test(n))return;let r=t.anchorLabel;return{anchor:n,label:typeof r==`string`&&r.length>0&&r.length<=200?r:n}}function xe(e,t){let n=[];if(!Array.isArray(e))return{anchors:n,truncated:!1,visited:0};let r=e,i=0,a=!1;r.length>1e3&&(a=!0);let o=[];for(let e=Math.min(r.length,K)-1;e>=0;e--)o.push(r[e]);for(;o.length>0;){if(i>=1e3||n.length>=200){a=!0;break}let e=o.pop();if(i++,!q(e))continue;let t=be(e);t!==void 0&&n.push(t);let r=e.children;if(Array.isArray(r)){let e=r,t=Math.max(K-i,0);e.length>t&&(a=!0);for(let n=Math.min(e.length,t)-1;n>=0;n--)o.push(e[n])}}return a&&t?.warn({visited:i,collected:n.length,nodeBudget:K,resultCap:200},`anchor collection stopped at a hard bound — the returned list is incomplete`),{anchors:n,truncated:a,visited:i}}function J(e,t){return`${e}:${t}`}const Se={title:null,exists:!1,published:null};function Ce(e){let t=new Map,n=0,r=0;for(let i of e){if(typeof i.entity!=`string`||typeof i.id!=`string`||i.entity.length===0||i.id.length===0)continue;let e=t.get(i.entity);if(!e?.has(i.id)){if(n>=500){r++;continue}n++,e?e.add(i.id):t.set(i.entity,new Set([i.id]))}}let i=Math.max(0,t.size-16);return{groups:t,droppedRefs:r,droppedGroups:i}}async function Y(e,t){let n=new Map;if(t.length===0)return n;let r=e.logger.child({resolve:`menu-item-summaries`}),{groups:i,droppedRefs:a,droppedGroups:o}=Ce(t);return a>0&&r.warn({dropped:a,cap:500},`menu item summaries dropped targets over the ref budget — some rows will show no status`),o>0&&r.warn({dropped:o,cap:16},`menu item summaries dropped entity-type groups over the fan-out cap`),await Promise.all([...i.entries()].slice(0,16).map(([t,i])=>we(e,r,t,i,n))),n}async function we(e,t,n,r,i){let a=e.entities.get(n);if(a===void 0)return;let o=D(a),s=o!==`id`,c=a.behaviors?.some(e=>e.name===`publishable`)??!1,l=[...r];try{let t=e.getClient(a),r=await t.findMany({where:w(u(t.getTable(),`id`),l),limit:l.length}),d=new Set;for(let e of r){let t=e,r=t.id;if(typeof r!=`string`)continue;let a=s?t[o]:void 0;d.add(r),i.set(J(n,r),{title:typeof a==`string`&&a.length>0?a:null,exists:!0,published:c?t.status===`published`:null})}if((a.scope??`global`)===`global`)for(let e of l)d.has(e)||i.set(J(n,e),Se)}catch(e){t.warn({entity:n,count:l.length,error:String(e)},`menu item summaries could not be resolved for this entity type`)}}const Te=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function X(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}function Z(e,t){return X({error:e},t)}function Q(){return X([])}function Ee(e){return E(e)??null}const De=_({prefix:`menus`,path:`anchors`,method:`GET`,permission:`menu:view`,defaultRoles:[`admin`],description:`List a published document's addressable block anchors for the menu editor's anchor picker.`,tokens:`accept`,handler:async(e,t)=>{let n=new URL(e.url).searchParams,r=n.get(`entity`),i=n.get(`id`);if(r===null||r.length===0)return Z(`query param 'entity' is required`,400);if(r.length>64)return Z(`query param 'entity' is too long`,400);if(i===null||!Te.test(i))return Z(`query param 'id' must be a uuid`,400);let a=y();if(a===void 0)return Z(`Internal error`,500);let o=a.logger.child({route:`menus/anchors`,entity:r});if(!t.checkPermission(r,`view`))return o.debug({userId:t.user.id,role:t.user.role},`anchors lookup returned empty — caller holds no view grant on the target entity`),Q();let s=a.entities.get(r);if(s===void 0)return Q();let c=Ee(s);if(c===null)return Q();let l=b(),u=await new O({entity:s,db:a.db.readOnly,logger:o,contextResolver:()=>({user:{id:t.user.id,groups:[t.user.role??`viewer`]},checker:(e,n,r)=>r!==T&&t.checkPermission(n,r),...l!==void 0&&{scope:l},...x(void 0,a)})}).findById(i,{...t.locale!==void 0&&{locale:t.locale},...t.defaultLocale!==void 0&&{defaultLocale:t.defaultLocale}});if(u===null)return Q();let{anchors:d}=xe(u[c],o);return X(d)}}),Oe=async(e,t)=>{let n=new URL(e.url).searchParams.get(`handle`);if(n===null||n.length===0)return Z(`query param 'handle' is required`,400);if(n.length>128)return Z(`query param 'handle' is too long`,400);let r=y();if(r===void 0)return Z(`Internal error`,500);let i=r.logger.child({route:`menus/editor-state`}),a=r.getClient(o),s=(await a.findMany({where:ee(u(a.getTable(),`handle`),n),limit:1}))[0];if(s===void 0)return Z(`Menu not found`,404);let l=s.id,d=typeof s.title==`string`?s.title:``,f=s.maxDepth,p=typeof f==`number`&&Number.isInteger(f)&&f>=1?Math.min(f,6):3,m=s.status===`published`?`published`:`draft`,h=v()?.availableLocales,g=t.defaultLocale??h?.[0]??`en`,_=h&&h.length>0?[...h]:[],b=(_.includes(g)?_:[g,..._]).map(e=>({code:e,label:e})),x=await ke(r,l,i),S=x?.data,{items:C,dropped:w}=ve(S!==void 0&&`tree`in S?S.tree:s.tree);w>0&&i.warn({menuId:l,handle:n,dropped:w},`menu editor-state: stored tree contained items that could not be read — the embedded editor shows the rest`);let T=await Y(r,ye(C)),E=Object.fromEntries(T),D=[...r.entities.values()].filter(e=>te(e)!==void 0&&t.checkPermission(e.name,`view`)).map(e=>({name:e.name,label:e.admin?.labelSingular??e.admin?.label??e.name})),O=t.checkPermission(`menu`,`publish`);return X({menu:{id:l,handle:n,title:d,maxDepth:p,status:m,tree:C,entityTypes:c(S!==void 0&&`entityTypes`in S?S.entityTypes:s.entityTypes)},summaries:E,locales:b,defaultLocale:g,droppedItemCount:w,entityTypes:D,canPublish:O,...x!==void 0&&{draft:x}})};async function ke(e,t,n){try{let{ContentClient:n}=await import(`@murumets-ee/content/client`),r=await new n({admin:e.getClient(o),entity:o}).getDraft(t,`_`);return r?{data:r.data,createdBy:r.createdBy,createdByName:r.createdByName,updatedAt:r.updatedAt instanceof Date?r.updatedAt.toISOString():String(r.updatedAt)}:void 0}catch(e){n.warn({err:e,menuId:t},`menu editor-state: draft state unavailable — continuing without`);return}}const Ae=_({prefix:`menus`,path:`editor-state`,method:`GET`,permission:`menu:view`,defaultRoles:[`admin`],description:`Read a menu's full tree + editor metadata by handle, for the blocks editor's in-place Tier 3 menu surface.`,tokens:`accept`,handler:Oe}),$=8,je=p.object({handles:p.array(p.string().min(1).max(128)).min(1).max(8)}),Me=_({prefix:`menus`,path:`resolved`,method:`GET`,permission:`blocks:read-editor-state`,defaultRoles:[`admin`,`editor`],description:`Read the published, resolved items of the menus an editing canvas names, by handle.`,tokens:`accept`,handler:async(e,t)=>{let n=je.safeParse({handles:[...new Set(new URL(e.url).searchParams.getAll(`handle`))]});if(!n.success)return Z(`query param 'handle' must be given 1 to 8 times, each 1 to 128 characters`,400);let{handles:r}=n.data,i=y();if(i===void 0)return Z(`Internal error`,500);let a=i.logger.child({route:`menus/resolved`}),{buildMenuResolveDeps:o}=await import(`./menu-resolve-context-DLIUP8RY.mjs`),s=o(i),c=t.locale??t.defaultLocale??s.defaultLocale,l=await Promise.all(r.map(async e=>{try{let t=await he(e,{locale:c},s);return[e,t===null?null:t.items]}catch(t){a.warn({err:t,handle:e,locale:c},`menu failed to resolve for the canvas; it is left out of the answer`);return}}));return X({menus:Object.fromEntries(l.filter(e=>e!==void 0))})}});function Ne(){return g([De,Ae,Me])}export{i as ANCHOR_PATTERN,s as MAX_ANCHOR_LENGTH,l as MAX_MENU_DEPTH,ne as MAX_MENU_SOURCE_PAGE,t as MAX_MENU_TREE_NODES,$ as MAX_RESOLVED_HANDLES,F as applyMenuLabelTranslations,a as buildMenuItemSchema,d as buildMenuTreeSchema,P as collectMenuLabelTexts,n as measureMenuTree,z as menuTranslatableSource,N as menuTreeRevision,le as menusContributions,Ne as menusRoutes,Y as resolveMenuItemSummaries,r as validateTreeDepth};
2
+ //# sourceMappingURL=admin.mjs.map