@jini-ai/cms 0.5.4 → 0.5.5
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/navigation/agent-tools.js +20 -3
- package/dist/navigation/index.d.ts +3 -1
- package/dist/navigation/index.js +2 -0
- package/dist/navigation/menu-html.d.ts +31 -0
- package/dist/navigation/menu-html.js +53 -0
- package/dist/navigation/menu-service.d.ts +9 -3
- package/dist/navigation/menu-service.js +15 -3
- package/dist/navigation/tool-registrations.d.ts +5 -0
- package/dist/navigation/tool-registrations.js +32 -2
- package/dist/navigation/types.d.ts +6 -0
- package/dist/widgets/resolvers/menu.d.ts +8 -0
- package/dist/widgets/resolvers/menu.js +21 -6
- package/package.json +3 -3
- package/dist/http/settings/cms-adapter.d.ts +0 -20
- package/dist/http/settings/cms-adapter.js +0 -124
- package/dist/http/settings/contracts.d.ts +0 -135
- package/dist/http/settings/contracts.js +0 -2
- package/dist/http/settings/http.d.ts +0 -10
- package/dist/http/settings/http.js +0 -211
- package/dist/http/settings/index.d.ts +0 -5
- package/dist/http/settings/index.js +0 -4
- package/dist/http/settings/shared.d.ts +0 -68
- package/dist/http/settings/shared.js +0 -54
- package/dist/http/settings/stream.d.ts +0 -24
- package/dist/http/settings/stream.js +0 -147
- package/dist/settings/agent-tools.d.ts +0 -10
- package/dist/settings/agent-tools.js +0 -204
- package/dist/settings/agent-value-write-tools.d.ts +0 -22
- package/dist/settings/agent-value-write-tools.js +0 -154
- package/dist/settings/agent-writable-preferences.d.ts +0 -67
- package/dist/settings/agent-writable-preferences.js +0 -132
- package/dist/settings/agent-write-denylist.d.ts +0 -19
- package/dist/settings/agent-write-denylist.js +0 -15
- package/dist/settings/change-feed.d.ts +0 -83
- package/dist/settings/change-feed.js +0 -61
- package/dist/settings/definitions-dispatch.d.ts +0 -56
- package/dist/settings/definitions-dispatch.js +0 -86
- package/dist/settings/dictionaries/index.d.ts +0 -48
- package/dist/settings/dictionaries/index.js +0 -69
- package/dist/settings/dictionaries/settings-dialog.ar.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.ar.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.bn.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.bn.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.de.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.de.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.es.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.es.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.fa.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.fa.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.fr.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.fr.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.hi.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.hi.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.hu.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.hu.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.id.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.id.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.it.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.it.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.ja.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.ja.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.ko.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.ko.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.pl.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.pl.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.pt-BR.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.ru.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.ru.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.th.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.th.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.tr.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.tr.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.uk.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.uk.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.ur.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.ur.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.zh-CN.js +0 -27
- package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts +0 -3
- package/dist/settings/dictionaries/settings-dialog.zh-TW.js +0 -27
- package/dist/settings/ensure-definitions.d.ts +0 -88
- package/dist/settings/ensure-definitions.js +0 -91
- package/dist/settings/errors.d.ts +0 -42
- package/dist/settings/errors.js +0 -46
- package/dist/settings/express/cms-adapter.d.ts +0 -20
- package/dist/settings/express/cms-adapter.js +0 -124
- package/dist/settings/express/contracts.d.ts +0 -135
- package/dist/settings/express/contracts.js +0 -2
- package/dist/settings/express/http.d.ts +0 -10
- package/dist/settings/express/http.js +0 -211
- package/dist/settings/express/shared.d.ts +0 -68
- package/dist/settings/express/shared.js +0 -54
- package/dist/settings/express/stream.d.ts +0 -24
- package/dist/settings/express/stream.js +0 -147
- package/dist/settings/ports.d.ts +0 -118
- package/dist/settings/ports.js +0 -2
- package/dist/settings/principal-lookup.d.ts +0 -9
- package/dist/settings/principal-lookup.js +0 -2
- package/dist/settings/purge-service.d.ts +0 -54
- package/dist/settings/purge-service.js +0 -106
- package/dist/settings/repo.memory.d.ts +0 -119
- package/dist/settings/repo.memory.js +0 -199
- package/dist/settings/settings.d.ts +0 -139
- package/dist/settings/settings.js +0 -259
- package/dist/settings/tool-registrations.d.ts +0 -51
- package/dist/settings/tool-registrations.js +0 -250
- package/dist/settings/types.d.ts +0 -125
- package/dist/settings/types.js +0 -3
- package/dist/settings/ui-tab-definitions.d.ts +0 -41
- package/dist/settings/ui-tab-definitions.js +0 -148
- package/dist/settings/write-service.d.ts +0 -320
- package/dist/settings/write-service.js +0 -683
|
@@ -71,6 +71,18 @@ const MENU_ID_SCHEMA = {
|
|
|
71
71
|
type: "string",
|
|
72
72
|
description: "The menu's id, as returned by menus_list_menus, menus_get_menu, or menus_create_menu.",
|
|
73
73
|
};
|
|
74
|
+
/** HTML mode (2026-10-08, `menu-html.ts`): the same two fields on create and update. */
|
|
75
|
+
const MENU_MODE_SCHEMA = {
|
|
76
|
+
type: "string",
|
|
77
|
+
enum: ["items", "html"],
|
|
78
|
+
description: "What the site renders for this menu: 'items' (the item tree, the default) or 'html' (the menu's own 'html'). Switching keeps the other mode's data. Omit to leave unchanged.",
|
|
79
|
+
};
|
|
80
|
+
const MENU_HTML_SCHEMA = {
|
|
81
|
+
type: "string",
|
|
82
|
+
maxLength: 200000,
|
|
83
|
+
description: "The menu's own HTML for 'html' mode — any markup and classes, e.g. '<ul class=\"nav\"><li><a href=\"/\">Home</a></li></ul>'. " +
|
|
84
|
+
"Rendered inside the theme element that holds the menu's marker. Needs raw-HTML permission (admins/owners). Omit to leave unchanged.",
|
|
85
|
+
};
|
|
74
86
|
/**
|
|
75
87
|
* A link target, published as a `oneOf` over the 4 v1 kinds `menu-service.ts`'s `validateTarget`
|
|
76
88
|
* accepts (`entryRef`/`termRef`/`url`/`route`) — `dynamicQuery`/`content` are named-but-rejected
|
|
@@ -208,6 +220,8 @@ export const menusAgentToolCatalog = [
|
|
|
208
220
|
title: { type: "string", minLength: 1, description: "Human-readable menu name." },
|
|
209
221
|
slug: { type: "string", pattern: "^[a-z0-9-]+$", description: "Stable machine handle. Lowercase letters, digits, and hyphens only." },
|
|
210
222
|
items: { ...ITEMS_TREE_SCHEMA, description: "Optional initial item tree. Defaults to an empty menu if omitted." },
|
|
223
|
+
mode: MENU_MODE_SCHEMA,
|
|
224
|
+
html: MENU_HTML_SCHEMA,
|
|
211
225
|
},
|
|
212
226
|
},
|
|
213
227
|
},
|
|
@@ -215,19 +229,22 @@ export const menusAgentToolCatalog = [
|
|
|
215
229
|
name: "menus_update_menu_tree",
|
|
216
230
|
description: "Replaces an existing menu's whole item tree (and optionally its title/slug) in one version-guarded write. " +
|
|
217
231
|
"Always a whole-tree replace — there is no per-item patch operation, so the submitted tree must include every " +
|
|
218
|
-
"item the menu keeps, each with its fields copied unchanged from the read.
|
|
232
|
+
"item the menu keeps, each with its fields copied unchanged from the read. For a hand-written HTML menu, send " +
|
|
233
|
+
"'html' (and 'mode': 'html') and omit 'items' to keep the stored tree. To delete a menu, use trash_item with entityType 'menu' (it moves to the Trash and can be restored).",
|
|
219
234
|
sideEffects: "mutates-durable-state",
|
|
220
235
|
authorization: { permission: "admin.menus.update" },
|
|
221
236
|
inputSchema: {
|
|
222
237
|
type: "object",
|
|
223
238
|
additionalProperties: false,
|
|
224
|
-
required: ["menuId", "expectedVersion"
|
|
239
|
+
required: ["menuId", "expectedVersion"],
|
|
225
240
|
properties: {
|
|
226
241
|
menuId: MENU_ID_SCHEMA,
|
|
227
242
|
expectedVersion: { type: "integer", description: "The menu's current 'version', as last returned by a read or write on it — the optimistic-concurrency guard. A stale value is rejected with a conflict naming the current version." },
|
|
228
243
|
title: { type: "string", description: "New title. Omit to leave unchanged." },
|
|
229
244
|
slug: { type: "string", pattern: "^[a-z0-9-]+$", description: "New slug. Omit to leave unchanged. Must not collide with another menu's slug." },
|
|
230
|
-
items: ITEMS_TREE_SCHEMA,
|
|
245
|
+
items: { ...ITEMS_TREE_SCHEMA, description: "The full replacement item tree. Omit to keep the stored tree (e.g. an HTML-only edit); send at least one of items, html or mode." },
|
|
246
|
+
mode: MENU_MODE_SCHEMA,
|
|
247
|
+
html: MENU_HTML_SCHEMA,
|
|
231
248
|
},
|
|
232
249
|
},
|
|
233
250
|
},
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* A module's public contract is its `index.ts` — deep imports
|
|
5
5
|
* from outside this directory should go through here.
|
|
6
6
|
*/
|
|
7
|
-
export type { NavTargetKind, ReservedNavTargetKind, NavTarget, NavEntryTarget, NavTermTarget, NavUrlTarget, NavRouteTarget, NavItemAttrs, NavItemNode, NavMenuDoc, NavMenuEntry, MenuStatus, NavLocationKey, NavLocationBindingRow, NavLocationDescriptor, ResolvedNavItem, ResolvedNav, } from "./types.js";
|
|
7
|
+
export type { NavTargetKind, ReservedNavTargetKind, NavTarget, NavEntryTarget, NavTermTarget, NavUrlTarget, NavRouteTarget, NavItemAttrs, NavItemNode, NavMenuDoc, NavMenuMode, NavMenuEntry, MenuStatus, NavLocationKey, NavLocationBindingRow, NavLocationDescriptor, ResolvedNavItem, ResolvedNav, } from "./types.js";
|
|
8
8
|
export { NAV_MENU_CONTENT_TYPE, NAV_FIELD_NAMESPACE, NAV_DOC_TYPE } from "./types.js";
|
|
9
9
|
export type { NavLocationBindingRepoPort, NavResolveContext, NavTargetResolver, NavMenuReadModel, NavLocationRegistry, } from "./ports.js";
|
|
10
10
|
export type { NavigationPermission, CreateMenuInput, UpdateMenuInput, AssignLocationInput, UnassignLocationInput, DeleteMenuInput, NavigationEventName, NavMenuChangedPayload, NavLocationChangedPayload, NavigationHookName, NavigationAiTool, NavWhereUsedResult, } from "./contracts.js";
|
|
@@ -26,6 +26,8 @@ export { InMemoryMenuRepo, InMemoryNavLocationBindingRepo, menuVersionConflictEr
|
|
|
26
26
|
* `check:boundaries` blocks either host-side call site from doing so.
|
|
27
27
|
*/
|
|
28
28
|
export { createMenu, updateMenuTree, assignLocation, deleteMenu, validateAndCloneTree, isAllowedHref, ALLOWED_HREF_SHAPES_DESCRIPTION, MenuNotFoundError, MenuValidationError, MenuConflictError, MenuVersionConflictError, MenuLocationBoundError, DEFAULT_MAX_TREE_DEPTH, DEFAULT_MAX_ITEM_COUNT, type TreeValidationLimits, type CreateMenuDeps, type CreateMenuServiceInput, type CreateMenuRequired, type CreateMenuOptional, type UpdateMenuTreeDeps, type UpdateMenuTreeServiceInput, type UpdateMenuTreeRequired, type UpdateMenuTreeOptional, type AssignLocationDeps, type AssignLocationServiceInput, type AssignLocationRequired, type AssignLocationOptional, type DeleteMenuDeps, type DeleteMenuServiceInput, type DeleteMenuRequired, type DeleteMenuOptional, } from "./menu-service.js";
|
|
29
|
+
/** HTML mode (2026-10-08): validation/normalization for a menu's author-written markup. */
|
|
30
|
+
export { MAX_MENU_HTML_LENGTH, isMenuHtmlAuthoring, menuHtmlInputProblem, mergeMenuHtml, type MenuHtmlAuthoring, } from "./menu-html.js";
|
|
29
31
|
export { resolveForLocation, resolveMenuDoc, type ResolveTargetHrefFn, type ResolvedTargetHref, type ResolveForLocationDeps, type ResolveForLocationServiceInput, type ResolveForLocationRequired, type ResolveForLocationOptional, } from "./resolver.js";
|
|
30
32
|
export { rebuildNavLocationBindings, type RebuildNavLocationBindingsDeps, type RebuildNavLocationBindingsResult, } from "./reconcile.js";
|
|
31
33
|
/** The agent-tool surface for this domain (see `agent-tools.ts` for what is deliberately omitted). */
|
package/dist/navigation/index.js
CHANGED
|
@@ -17,6 +17,8 @@ export { InMemoryMenuRepo, InMemoryNavLocationBindingRepo, menuVersionConflictEr
|
|
|
17
17
|
* `check:boundaries` blocks either host-side call site from doing so.
|
|
18
18
|
*/
|
|
19
19
|
export { createMenu, updateMenuTree, assignLocation, deleteMenu, validateAndCloneTree, isAllowedHref, ALLOWED_HREF_SHAPES_DESCRIPTION, MenuNotFoundError, MenuValidationError, MenuConflictError, MenuVersionConflictError, MenuLocationBoundError, DEFAULT_MAX_TREE_DEPTH, DEFAULT_MAX_ITEM_COUNT, } from "./menu-service.js";
|
|
20
|
+
/** HTML mode (2026-10-08): validation/normalization for a menu's author-written markup. */
|
|
21
|
+
export { MAX_MENU_HTML_LENGTH, isMenuHtmlAuthoring, menuHtmlInputProblem, mergeMenuHtml, } from "./menu-html.js";
|
|
20
22
|
export { resolveForLocation, resolveMenuDoc, } from "./resolver.js";
|
|
21
23
|
export { rebuildNavLocationBindings, } from "./reconcile.js";
|
|
22
24
|
/** The agent-tool surface for this domain (see `agent-tools.ts` for what is deliberately omitted). */
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { NavMenuDoc, NavMenuMode } from "./types.js";
|
|
2
|
+
/** Same ceiling HTML-mode forms use. */
|
|
3
|
+
export declare const MAX_MENU_HTML_LENGTH = 200000;
|
|
4
|
+
/** The optional HTML-mode fields a create/update write may carry. */
|
|
5
|
+
export interface MenuHtmlAuthoring {
|
|
6
|
+
mode?: NavMenuMode | undefined;
|
|
7
|
+
html?: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Whether a write authors raw HTML, and so needs the host's raw-HTML permission. Switching back to
|
|
11
|
+
* items without sending markup does not author anything (forms apply the same rule to "builder").
|
|
12
|
+
* @complexity O(1).
|
|
13
|
+
*/
|
|
14
|
+
export declare function isMenuHtmlAuthoring({ mode, html }: MenuHtmlAuthoring, _optional?: Record<string, never>): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Why a write's `mode`/`html` is invalid, or `null` when it is fine. A message rather than a throw so
|
|
17
|
+
* `menu-service.ts` raises its own `MenuValidationError` without this file importing it back.
|
|
18
|
+
* @complexity O(1).
|
|
19
|
+
*/
|
|
20
|
+
export declare function menuHtmlInputProblem({ mode, html }: MenuHtmlAuthoring, _optional?: Record<string, never>): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* The doc's `mode`/`html` after a valid write: submitted values win, omitted ones keep what is
|
|
23
|
+
* stored, and a menu that never used HTML mode gets neither key, so its stored doc (and publish
|
|
24
|
+
* hash) is unchanged. Call {@link menuHtmlInputProblem} first.
|
|
25
|
+
* @complexity O(h) in the submitted html length (one parse + serialize).
|
|
26
|
+
*/
|
|
27
|
+
export declare function mergeMenuHtml({ input, existing }: {
|
|
28
|
+
input: MenuHtmlAuthoring;
|
|
29
|
+
existing?: Pick<NavMenuDoc, "mode" | "html"> | undefined;
|
|
30
|
+
}, _optional?: Record<string, never>): Pick<NavMenuDoc, "mode" | "html">;
|
|
31
|
+
//# sourceMappingURL=menu-html.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file HTML mode for a menu (2026-10-08): the author writes the menu's markup by hand instead of
|
|
3
|
+
* building an item tree, so it can be styled however they want.
|
|
4
|
+
*
|
|
5
|
+
* Same trust model as HTML-mode forms (`forms/html/html-authoring.ts`) and HTML Pages: the markup is
|
|
6
|
+
* admin/owner-trusted and NOT sanitized. Hosts gate it behind their raw-HTML permission (Tovu binds
|
|
7
|
+
* `pages.edit_html`, the one forms use); this module only validates and normalizes. parse5 balances
|
|
8
|
+
* the fragment so a stray `</nav>` or an unclosed `<div>` cannot close or swallow the theme element
|
|
9
|
+
* the menu renders inside.
|
|
10
|
+
*
|
|
11
|
+
* Both modes' data live side by side in the doc: switching mode never deletes the other one, so an
|
|
12
|
+
* author can flip back and find their items (or their HTML) still there.
|
|
13
|
+
*/
|
|
14
|
+
import { parseFragment, serialize } from "parse5";
|
|
15
|
+
/** Same ceiling HTML-mode forms use. */
|
|
16
|
+
export const MAX_MENU_HTML_LENGTH = 200_000;
|
|
17
|
+
/**
|
|
18
|
+
* Whether a write authors raw HTML, and so needs the host's raw-HTML permission. Switching back to
|
|
19
|
+
* items without sending markup does not author anything (forms apply the same rule to "builder").
|
|
20
|
+
* @complexity O(1).
|
|
21
|
+
*/
|
|
22
|
+
export function isMenuHtmlAuthoring({ mode, html }, _optional = {}) {
|
|
23
|
+
return html !== undefined || mode === "html";
|
|
24
|
+
}
|
|
25
|
+
/** Balance the fragment; scripts and authored styling stay as written (trusted markup). */
|
|
26
|
+
function normalizeMenuHtml(html) {
|
|
27
|
+
return serialize(parseFragment(html));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Why a write's `mode`/`html` is invalid, or `null` when it is fine. A message rather than a throw so
|
|
31
|
+
* `menu-service.ts` raises its own `MenuValidationError` without this file importing it back.
|
|
32
|
+
* @complexity O(1).
|
|
33
|
+
*/
|
|
34
|
+
export function menuHtmlInputProblem({ mode, html }, _optional = {}) {
|
|
35
|
+
if (mode !== undefined && mode !== "items" && mode !== "html")
|
|
36
|
+
return "mode must be 'items' or 'html'";
|
|
37
|
+
if (html !== undefined && (typeof html !== "string" || html.length > MAX_MENU_HTML_LENGTH)) {
|
|
38
|
+
return `html must be a string of at most ${MAX_MENU_HTML_LENGTH} characters`;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The doc's `mode`/`html` after a valid write: submitted values win, omitted ones keep what is
|
|
44
|
+
* stored, and a menu that never used HTML mode gets neither key, so its stored doc (and publish
|
|
45
|
+
* hash) is unchanged. Call {@link menuHtmlInputProblem} first.
|
|
46
|
+
* @complexity O(h) in the submitted html length (one parse + serialize).
|
|
47
|
+
*/
|
|
48
|
+
export function mergeMenuHtml({ input, existing }, _optional = {}) {
|
|
49
|
+
const mode = input.mode ?? existing?.mode;
|
|
50
|
+
const html = input.html !== undefined ? normalizeMenuHtml(input.html) : existing?.html;
|
|
51
|
+
return { ...(mode !== undefined ? { mode } : {}), ...(html !== undefined ? { html } : {}) };
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=menu-html.js.map
|
|
@@ -2,7 +2,7 @@ import type { Clock, IdGenerator, UUID } from "@jini-ai/core/primitives";
|
|
|
2
2
|
import type { OutboxPort } from "../core/ports.js";
|
|
3
3
|
import type { MenuRepoPort } from "./repo.memory.js";
|
|
4
4
|
import type { NavLocationBindingRepoPort } from "./ports.js";
|
|
5
|
-
import { type NavItemNode, type NavLocationBindingRow, type NavLocationKey, type NavMenuEntry } from "./types.js";
|
|
5
|
+
import { type NavItemNode, type NavLocationBindingRow, type NavLocationKey, type NavMenuEntry, type NavMenuMode } from "./types.js";
|
|
6
6
|
export declare class MenuNotFoundError extends Error {
|
|
7
7
|
constructor({ message }: {
|
|
8
8
|
message: string;
|
|
@@ -147,6 +147,9 @@ export interface CreateMenuServiceInput {
|
|
|
147
147
|
slug: string;
|
|
148
148
|
/** Optional initial tree; defaults to an empty menu. Items must carry ids. */
|
|
149
149
|
items?: readonly NavItemNode[] | undefined;
|
|
150
|
+
/** HTML mode (`menu-html.ts`). The host gates raw HTML behind its own permission first. */
|
|
151
|
+
mode?: NavMenuMode | undefined;
|
|
152
|
+
html?: string | undefined;
|
|
150
153
|
}
|
|
151
154
|
export interface CreateMenuRequired {
|
|
152
155
|
deps: CreateMenuDeps;
|
|
@@ -188,8 +191,11 @@ export interface UpdateMenuTreeServiceInput {
|
|
|
188
191
|
expectedVersion: number;
|
|
189
192
|
title?: string | undefined;
|
|
190
193
|
slug?: string | undefined;
|
|
191
|
-
/** The full replacement tree (whole-tree edit). */
|
|
192
|
-
items
|
|
194
|
+
/** The full replacement tree (whole-tree edit). Omitted keeps the stored tree (an HTML-only edit). */
|
|
195
|
+
items?: readonly NavItemNode[] | undefined;
|
|
196
|
+
/** HTML mode (`menu-html.ts`); omitted keeps the stored value. The host gates raw HTML first. */
|
|
197
|
+
mode?: NavMenuMode | undefined;
|
|
198
|
+
html?: string | undefined;
|
|
193
199
|
}
|
|
194
200
|
export interface UpdateMenuTreeRequired {
|
|
195
201
|
deps: UpdateMenuTreeDeps;
|
|
@@ -24,6 +24,7 @@ import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
|
|
|
24
24
|
* through the injected repo ports (`deps`).
|
|
25
25
|
*/
|
|
26
26
|
import { assertEntityLive } from "../core/entity-liveness.js";
|
|
27
|
+
import { menuHtmlInputProblem, mergeMenuHtml } from "./menu-html.js";
|
|
27
28
|
import { NAV_DOC_TYPE, } from "./types.js";
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
// Outbox event publication — each mutating function below
|
|
@@ -281,6 +282,13 @@ function assertValidTitleAndSlug(title, slug) {
|
|
|
281
282
|
throw new MenuValidationError({ message: "slug must use lowercase letters, numbers, and dashes" });
|
|
282
283
|
}
|
|
283
284
|
}
|
|
285
|
+
/** Validated, merged HTML-mode doc fields — `menu-html.ts`'s problem check raised as this file's own error. */
|
|
286
|
+
function menuHtmlFields({ input, existing }) {
|
|
287
|
+
const problem = menuHtmlInputProblem(input);
|
|
288
|
+
if (problem)
|
|
289
|
+
throw new MenuValidationError({ message: problem });
|
|
290
|
+
return mergeMenuHtml({ input, existing });
|
|
291
|
+
}
|
|
284
292
|
/**
|
|
285
293
|
* Creates a new `menu` entry in `published` status with no location
|
|
286
294
|
* assignments. Rejects a duplicate slug within the workspace.
|
|
@@ -305,13 +313,14 @@ export async function createMenu(required, optional = {}) {
|
|
|
305
313
|
if (duplicate)
|
|
306
314
|
throw new MenuConflictError({ message: `slug '${slug}' already exists` });
|
|
307
315
|
const items = validateAndCloneTree({ items: input.items ?? [] }, optional.limits);
|
|
316
|
+
const htmlFields = menuHtmlFields({ input });
|
|
308
317
|
const menu = {
|
|
309
318
|
id: deps.idGen.newId(),
|
|
310
319
|
workspaceId: input.workspaceId,
|
|
311
320
|
slug,
|
|
312
321
|
title,
|
|
313
322
|
status: "published",
|
|
314
|
-
doc: { type: NAV_DOC_TYPE, version: 1, items },
|
|
323
|
+
doc: { type: NAV_DOC_TYPE, version: 1, items, ...htmlFields },
|
|
315
324
|
locations: [],
|
|
316
325
|
updatedAt: kernelNowIso({ clock: deps.clock }),
|
|
317
326
|
version: 1,
|
|
@@ -407,15 +416,18 @@ export async function updateMenuTree(required, optional = {}) {
|
|
|
407
416
|
throw new MenuConflictError({ message: `slug '${slug}' already exists` });
|
|
408
417
|
}
|
|
409
418
|
}
|
|
410
|
-
const items = carryForwardEntryHints({
|
|
419
|
+
const items = input.items === undefined ? existing.doc.items : carryForwardEntryHints({
|
|
411
420
|
previous: existing.doc.items,
|
|
412
421
|
next: validateAndCloneTree({ items: input.items }, optional.limits),
|
|
413
422
|
});
|
|
423
|
+
// Every writer (admin, agent tool, publish repoint, reverters) comes through here, so the stored
|
|
424
|
+
// HTML-mode fields are carried forward unless this write sets them — an items-only save never drops them.
|
|
425
|
+
const htmlFields = menuHtmlFields({ input, existing: existing.doc });
|
|
414
426
|
const menu = {
|
|
415
427
|
...existing,
|
|
416
428
|
title,
|
|
417
429
|
slug,
|
|
418
|
-
doc: { type: NAV_DOC_TYPE, version: existing.doc.version, items },
|
|
430
|
+
doc: { type: NAV_DOC_TYPE, version: existing.doc.version, items, ...htmlFields },
|
|
419
431
|
updatedAt: kernelNowIso({ clock: deps.clock }),
|
|
420
432
|
version: existing.version + 1,
|
|
421
433
|
};
|
|
@@ -19,6 +19,11 @@ export interface MenusToolDeps {
|
|
|
19
19
|
outbox: OutboxPort;
|
|
20
20
|
menuRepo: MenuRepoPort;
|
|
21
21
|
navLocationBindingRepo: NavLocationBindingRepoPort;
|
|
22
|
+
/**
|
|
23
|
+
* The host's raw-HTML permission (Tovu: `pages.edit_html`, the one HTML-mode forms use). A write
|
|
24
|
+
* carrying menu HTML is checked against it on top of the menu permission; unset, HTML menus are refused.
|
|
25
|
+
*/
|
|
26
|
+
rawHtmlPermission?: string | undefined;
|
|
22
27
|
}
|
|
23
28
|
/**
|
|
24
29
|
* This wiring layer's OWN risk classification, authored from what each handler below actually
|
|
@@ -15,6 +15,7 @@ import { buildDomainRegistrations, indexCatalogById, requireInputRecord, require
|
|
|
15
15
|
import { requireToolPermission } from "../core/tools/index.js";
|
|
16
16
|
import { menusAgentToolCatalog } from "./agent-tools.js";
|
|
17
17
|
import { assignLocation, createMenu, MenuNotFoundError, MenuValidationError, updateMenuTree } from "./menu-service.js";
|
|
18
|
+
import { isMenuHtmlAuthoring } from "./menu-html.js";
|
|
18
19
|
const CATALOG_BY_ID = indexCatalogById({ catalog: menusAgentToolCatalog });
|
|
19
20
|
/**
|
|
20
21
|
* This wiring layer's OWN risk classification, authored from what each handler below actually
|
|
@@ -54,10 +55,30 @@ function toMenuToolView(menu) {
|
|
|
54
55
|
title: menu.title,
|
|
55
56
|
status: menu.status,
|
|
56
57
|
items: JSON.parse(JSON.stringify(menu.doc.items)),
|
|
58
|
+
...(menu.doc.mode !== undefined ? { mode: menu.doc.mode } : {}),
|
|
59
|
+
...(menu.doc.html !== undefined ? { html: menu.doc.html } : {}),
|
|
57
60
|
locations: [...menu.locations],
|
|
58
61
|
version: menu.version,
|
|
59
62
|
};
|
|
60
63
|
}
|
|
64
|
+
/** A write's HTML-mode fields as sent; `menu-service.ts` validates their values. */
|
|
65
|
+
function htmlAuthoring(input) {
|
|
66
|
+
return {
|
|
67
|
+
...(input.mode !== undefined ? { mode: input.mode } : {}),
|
|
68
|
+
...(input.html !== undefined ? { html: input.html } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Raw HTML is the host's trust boundary (HTML Pages, HTML-mode forms), so a write that authors menu
|
|
73
|
+
* HTML also needs that permission. Checked before the write, like forms' `prepareFormAuthoring`.
|
|
74
|
+
*/
|
|
75
|
+
async function requireHtmlAuthoringPermission({ routeDeps, principalId, authoring, menuId }) {
|
|
76
|
+
if (!isMenuHtmlAuthoring(authoring))
|
|
77
|
+
return;
|
|
78
|
+
if (!routeDeps.rawHtmlPermission)
|
|
79
|
+
throw new ToolInputError({ message: "HTML menus are not enabled on this site" });
|
|
80
|
+
await requireToolPermission({ authorize: adaptLegacyAuthorize({ authorize: routeDeps.authorize }), workspaceId: routeDeps.workspaceId, principalId, permission: routeDeps.rawHtmlPermission }, { entityType: "menu", ...(menuId ? { entityId: menuId } : {}) });
|
|
81
|
+
}
|
|
61
82
|
function menusDeps(routeDeps) {
|
|
62
83
|
return { repo: routeDeps.menuRepo, clock: routeDeps.clock, idGen: routeDeps.idGen, outbox: routeDeps.outbox };
|
|
63
84
|
}
|
|
@@ -82,6 +103,8 @@ export function buildMenusRegistrations(routeDeps) {
|
|
|
82
103
|
if (input.items !== undefined && !Array.isArray(input.items)) {
|
|
83
104
|
throw new ToolInputError({ message: "'items' must be an array of nav items" });
|
|
84
105
|
}
|
|
106
|
+
const authoring = htmlAuthoring(input);
|
|
107
|
+
await requireHtmlAuthoringPermission({ routeDeps, principalId: ctx.principal.id, authoring });
|
|
85
108
|
return withSchemaOnRejection({ toolId: "menus_create_menu", catalog: CATALOG_BY_ID, isShapeRejection: ({ error }) => isMenusShapeRejection(error), fn: async () => {
|
|
86
109
|
const { menu } = await createMenu({
|
|
87
110
|
deps: menusDeps(routeDeps),
|
|
@@ -90,6 +113,7 @@ export function buildMenusRegistrations(routeDeps) {
|
|
|
90
113
|
title: requireString({ input: input, key: "title" }),
|
|
91
114
|
slug: requireString({ input: input, key: "slug" }),
|
|
92
115
|
items: Array.isArray(input.items) ? input.items : undefined,
|
|
116
|
+
...authoring,
|
|
93
117
|
},
|
|
94
118
|
});
|
|
95
119
|
return { menu: toMenuToolView(menu) };
|
|
@@ -99,8 +123,13 @@ export function buildMenusRegistrations(routeDeps) {
|
|
|
99
123
|
const input = requireInputRecord({ input: ctx.input });
|
|
100
124
|
const menuId = requireString({ input: input, key: "menuId" });
|
|
101
125
|
await requireToolPermission({ authorize: adaptLegacyAuthorize({ authorize: routeDeps.authorize }), workspaceId: routeDeps.workspaceId, principalId: ctx.principal.id, permission: "admin.menus.update" }, { entityType: "menu", entityId: menuId });
|
|
102
|
-
|
|
103
|
-
|
|
126
|
+
const authoring = htmlAuthoring(input);
|
|
127
|
+
if (input.items === undefined && authoring.mode === undefined && authoring.html === undefined) {
|
|
128
|
+
throw new Error("'items' (array), 'html' or 'mode' is required");
|
|
129
|
+
}
|
|
130
|
+
if (input.items !== undefined && !Array.isArray(input.items))
|
|
131
|
+
throw new Error("'items' must be an array of nav items");
|
|
132
|
+
await requireHtmlAuthoringPermission({ routeDeps, principalId: ctx.principal.id, authoring, menuId });
|
|
104
133
|
return withSchemaOnRejection({ toolId: "menus_update_menu_tree", catalog: CATALOG_BY_ID, isShapeRejection: ({ error }) => isMenusShapeRejection(error), fn: async () => {
|
|
105
134
|
const { menu } = await updateMenuTree({
|
|
106
135
|
deps: menusDeps(routeDeps),
|
|
@@ -111,6 +140,7 @@ export function buildMenusRegistrations(routeDeps) {
|
|
|
111
140
|
title: typeof input.title === "string" ? input.title : undefined,
|
|
112
141
|
slug: typeof input.slug === "string" ? input.slug : undefined,
|
|
113
142
|
items: input.items,
|
|
143
|
+
...authoring,
|
|
114
144
|
},
|
|
115
145
|
});
|
|
116
146
|
return { menu: toMenuToolView(menu) };
|
|
@@ -123,7 +123,13 @@ export interface NavMenuDoc {
|
|
|
123
123
|
/** Doc-schema version, so the validator can migrate shapes later. */
|
|
124
124
|
readonly version: number;
|
|
125
125
|
readonly items: readonly NavItemNode[];
|
|
126
|
+
/** What renders at the menu's marker; absent means `"items"`. See `menu-html.ts`. */
|
|
127
|
+
readonly mode?: NavMenuMode | undefined;
|
|
128
|
+
/** Author-written HTML for `"html"` mode, kept even while the menu renders its items. */
|
|
129
|
+
readonly html?: string | undefined;
|
|
126
130
|
}
|
|
131
|
+
/** `"items"` renders the item tree; `"html"` renders the author's own markup (`menu-html.ts`). */
|
|
132
|
+
export type NavMenuMode = "items" | "html";
|
|
127
133
|
export type MenuStatus = "draft" | "published" | "trash";
|
|
128
134
|
/**
|
|
129
135
|
* A typed read model over a `type='menu'` entries row (universal
|
|
@@ -38,5 +38,13 @@ export interface MenuResolverDeps {
|
|
|
38
38
|
/** Host routing injection; callers without it retain the diagnostic placeholder documented above. */
|
|
39
39
|
resolveTargetHref?: ResolveTargetHrefFn;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve menu widgets through navigation's read model: stored HTML or resolved items by doc mode.
|
|
43
|
+
* HTML is trusted and balanced by navigation's save path (`mergeMenuHtml`), like static menus;
|
|
44
|
+
* forwarding it adds no second parser or sanitization policy. Read failures propagate to dispatch.
|
|
45
|
+
* @param deps - Navigation read/resolution ports and host routing/public-filter policy.
|
|
46
|
+
* @returns A resolver producing mode-specific menu IR, or existing typed config/target failures.
|
|
47
|
+
* @complexity One read per instance; item resolution/filtering scales with the selected item trees.
|
|
48
|
+
*/
|
|
41
49
|
export declare function createMenuResolver(deps: MenuResolverDeps, _optional?: Record<string, never>): WidgetResolver;
|
|
42
50
|
//# sourceMappingURL=menu.d.ts.map
|
|
@@ -9,6 +9,14 @@ function toMenuItemProps(items, publicOnly) {
|
|
|
9
9
|
}) : items;
|
|
10
10
|
return visible;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve menu widgets through navigation's read model: stored HTML or resolved items by doc mode.
|
|
14
|
+
* HTML is trusted and balanced by navigation's save path (`mergeMenuHtml`), like static menus;
|
|
15
|
+
* forwarding it adds no second parser or sanitization policy. Read failures propagate to dispatch.
|
|
16
|
+
* @param deps - Navigation read/resolution ports and host routing/public-filter policy.
|
|
17
|
+
* @returns A resolver producing mode-specific menu IR, or existing typed config/target failures.
|
|
18
|
+
* @complexity One read per instance; item resolution/filtering scales with the selected item trees.
|
|
19
|
+
*/
|
|
12
20
|
export function createMenuResolver(deps, _optional = {}) {
|
|
13
21
|
const resolveTargetHref = deps.resolveTargetHref ?? DEFAULT_RESOLVE_TARGET_HREF;
|
|
14
22
|
return {
|
|
@@ -25,14 +33,21 @@ export function createMenuResolver(deps, _optional = {}) {
|
|
|
25
33
|
results.set(instance.id, { ok: false, reason: "target-disabled" });
|
|
26
34
|
continue;
|
|
27
35
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
let props;
|
|
37
|
+
if (menu.doc.mode === "html") {
|
|
38
|
+
props = { title: menu.title, mode: "html", html: menu.doc.html ?? "" };
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const items = await deps.menus.resolveMenuDoc({
|
|
42
|
+
doc: menu.doc,
|
|
43
|
+
context: { workspaceId: context.workspaceId },
|
|
44
|
+
resolveTargetHref,
|
|
45
|
+
});
|
|
46
|
+
props = { title: menu.title, items: toMenuItemProps(items, deps.publicOnly === true) };
|
|
47
|
+
}
|
|
33
48
|
results.set(instance.id, {
|
|
34
49
|
ok: true,
|
|
35
|
-
ir: { componentId: "menu", props
|
|
50
|
+
ir: { componentId: "menu", props },
|
|
36
51
|
dependencyKeys: [menu.id],
|
|
37
52
|
});
|
|
38
53
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/cms",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "Content-model capability for Jini-hosted products: framework-free contracts, ports, and domain services in `/core` and per-domain subpaths; Node-bound adapters in domain-specific subpaths.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -319,9 +319,9 @@
|
|
|
319
319
|
"@types/better-sqlite3": "^7.6.13",
|
|
320
320
|
"@electric-sql/pglite": "0.5.8",
|
|
321
321
|
"jsdom": "^25.0.1",
|
|
322
|
-
"@jini-ai/core": "0.4.2",
|
|
323
322
|
"@jini-ai/http-kit": "0.4.1",
|
|
324
|
-
"@jini-ai/db": "0.3.1"
|
|
323
|
+
"@jini-ai/db": "0.3.1",
|
|
324
|
+
"@jini-ai/core": "0.4.2"
|
|
325
325
|
},
|
|
326
326
|
"dependencies": {
|
|
327
327
|
"@jini-ai/core": "^0.4.2"
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { type SettingsWriteServiceDeps, type SettingsRepoPort } from "../../settings/index.js";
|
|
2
|
-
import type { SettingsService, SettingsPermissions, SettingsChangeFeed } from "./contracts.js";
|
|
3
|
-
/** Keep the CMS write chokepoint while translating its permission vocabulary to host policy. */
|
|
4
|
-
/** Bind the CMS ledger and write chokepoint without copying their domain logic.
|
|
5
|
-
* Effective reads scan definitions and resolve each namespace key; mutations inherit CMS transactions/errors.
|
|
6
|
-
* @complexity O(1) setup. Per operation: O(definitions + keys) reads, or O(batch size) writes.
|
|
7
|
-
* @example createCmsSettingsService({ deps: cmsPorts, permissions });
|
|
8
|
-
*/
|
|
9
|
-
export declare function createCmsSettingsService(required: {
|
|
10
|
-
deps: SettingsWriteServiceDeps;
|
|
11
|
-
permissions: SettingsPermissions;
|
|
12
|
-
}): SettingsService;
|
|
13
|
-
/** Adapt the ledger through CMS tenant/principal disclosure rules, returning names and cursors only.
|
|
14
|
-
* @complexity O(1) setup; collect is O(page size), with namespace lookup per visible revision.
|
|
15
|
-
* @example createCmsSettingsChangeFeed({ repo });
|
|
16
|
-
*/
|
|
17
|
-
export declare function createCmsSettingsChangeFeed(required: {
|
|
18
|
-
repo: SettingsRepoPort;
|
|
19
|
-
}): SettingsChangeFeed;
|
|
20
|
-
//# sourceMappingURL=cms-adapter.d.ts.map
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { getEffective, resolveDefinition, set, clear, resetNamespace, registerDefinitions, NON_REGISTER_DEFINITION_OPS, parseNonRegisterDefinitionOp, collectChangedNamespaces, } from "../../settings/index.js";
|
|
2
|
-
/** Keep the CMS write chokepoint while translating its permission vocabulary to host policy. */
|
|
3
|
-
/** Bind the CMS ledger and write chokepoint without copying their domain logic.
|
|
4
|
-
* Effective reads scan definitions and resolve each namespace key; mutations inherit CMS transactions/errors.
|
|
5
|
-
* @complexity O(1) setup. Per operation: O(definitions + keys) reads, or O(batch size) writes.
|
|
6
|
-
* @example createCmsSettingsService({ deps: cmsPorts, permissions });
|
|
7
|
-
*/
|
|
8
|
-
export function createCmsSettingsService(required) {
|
|
9
|
-
const { deps: original, permissions } = required;
|
|
10
|
-
const permissionMap = new Map([
|
|
11
|
-
["settings.definitions.manage", permissions.manageDefinitions],
|
|
12
|
-
["settings.global.write", permissions.writeGlobal],
|
|
13
|
-
["settings.workspace.write", permissions.writeWorkspace],
|
|
14
|
-
["settings.user.self.write", permissions.writeUserSelf],
|
|
15
|
-
["settings.user.write", permissions.writeUserOther],
|
|
16
|
-
["settings.reset.global", permissions.reset.global],
|
|
17
|
-
["settings.reset.workspace", permissions.reset.workspace],
|
|
18
|
-
["settings.reset.user", permissions.reset.user],
|
|
19
|
-
]);
|
|
20
|
-
const deps = { ...original,
|
|
21
|
-
authorize: (input) => {
|
|
22
|
-
const permission = permissionMap.get(input.permission);
|
|
23
|
-
if (permission === undefined)
|
|
24
|
-
return Promise.resolve({ allowed: false, reason: "unknown_settings_permission" });
|
|
25
|
-
return original.authorize({ ...input, permission });
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
const repo = deps.repo;
|
|
29
|
-
const listDefinitions = async ({ workspaceId }) => {
|
|
30
|
-
const [platform, workspace] = await Promise.all([
|
|
31
|
-
repo.listActiveDefinitions({ workspaceId: null }), repo.listActiveDefinitions({ workspaceId }),
|
|
32
|
-
]);
|
|
33
|
-
return [...platform, ...workspace];
|
|
34
|
-
};
|
|
35
|
-
return {
|
|
36
|
-
listDefinitions,
|
|
37
|
-
effective: async ({ namespace, workspaceId, principalId }) => {
|
|
38
|
-
const defs = await listDefinitions({ workspaceId });
|
|
39
|
-
const keys = new Set(defs.filter((d) => d.namespace === namespace).map((d) => d.key));
|
|
40
|
-
const data = [];
|
|
41
|
-
for (const key of keys) {
|
|
42
|
-
const result = await getEffective({ repo }, { namespace, key, scopeContext: { workspaceId, principalId } });
|
|
43
|
-
if (result)
|
|
44
|
-
data.push({ key, value: result.value, sourceLayer: result.sourceLayer, defVersion: result.defVersion });
|
|
45
|
-
}
|
|
46
|
-
return data;
|
|
47
|
-
},
|
|
48
|
-
raw: async ({ namespace, key, workspaceId, principalId }) => {
|
|
49
|
-
const definition = await resolveDefinition({ repo }, { namespace, key, workspaceId });
|
|
50
|
-
if (!definition)
|
|
51
|
-
return null;
|
|
52
|
-
const settingId = definition.settingId;
|
|
53
|
-
const records = await Promise.all([
|
|
54
|
-
repo.getGlobalValue(settingId), repo.getWorkspaceValue({ workspaceId, settingId }),
|
|
55
|
-
principalId ? repo.getUserValue({ workspaceId, principalId, settingId }) : Promise.resolve(null),
|
|
56
|
-
]);
|
|
57
|
-
const values = records.map((r) => r?.state === "set" ? r.valueJson : null);
|
|
58
|
-
return { key: `${namespace}.${key}`, global: values[0], workspace: values[1], user: values[2], default: definition.defaultValue };
|
|
59
|
-
},
|
|
60
|
-
set: (input) => set({ deps, input }),
|
|
61
|
-
clear: (input) => clear({ deps, input }),
|
|
62
|
-
reset: async (input) => {
|
|
63
|
-
const definitions = await repo.listActiveDefinitions({ workspaceId: input.scope === "global" ? null : (input.workspaceId ?? input.authWorkspaceId ?? null) });
|
|
64
|
-
const keys = definitions.filter((d) => d.namespace === input.namespace).map((d) => d.key);
|
|
65
|
-
return resetNamespace({ deps, input }, keys);
|
|
66
|
-
},
|
|
67
|
-
definitions: ({ items, callerPrincipalId, authWorkspaceId }) => applyDefinitions({ deps, items, callerPrincipalId, authWorkspaceId }),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
/** Preserve register batching and ordered non-register writes over existing CMS operation contracts.
|
|
71
|
-
* Unknown operations return their name. Earlier non-register writes are not rolled back by later failures.
|
|
72
|
-
* @complexity O(item count) time/space plus CMS effects; this batch is not atomic.
|
|
73
|
-
*/
|
|
74
|
-
async function applyDefinitions(required) {
|
|
75
|
-
const { deps, items, callerPrincipalId, authWorkspaceId } = required;
|
|
76
|
-
const toRegister = [];
|
|
77
|
-
const applied = [];
|
|
78
|
-
for (const raw of items) {
|
|
79
|
-
const item = raw;
|
|
80
|
-
const op = String(item.op ?? "register");
|
|
81
|
-
const namespace = String(item.namespace ?? "");
|
|
82
|
-
const key = String(item.key ?? "");
|
|
83
|
-
const ownerKind = item.ownerKind;
|
|
84
|
-
const workspaceId = ownerKind === "site" ? authWorkspaceId : null;
|
|
85
|
-
const defaultValue = (item.defaultJson ?? null);
|
|
86
|
-
if (op === "register") {
|
|
87
|
-
toRegister.push({ namespace, key, ownerKind, workspaceId, schema: item.schemaJson,
|
|
88
|
-
defaultValue, scopes: Number(item.scopes), secret: Boolean(item.secret ?? false) });
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
const parsedOp = parseNonRegisterDefinitionOp({ op });
|
|
92
|
-
if (!parsedOp)
|
|
93
|
-
return { unknownOp: op };
|
|
94
|
-
const opItem = { namespace, key, ownerKind, workspaceId, defaultJson: defaultValue,
|
|
95
|
-
...(item.newNamespace === undefined ? {} : { newNamespace: item.newNamespace }),
|
|
96
|
-
...(item.newKey === undefined ? {} : { newKey: item.newKey }),
|
|
97
|
-
...(item.schemaJson === undefined ? {} : { schemaJson: item.schemaJson }),
|
|
98
|
-
...(item.coercionJson === undefined ? {} : { coercionJson: item.coercionJson }),
|
|
99
|
-
};
|
|
100
|
-
await NON_REGISTER_DEFINITION_OPS[parsedOp]({ ctx: { deps, callerPrincipalId, authWorkspaceId }, item: opItem });
|
|
101
|
-
}
|
|
102
|
-
applied.push({ key: `${namespace}.${key}`, op, status: "applied" });
|
|
103
|
-
}
|
|
104
|
-
if (toRegister.length)
|
|
105
|
-
await registerDefinitions({ deps, input: { definitions: toRegister, callerPrincipalId, authWorkspaceId } });
|
|
106
|
-
return { applied };
|
|
107
|
-
}
|
|
108
|
-
/** Adapt the ledger through CMS tenant/principal disclosure rules, returning names and cursors only.
|
|
109
|
-
* @complexity O(1) setup; collect is O(page size), with namespace lookup per visible revision.
|
|
110
|
-
* @example createCmsSettingsChangeFeed({ repo });
|
|
111
|
-
*/
|
|
112
|
-
export function createCmsSettingsChangeFeed(required) {
|
|
113
|
-
const { repo } = required;
|
|
114
|
-
return {
|
|
115
|
-
head: () => repo.maxRevisionSeq(),
|
|
116
|
-
collect: async ({ sinceSeq, limit, viewer }) => {
|
|
117
|
-
const revisions = await repo.listRevisionsSince({ sinceSeq, limit, workspaceId: viewer.workspaceId });
|
|
118
|
-
// Resolve afresh each batch: a rename must never leave a stale namespace cached forever.
|
|
119
|
-
const batch = await collectChangedNamespaces({ revisions, viewer, resolveNamespace: async ({ settingId }) => (await repo.findDefinitionBySettingId({ settingId }))?.namespace ?? null });
|
|
120
|
-
return { ...batch, examinedCount: revisions.length };
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
//# sourceMappingURL=cms-adapter.js.map
|