callman-mcp 0.1.0 → 0.2.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/CHANGELOG.md +82 -0
- package/README.md +45 -16
- package/dist/client/api.d.ts +64 -3
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/api.js +67 -2
- package/dist/client/api.js.map +1 -1
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +33 -7
- package/dist/client/errors.js.map +1 -1
- package/dist/client/types.d.ts +91 -2
- package/dist/client/types.d.ts.map +1 -1
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +7 -2
- package/dist/context.js.map +1 -1
- package/dist/domain/contracts.d.ts +893 -0
- package/dist/domain/contracts.d.ts.map +1 -0
- package/dist/domain/contracts.js +90 -0
- package/dist/domain/contracts.js.map +1 -0
- package/dist/domain/diagram/build.d.ts +520 -18
- package/dist/domain/diagram/build.d.ts.map +1 -1
- package/dist/domain/diagram/build.js +65 -19
- package/dist/domain/diagram/build.js.map +1 -1
- package/dist/domain/diagram/patch.d.ts +624 -51
- package/dist/domain/diagram/patch.d.ts.map +1 -1
- package/dist/domain/diagram/patch.js +180 -40
- package/dist/domain/diagram/patch.js.map +1 -1
- package/dist/domain/diagram/style.d.ts +315 -0
- package/dist/domain/diagram/style.d.ts.map +1 -0
- package/dist/domain/diagram/style.js +124 -0
- package/dist/domain/diagram/style.js.map +1 -0
- package/dist/domain/diagram/summarize.d.ts +8 -0
- package/dist/domain/diagram/summarize.d.ts.map +1 -1
- package/dist/domain/diagram/summarize.js +28 -13
- package/dist/domain/diagram/summarize.js.map +1 -1
- package/dist/domain/requestIntegrations.d.ts +719 -0
- package/dist/domain/requestIntegrations.d.ts.map +1 -0
- package/dist/domain/requestIntegrations.js +222 -0
- package/dist/domain/requestIntegrations.js.map +1 -0
- package/dist/domain/requestShape.d.ts +1350 -152
- package/dist/domain/requestShape.d.ts.map +1 -1
- package/dist/domain/requestShape.js +336 -121
- package/dist/domain/requestShape.js.map +1 -1
- package/dist/domain/resolve.d.ts +2 -1
- package/dist/domain/resolve.d.ts.map +1 -1
- package/dist/domain/resolve.js +5 -1
- package/dist/domain/resolve.js.map +1 -1
- package/dist/domain/scenario/build.d.ts +82 -26
- package/dist/domain/scenario/build.d.ts.map +1 -1
- package/dist/domain/scenario/build.js +196 -65
- package/dist/domain/scenario/build.js.map +1 -1
- package/dist/domain/scenario/nodeConfig.d.ts +11 -0
- package/dist/domain/scenario/nodeConfig.d.ts.map +1 -0
- package/dist/domain/scenario/nodeConfig.js +251 -0
- package/dist/domain/scenario/nodeConfig.js.map +1 -0
- package/dist/domain/scenario/summarize.d.ts +145 -25
- package/dist/domain/scenario/summarize.d.ts.map +1 -1
- package/dist/domain/scenario/summarize.js +84 -29
- package/dist/domain/scenario/summarize.js.map +1 -1
- package/dist/lib/mask.d.ts +0 -18
- package/dist/lib/mask.d.ts.map +1 -1
- package/dist/lib/mask.js +2 -44
- package/dist/lib/mask.js.map +1 -1
- package/dist/lib/pagination.d.ts +3 -2
- package/dist/lib/pagination.d.ts.map +1 -1
- package/dist/lib/pagination.js +13 -8
- package/dist/lib/pagination.js.map +1 -1
- package/dist/lib/redact.d.ts +12 -0
- package/dist/lib/redact.d.ts.map +1 -0
- package/dist/lib/redact.js +50 -0
- package/dist/lib/redact.js.map +1 -0
- package/dist/lib/strict.d.ts +4 -0
- package/dist/lib/strict.d.ts.map +1 -0
- package/dist/lib/strict.js +14 -0
- package/dist/lib/strict.js.map +1 -0
- package/dist/prompts/index.js +7 -7
- package/dist/prompts/index.js.map +1 -1
- package/dist/resources/docs/contracts.d.ts +2 -0
- package/dist/resources/docs/contracts.d.ts.map +1 -0
- package/dist/resources/docs/contracts.js +74 -0
- package/dist/resources/docs/contracts.js.map +1 -0
- package/dist/resources/docs/diagramSchema.d.ts +1 -1
- package/dist/resources/docs/diagramSchema.d.ts.map +1 -1
- package/dist/resources/docs/diagramSchema.js +95 -28
- package/dist/resources/docs/diagramSchema.js.map +1 -1
- package/dist/resources/docs/overview.d.ts +1 -1
- package/dist/resources/docs/overview.d.ts.map +1 -1
- package/dist/resources/docs/overview.js +29 -7
- package/dist/resources/docs/overview.js.map +1 -1
- package/dist/resources/docs/requestSchema.d.ts +1 -1
- package/dist/resources/docs/requestSchema.d.ts.map +1 -1
- package/dist/resources/docs/requestSchema.js +85 -16
- package/dist/resources/docs/requestSchema.js.map +1 -1
- package/dist/resources/generated/aiPacks.d.ts +1 -1
- package/dist/resources/generated/aiPacks.d.ts.map +1 -1
- package/dist/resources/generated/aiPacks.js +3 -2
- package/dist/resources/generated/aiPacks.js.map +1 -1
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +19 -19
- package/dist/resources/index.js.map +1 -1
- package/dist/run/executeCollection.d.ts +3 -1
- package/dist/run/executeCollection.d.ts.map +1 -1
- package/dist/run/executeCollection.js +16 -4
- package/dist/run/executeCollection.js.map +1 -1
- package/dist/run/summarize.d.ts +17 -6
- package/dist/run/summarize.d.ts.map +1 -1
- package/dist/run/summarize.js +48 -41
- package/dist/run/summarize.js.map +1 -1
- package/dist/run/types.d.ts +1 -0
- package/dist/run/types.d.ts.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/catalog.d.ts +1 -1
- package/dist/tools/catalog.d.ts.map +1 -1
- package/dist/tools/catalog.js +18 -0
- package/dist/tools/catalog.js.map +1 -1
- package/dist/tools/collections.d.ts.map +1 -1
- package/dist/tools/collections.js +53 -41
- package/dist/tools/collections.js.map +1 -1
- package/dist/tools/contracts.d.ts +4 -0
- package/dist/tools/contracts.d.ts.map +1 -0
- package/dist/tools/contracts.js +201 -0
- package/dist/tools/contracts.js.map +1 -0
- package/dist/tools/diagrams.d.ts.map +1 -1
- package/dist/tools/diagrams.js +19 -10
- package/dist/tools/diagrams.js.map +1 -1
- package/dist/tools/environments.d.ts.map +1 -1
- package/dist/tools/environments.js +184 -64
- package/dist/tools/environments.js.map +1 -1
- package/dist/tools/import.d.ts.map +1 -1
- package/dist/tools/import.js +111 -20
- package/dist/tools/import.js.map +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +19 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/lookups.d.ts +38 -7
- package/dist/tools/lookups.d.ts.map +1 -1
- package/dist/tools/lookups.js +113 -10
- package/dist/tools/lookups.js.map +1 -1
- package/dist/tools/meta.d.ts.map +1 -1
- package/dist/tools/meta.js +9 -4
- package/dist/tools/meta.js.map +1 -1
- package/dist/tools/requests.d.ts.map +1 -1
- package/dist/tools/requests.js +138 -50
- package/dist/tools/requests.js.map +1 -1
- package/dist/tools/run.d.ts.map +1 -1
- package/dist/tools/run.js +84 -28
- package/dist/tools/run.js.map +1 -1
- package/dist/tools/scenarios.d.ts.map +1 -1
- package/dist/tools/scenarios.js +238 -75
- package/dist/tools/scenarios.js.map +1 -1
- package/dist/tools/shared.d.ts +1 -0
- package/dist/tools/shared.d.ts.map +1 -1
- package/dist/tools/shared.js +9 -1
- package/dist/tools/shared.js.map +1 -1
- package/dist/tools/workspaces.d.ts.map +1 -1
- package/dist/tools/workspaces.js +44 -7
- package/dist/tools/workspaces.js.map +1 -1
- package/dist-cjs/client/api.js +67 -2
- package/dist-cjs/client/errors.js +33 -7
- package/dist-cjs/context.js +7 -2
- package/dist-cjs/domain/contracts.js +93 -0
- package/dist-cjs/domain/diagram/build.js +71 -20
- package/dist-cjs/domain/diagram/patch.js +179 -39
- package/dist-cjs/domain/diagram/style.js +130 -0
- package/dist-cjs/domain/diagram/summarize.js +28 -13
- package/dist-cjs/domain/requestIntegrations.js +228 -0
- package/dist-cjs/domain/requestShape.js +342 -121
- package/dist-cjs/domain/resolve.js +5 -1
- package/dist-cjs/domain/scenario/build.js +197 -65
- package/dist-cjs/domain/scenario/nodeConfig.js +260 -0
- package/dist-cjs/domain/scenario/summarize.js +86 -29
- package/dist-cjs/lib/mask.js +3 -48
- package/dist-cjs/lib/pagination.js +13 -8
- package/dist-cjs/lib/redact.js +53 -0
- package/dist-cjs/lib/strict.js +18 -0
- package/dist-cjs/prompts/index.js +7 -7
- package/dist-cjs/resources/docs/contracts.js +76 -0
- package/dist-cjs/resources/docs/diagramSchema.js +95 -28
- package/dist-cjs/resources/docs/overview.js +29 -7
- package/dist-cjs/resources/docs/requestSchema.js +85 -16
- package/dist-cjs/resources/generated/aiPacks.js +3 -2
- package/dist-cjs/resources/index.js +17 -17
- package/dist-cjs/run/executeCollection.js +16 -4
- package/dist-cjs/run/summarize.js +48 -41
- package/dist-cjs/server.js +1 -1
- package/dist-cjs/tools/catalog.js +18 -0
- package/dist-cjs/tools/collections.js +52 -40
- package/dist-cjs/tools/contracts.js +204 -0
- package/dist-cjs/tools/diagrams.js +19 -10
- package/dist-cjs/tools/environments.js +182 -62
- package/dist-cjs/tools/import.js +107 -16
- package/dist-cjs/tools/index.js +21 -2
- package/dist-cjs/tools/lookups.js +123 -11
- package/dist-cjs/tools/meta.js +9 -4
- package/dist-cjs/tools/requests.js +134 -46
- package/dist-cjs/tools/run.js +82 -26
- package/dist-cjs/tools/scenarios.js +235 -72
- package/dist-cjs/tools/shared.js +11 -2
- package/dist-cjs/tools/workspaces.js +43 -6
- package/package.json +2 -2
|
@@ -1,45 +1,62 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.buildScenarioCreateBody = exports.buildScenarioGraph = exports.deepMerge = exports.scenarioInputSchema = exports.scenarioEnvironmentInputSchema = exports.scenarioEdgeInputSchema = exports.scenarioNodeInputSchema = exports.SCENARIO_NODE_TYPES = void 0;
|
|
3
|
+
exports.buildScenarioCreateBody = exports.buildEnvironments = exports.buildScenarioGraph = exports.deepMerge = exports.scenarioInputSchema = exports.scenarioDbSettingsSchema = exports.scenarioDatasetSchema = exports.scenarioEnvironmentInputSchema = exports.scenarioEdgeInputSchema = exports.scenarioNodeInputSchema = exports.SCENARIO_NODE_TYPES = void 0;
|
|
4
4
|
const callman_core_1 = require("callman-core");
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const errors_js_1 = require("../../client/errors.js");
|
|
7
7
|
const ids_js_1 = require("../../lib/ids.js");
|
|
8
|
-
const
|
|
8
|
+
const strict_js_1 = require("../../lib/strict.js");
|
|
9
|
+
const nodeConfig_js_1 = require("./nodeConfig.js");
|
|
9
10
|
// From the COMPACT scenario a model writes to the backend's `POST /scenarios`
|
|
10
|
-
// body.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// are short and stable; missing positions are laid out in layers
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
exports.SCENARIO_NODE_TYPES =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
// body. A node's config is deep-merged over a BASE: the node's stored config
|
|
12
|
+
// when a node with that id already exists (update / patch — so editing one
|
|
13
|
+
// field never resets the rest), otherwise core's defaults for the type.
|
|
14
|
+
// Ids are short and stable; missing positions are laid out in layers (stored
|
|
15
|
+
// positions are kept). The backend's zod schema stays the arbiter of node
|
|
16
|
+
// config validity; callman://docs/scenario-nodes documents every field.
|
|
17
|
+
exports.SCENARIO_NODE_TYPES = callman_core_1.SCENARIO_NODE_REFERENCE_TYPES;
|
|
18
|
+
// Output roots a label would shadow (warning, not an error).
|
|
19
|
+
const SHADOWING_LABELS = new Set(["response", "lastResponse", "data", "env", "global", "globals", "db", "redis", "kafkaEvent", "kafkaevent"]);
|
|
20
|
+
exports.scenarioNodeInputSchema = (0, strict_js_1.strictObject)({
|
|
21
|
+
id: zod_1.z.string().min(1).max(64).optional().describe("Stable id; generated (n1, n2…) when omitted. Reusing an existing id on update merges into that node"),
|
|
22
|
+
type: zod_1.z.string().min(1).max(40).describe(`Node type: ${exports.SCENARIO_NODE_TYPES.join(", ")} (callman://docs/scenario-nodes)`),
|
|
23
|
+
label: zod_1.z.string().max(200).optional().describe("Unique name; later nodes read its output as {{Label.path}}. Generated when omitted"),
|
|
24
|
+
config: zod_1.z.record(zod_1.z.unknown()).optional().describe("Type-specific config (callman://docs/scenario-nodes), merged over the stored node (same id) or the type's defaults"),
|
|
25
|
+
position: zod_1.z.object({ x: zod_1.z.number(), y: zod_1.z.number() }).strict().optional(),
|
|
26
|
+
}, "node field");
|
|
27
|
+
exports.scenarioEdgeInputSchema = (0, strict_js_1.strictObject)({
|
|
25
28
|
id: zod_1.z.string().min(1).max(64).optional(),
|
|
26
29
|
source: zod_1.z.string().min(1).max(64),
|
|
27
30
|
target: zod_1.z.string().min(1).max(64),
|
|
28
|
-
type: zod_1.z.enum(["default", "yes", "no", "error"]).optional().describe("yes/no
|
|
29
|
-
});
|
|
30
|
-
exports.scenarioEnvironmentInputSchema =
|
|
31
|
-
id: zod_1.z.string().min(1).max(
|
|
32
|
-
name: zod_1.z.string().min(1).max(200),
|
|
33
|
-
variables: zod_1.z
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
type: zod_1.z.enum(["default", "yes", "no", "error"]).optional().describe("yes/no leave a condition node; error leaves any other node on failure"),
|
|
32
|
+
}, "edge field");
|
|
33
|
+
exports.scenarioEnvironmentInputSchema = (0, strict_js_1.strictObject)({
|
|
34
|
+
id: zod_1.z.string().min(1).max(200).optional(),
|
|
35
|
+
name: zod_1.z.string().trim().min(1).max(200),
|
|
36
|
+
variables: zod_1.z
|
|
37
|
+
.array((0, strict_js_1.strictObject)({ key: zod_1.z.string().trim().min(1).max(1000), value: zod_1.z.string().max(100_000).default(""), enabled: zod_1.z.boolean().optional() }, "variable field"))
|
|
38
|
+
.max(1000)
|
|
39
|
+
.default([]),
|
|
40
|
+
}, "environment field");
|
|
41
|
+
exports.scenarioDatasetSchema = (0, strict_js_1.strictObject)({
|
|
42
|
+
columns: zod_1.z.array(zod_1.z.string().max(200)).max(200),
|
|
43
|
+
rows: zod_1.z.array(zod_1.z.record(zod_1.z.string().max(100_000))).max(10_000).describe("Each row: column → value; nodes read {{data.<column>}}; the scenario runs once per row"),
|
|
44
|
+
}, "dataset field")
|
|
45
|
+
.nullable()
|
|
46
|
+
.describe("Data-driven runs: the whole scenario runs once per row (null removes the dataset)");
|
|
47
|
+
exports.scenarioDbSettingsSchema = (0, strict_js_1.strictObject)({ transactionMode: zod_1.z.enum(["none", "rollback", "commit"]).optional() }, "dbSettings field")
|
|
48
|
+
.nullable()
|
|
49
|
+
.describe('For db nodes with useSession: "rollback" wraps session DB work in BEGIN … ROLLBACK, "commit" commits on success, "none" keeps autocommit');
|
|
50
|
+
exports.scenarioInputSchema = (0, strict_js_1.strictObject)({
|
|
51
|
+
name: zod_1.z.string().trim().min(1).max(200),
|
|
37
52
|
description: zod_1.z.string().max(5000).optional(),
|
|
38
53
|
priority: zod_1.z.enum(["critical", "high", "medium", "low"]).optional(),
|
|
39
54
|
nodes: zod_1.z.array(exports.scenarioNodeInputSchema).min(1).max(1000),
|
|
40
55
|
edges: zod_1.z.array(exports.scenarioEdgeInputSchema).max(2000).default([]),
|
|
41
|
-
environments: zod_1.z.array(exports.scenarioEnvironmentInputSchema).max(
|
|
42
|
-
|
|
56
|
+
environments: zod_1.z.array(exports.scenarioEnvironmentInputSchema).max(100).optional(),
|
|
57
|
+
dataset: exports.scenarioDatasetSchema.optional(),
|
|
58
|
+
dbSettings: exports.scenarioDbSettingsSchema.optional(),
|
|
59
|
+
}, "scenario field");
|
|
43
60
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
44
61
|
// Objects merge key by key; arrays and scalars from the override win whole.
|
|
45
62
|
const deepMerge = (base, override) => {
|
|
@@ -52,35 +69,131 @@ const deepMerge = (base, override) => {
|
|
|
52
69
|
return out;
|
|
53
70
|
};
|
|
54
71
|
exports.deepMerge = deepMerge;
|
|
55
|
-
const
|
|
56
|
-
|
|
72
|
+
const labelBase = (type) => type
|
|
73
|
+
.split(/[-_]/)
|
|
74
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
75
|
+
.join("");
|
|
76
|
+
// Labels of stored nodes nested in foreach / subscenario / scenario-ref bodies.
|
|
77
|
+
const nestedLabels = (config) => {
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const node of Array.isArray(config.nodes) ? config.nodes : []) {
|
|
80
|
+
if (!isRecord(node))
|
|
81
|
+
continue;
|
|
82
|
+
const data = isRecord(node.data) ? node.data : {};
|
|
83
|
+
if (typeof data.label === "string" && data.label)
|
|
84
|
+
out.push(data.label);
|
|
85
|
+
if (isRecord(data.config))
|
|
86
|
+
out.push(...nestedLabels(data.config));
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
};
|
|
90
|
+
const buildScenarioGraph = (nodesInput, edgesInput, options = {}) => {
|
|
91
|
+
const existingById = new Map((options.existing ?? []).map((node) => [node.id, node]));
|
|
92
|
+
const nextNodeId = (0, ids_js_1.sequentialIds)("n", [...nodesInput.map((node) => node.id).filter((id) => Boolean(id)), ...existingById.keys()]);
|
|
57
93
|
const nextEdgeId = (0, ids_js_1.sequentialIds)("e", edgesInput.map((edge) => edge.id).filter((id) => Boolean(id)));
|
|
58
94
|
const ids = nodesInput.map((node) => node.id ?? nextNodeId());
|
|
59
95
|
const duplicate = ids.find((id, index) => ids.indexOf(id) !== index);
|
|
60
96
|
if (duplicate)
|
|
61
97
|
throw new errors_js_1.ToolInputError("SCENARIO_DUPLICATE_NODE_ID", `Duplicate node id "${duplicate}".`);
|
|
62
98
|
const idSet = new Set(ids);
|
|
63
|
-
const
|
|
99
|
+
const warnings = [];
|
|
100
|
+
const problems = [];
|
|
101
|
+
const sent = new Map();
|
|
102
|
+
const built = nodesInput.map((node, index) => {
|
|
103
|
+
const id = ids[index];
|
|
64
104
|
const type = node.type.trim();
|
|
65
105
|
if (!exports.SCENARIO_NODE_TYPES.includes(type)) {
|
|
66
106
|
throw new errors_js_1.ToolInputError("SCENARIO_UNKNOWN_NODE_TYPE", `Unknown scenario node type "${type}". Known: ${exports.SCENARIO_NODE_TYPES.join(", ")}.`);
|
|
67
107
|
}
|
|
68
|
-
const
|
|
69
|
-
const
|
|
108
|
+
const stored = existingById.get(id);
|
|
109
|
+
const sameNode = stored && stored.type === type ? stored : undefined;
|
|
110
|
+
if (type === "scenario-ref" && !sameNode) {
|
|
111
|
+
throw new errors_js_1.ToolInputError("SCENARIO_REF_READ_ONLY", `Node ${id}: scenario-ref nodes are created by linking a scenario in the desktop app; they can only be kept (send the stored node's id and type).`);
|
|
112
|
+
}
|
|
113
|
+
const base = sameNode?.data?.config ?? (0, callman_core_1.defaultScenarioNodeConfig)(type) ?? {};
|
|
114
|
+
const restored = sameNode ? (0, nodeConfig_js_1.restoreMasked)(node.config ?? {}, base) : (node.config ?? {});
|
|
115
|
+
const unwritable = (0, nodeConfig_js_1.findUnwritableValues)(restored, `${id}.config`);
|
|
116
|
+
if (unwritable.length > 0)
|
|
117
|
+
problems.push(...unwritable);
|
|
118
|
+
const normalized = (0, nodeConfig_js_1.normalizeNodeConfig)(type, restored);
|
|
119
|
+
warnings.push(...normalized.notes.map((note) => `${id}: ${note}`));
|
|
120
|
+
const missing = (0, nodeConfig_js_1.missingRequiredFields)(type, normalized.config, sameNode ? base : null);
|
|
121
|
+
if (missing.length > 0)
|
|
122
|
+
problems.push(`${id} (${type}) needs config.${missing.join(", config.")}`);
|
|
123
|
+
if (Object.keys(normalized.config).length > 0)
|
|
124
|
+
sent.set(id, normalized.config);
|
|
125
|
+
const config = (0, exports.deepMerge)(base, normalized.config);
|
|
70
126
|
return {
|
|
71
|
-
id
|
|
127
|
+
id,
|
|
72
128
|
type,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
129
|
+
label: node.label ?? sameNode?.data?.label ?? undefined,
|
|
130
|
+
position: node.position ?? sameNode?.position,
|
|
131
|
+
config,
|
|
132
|
+
// Label rules are enforced for what this call names or adds; a problem
|
|
133
|
+
// already in stored data only warns, so it never blocks an edit.
|
|
134
|
+
touched: node.label !== undefined || !sameNode || node.config !== undefined,
|
|
76
135
|
};
|
|
77
136
|
});
|
|
137
|
+
if (problems.length > 0) {
|
|
138
|
+
throw new errors_js_1.ToolInputError("SCENARIO_NODE_INVALID", `Fix these nodes: ${problems.join("; ")}. Masked values sent back unchanged keep the stored secret on update; otherwise pass real values or {{variables}}.`, { problems });
|
|
139
|
+
}
|
|
140
|
+
// Labels: generated when missing, unique across the whole scenario
|
|
141
|
+
// (nested bodies included), never reserved.
|
|
142
|
+
const taken = new Set();
|
|
143
|
+
for (const node of built) {
|
|
144
|
+
if (node.label)
|
|
145
|
+
taken.add(node.label);
|
|
146
|
+
for (const label of nestedLabels(node.config))
|
|
147
|
+
taken.add(label);
|
|
148
|
+
}
|
|
149
|
+
const counters = new Map();
|
|
150
|
+
for (const node of built) {
|
|
151
|
+
if (node.label)
|
|
152
|
+
continue;
|
|
153
|
+
const prefix = labelBase(node.type);
|
|
154
|
+
let n = counters.get(prefix) ?? 0;
|
|
155
|
+
let candidate;
|
|
156
|
+
do {
|
|
157
|
+
n += 1;
|
|
158
|
+
candidate = `${prefix}${n}`;
|
|
159
|
+
} while (taken.has(candidate));
|
|
160
|
+
counters.set(prefix, n);
|
|
161
|
+
node.label = candidate;
|
|
162
|
+
taken.add(candidate);
|
|
163
|
+
}
|
|
164
|
+
const seen = new Map();
|
|
165
|
+
const labelProblems = [];
|
|
166
|
+
const report = (message, touched) => {
|
|
167
|
+
if (touched)
|
|
168
|
+
labelProblems.push(message);
|
|
169
|
+
else
|
|
170
|
+
warnings.push(`Existing problem (not blocking): ${message}`);
|
|
171
|
+
};
|
|
172
|
+
for (const node of built) {
|
|
173
|
+
const label = node.label;
|
|
174
|
+
if ((0, callman_core_1.isReservedScenarioLabel)(label))
|
|
175
|
+
report(`"${label}" (${node.id}) is reserved — its output would be dropped`, node.touched);
|
|
176
|
+
const other = seen.get(label);
|
|
177
|
+
if (other)
|
|
178
|
+
report(`"${label}" is used by ${other.id} and ${node.id}`, node.touched || other.touched);
|
|
179
|
+
seen.set(label, { id: node.id, touched: node.touched });
|
|
180
|
+
for (const nested of nestedLabels(node.config)) {
|
|
181
|
+
const owner = seen.get(nested);
|
|
182
|
+
if (owner && owner.id !== node.id)
|
|
183
|
+
report(`"${nested}" is used by ${owner.id} and inside ${node.id}`, node.touched || owner.touched);
|
|
184
|
+
}
|
|
185
|
+
if (SHADOWING_LABELS.has(label) && node.touched)
|
|
186
|
+
warnings.push(`${node.id}: label "${label}" shadows a built-in root ({{${label}.…}}) — prefer another name`);
|
|
187
|
+
}
|
|
188
|
+
if (labelProblems.length > 0) {
|
|
189
|
+
throw new errors_js_1.ToolInputError("SCENARIO_LABEL_INVALID", `Labels must be unique and not reserved: ${labelProblems.join("; ")}.`);
|
|
190
|
+
}
|
|
78
191
|
const edges = edgesInput.map((edge) => {
|
|
79
192
|
for (const endpoint of [edge.source, edge.target]) {
|
|
80
193
|
if (!idSet.has(endpoint))
|
|
81
194
|
throw new errors_js_1.ToolInputError("SCENARIO_EDGE_ENDPOINT_NOT_FOUND", `Edge references "${endpoint}" which is not a node id.`);
|
|
82
195
|
}
|
|
83
|
-
return { id: edge.id ?? nextEdgeId(), source: edge.source, target: edge.target, ...(edge.type ? { type: edge.type } : {}) };
|
|
196
|
+
return { id: edge.id ?? nextEdgeId(), source: edge.source, target: edge.target, ...(edge.type && edge.type !== "default" ? { type: edge.type } : {}) };
|
|
84
197
|
});
|
|
85
198
|
// Layered positions for the nodes that have none: BFS from the sources.
|
|
86
199
|
const incoming = new Map(ids.map((id) => [id, 0]));
|
|
@@ -103,38 +216,57 @@ const buildScenarioGraph = (nodesInput, edgesInput) => {
|
|
|
103
216
|
}
|
|
104
217
|
}
|
|
105
218
|
const rowCounter = new Map();
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
219
|
+
const nodes = built.map((node) => {
|
|
220
|
+
let position = node.position;
|
|
221
|
+
if (!position) {
|
|
222
|
+
const layer = level.get(node.id) ?? 0;
|
|
223
|
+
const row = rowCounter.get(layer) ?? 0;
|
|
224
|
+
rowCounter.set(layer, row + 1);
|
|
225
|
+
position = { x: 80 + layer * 320, y: 80 + row * 170 };
|
|
226
|
+
}
|
|
227
|
+
return { id: node.id, type: node.type, position, data: { label: node.label, config: node.config } };
|
|
113
228
|
});
|
|
114
|
-
return {
|
|
115
|
-
nodes: positioned.map(({ _positioned, ...node }) => node),
|
|
116
|
-
edges,
|
|
117
|
-
};
|
|
229
|
+
return { nodes, edges, sent, warnings };
|
|
118
230
|
};
|
|
119
231
|
exports.buildScenarioGraph = buildScenarioGraph;
|
|
120
|
-
|
|
232
|
+
// Environments: stored ids are kept by name so an edit does not orphan them;
|
|
233
|
+
// a masked value sent back unchanged keeps the stored value.
|
|
234
|
+
const buildEnvironments = (input, existing = []) => {
|
|
235
|
+
const nextEnvId = (0, ids_js_1.sequentialIds)("env", existing.map((environment) => environment.id));
|
|
236
|
+
return input.map((environment) => {
|
|
237
|
+
const stored = existing.find((entry) => entry.id === environment.id) ?? existing.find((entry) => entry.name.trim().toLowerCase() === environment.name.trim().toLowerCase());
|
|
238
|
+
const nextVarId = (0, ids_js_1.sequentialIds)("v", (stored?.variables ?? []).map((variable) => variable.id ?? ""));
|
|
239
|
+
const masked = [];
|
|
240
|
+
const variables = environment.variables.map((variable) => {
|
|
241
|
+
const previous = stored?.variables.find((entry) => entry.key === variable.key);
|
|
242
|
+
const value = (0, nodeConfig_js_1.restoreMasked)(variable.value, previous?.value);
|
|
243
|
+
if (value.startsWith("••••"))
|
|
244
|
+
masked.push(variable.key);
|
|
245
|
+
return { id: previous?.id ?? nextVarId(), key: variable.key, value, enabled: variable.enabled ?? previous?.enabled ?? true };
|
|
246
|
+
});
|
|
247
|
+
if (masked.length > 0) {
|
|
248
|
+
throw new errors_js_1.ToolInputError("MASKED_VALUE", `Environment "${environment.name}": masked values cannot be saved (${masked.join(", ")}). Send them back unchanged only for keys that already exist, or pass real values.`, { keys: masked });
|
|
249
|
+
}
|
|
250
|
+
return { id: environment.id ?? stored?.id ?? nextEnvId(), name: environment.name, variables };
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
exports.buildEnvironments = buildEnvironments;
|
|
254
|
+
const buildScenarioCreateBody = (input, folderId, groupId) => {
|
|
121
255
|
const graph = (0, exports.buildScenarioGraph)(input.nodes, input.edges);
|
|
122
|
-
const nextEnvId = (0, ids_js_1.sequentialIds)("env");
|
|
123
256
|
return {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}),
|
|
257
|
+
graph,
|
|
258
|
+
body: {
|
|
259
|
+
name: input.name.trim(),
|
|
260
|
+
...(input.description ? { description: input.description } : {}),
|
|
261
|
+
...(input.priority ? { priority: input.priority } : {}),
|
|
262
|
+
folderId,
|
|
263
|
+
...(groupId !== undefined ? { groupId } : {}),
|
|
264
|
+
nodes: graph.nodes,
|
|
265
|
+
edges: graph.edges,
|
|
266
|
+
environments: (0, exports.buildEnvironments)(input.environments ?? []),
|
|
267
|
+
...(input.dataset !== undefined ? { dataset: input.dataset } : {}),
|
|
268
|
+
...(input.dbSettings !== undefined ? { dbSettings: input.dbSettings } : {}),
|
|
269
|
+
},
|
|
138
270
|
};
|
|
139
271
|
};
|
|
140
272
|
exports.buildScenarioCreateBody = buildScenarioCreateBody;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.droppedPaths = exports.maskConfigInPlace = exports.restoreMasked = exports.findUnwritableValues = exports.missingRequiredFields = exports.normalizeNodeConfig = exports.translateRequestConfig = void 0;
|
|
4
|
+
const callman_core_1 = require("callman-core");
|
|
5
|
+
const mask_js_1 = require("../../lib/mask.js");
|
|
6
|
+
// Node-config plumbing between the compact scenario a model writes and the
|
|
7
|
+
// stored config the backend validates (callman://docs/scenario-nodes):
|
|
8
|
+
// * request shapes from the request tools ({ type: "bearer", token },
|
|
9
|
+
// body: {…}) are translated to the scenario form, so a request read with
|
|
10
|
+
// get_request can be dropped into a request node;
|
|
11
|
+
// * convenience fixes that the backend would otherwise reject or ignore
|
|
12
|
+
// (assertion rule ids, expert-mode conditions);
|
|
13
|
+
// * secrets masked IN PLACE for reading (same shape — writable back), and
|
|
14
|
+
// a masked value sent back unchanged restored from the stored config;
|
|
15
|
+
// * a diff of what was sent vs what the server stored, because the
|
|
16
|
+
// backend's node schemas silently strip unknown keys.
|
|
17
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
let counter = 0;
|
|
19
|
+
const newId = (prefix) => `${prefix}_${Date.now().toString(36)}${(counter++).toString(36)}`;
|
|
20
|
+
// ── request / poll.request translation ────────────────────────────────
|
|
21
|
+
const translateAuth = (auth) => {
|
|
22
|
+
if (!isRecord(auth))
|
|
23
|
+
return auth;
|
|
24
|
+
const type = auth.type === "apiKey" ? "apikey" : auth.type;
|
|
25
|
+
const out = { ...auth, type };
|
|
26
|
+
const move = (from, to) => {
|
|
27
|
+
if (from in out && !(to in out))
|
|
28
|
+
out[to] = out[from];
|
|
29
|
+
delete out[from];
|
|
30
|
+
};
|
|
31
|
+
if (type === "bearer")
|
|
32
|
+
move("token", "bearerToken");
|
|
33
|
+
if (type === "basic") {
|
|
34
|
+
move("username", "basicUsername");
|
|
35
|
+
move("password", "basicPassword");
|
|
36
|
+
}
|
|
37
|
+
if (type === "apikey") {
|
|
38
|
+
move("key", "apiKeyKey");
|
|
39
|
+
move("value", "apiKeyValue");
|
|
40
|
+
move("location", "apiKeyLocation");
|
|
41
|
+
}
|
|
42
|
+
if (type === "oauth2" && !isRecord(out.oauth2)) {
|
|
43
|
+
// Flat oauth2 fields (the request tools' shape) → nested, all ten keys.
|
|
44
|
+
const oauth2 = {};
|
|
45
|
+
for (const key of ["grantType", "tokenUrl", "clientId", "clientSecret", "scope", "username", "password", "refreshToken", "clientAuth", "audience"]) {
|
|
46
|
+
oauth2[key] = out[key] ?? (key === "grantType" ? "client_credentials" : key === "clientAuth" ? "body" : "");
|
|
47
|
+
delete out[key];
|
|
48
|
+
}
|
|
49
|
+
out.oauth2 = oauth2;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
};
|
|
53
|
+
const RAW_LANGUAGES = new Set(["json", "xml", "text", "html"]);
|
|
54
|
+
const translateBody = (body) => {
|
|
55
|
+
if (body === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
if (body === null)
|
|
58
|
+
return { mode: "none" };
|
|
59
|
+
if (typeof body === "string") {
|
|
60
|
+
const state = (0, callman_core_1.inferBodyStateFromRaw)(body, []);
|
|
61
|
+
return { mode: "raw", raw: body, rawLanguage: state.rawLanguage ?? "text" };
|
|
62
|
+
}
|
|
63
|
+
if (isRecord(body) && typeof body.mode === "string") {
|
|
64
|
+
const out = { ...body };
|
|
65
|
+
if (out.mode === "json")
|
|
66
|
+
return { mode: "raw", raw: JSON.stringify(out.json ?? null, null, 2), rawLanguage: "json" };
|
|
67
|
+
if (out.mode === "raw" && typeof out.language === "string" && RAW_LANGUAGES.has(out.language) && out.rawLanguage === undefined) {
|
|
68
|
+
out.rawLanguage = out.language;
|
|
69
|
+
delete out.language;
|
|
70
|
+
}
|
|
71
|
+
if ((out.mode === "formdata" || out.mode === "urlencoded") && Array.isArray(out.entries) && out.formEntries === undefined) {
|
|
72
|
+
out.formEntries = out.entries;
|
|
73
|
+
delete out.entries;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
// A bare JSON value — what get_request returns for JSON bodies.
|
|
78
|
+
return { mode: "raw", raw: JSON.stringify(body, null, 2), rawLanguage: "json" };
|
|
79
|
+
};
|
|
80
|
+
// get_request's `scripts: { preRequest, postResponse, tests }` → flat fields.
|
|
81
|
+
const translateScripts = (config) => {
|
|
82
|
+
const scripts = config.scripts;
|
|
83
|
+
if (!isRecord(scripts))
|
|
84
|
+
return;
|
|
85
|
+
if (typeof scripts.preRequest === "string" && config.preRequestScript === undefined)
|
|
86
|
+
config.preRequestScript = scripts.preRequest;
|
|
87
|
+
if (typeof scripts.postResponse === "string" && config.postResponseScript === undefined)
|
|
88
|
+
config.postResponseScript = scripts.postResponse;
|
|
89
|
+
if (typeof scripts.tests === "string" && config.testsScript === undefined)
|
|
90
|
+
config.testsScript = scripts.tests;
|
|
91
|
+
delete config.scripts;
|
|
92
|
+
};
|
|
93
|
+
const translateRequestConfig = (config) => {
|
|
94
|
+
const out = { ...config };
|
|
95
|
+
if ("auth" in out)
|
|
96
|
+
out.auth = translateAuth(out.auth);
|
|
97
|
+
if ("body" in out)
|
|
98
|
+
out.body = translateBody(out.body);
|
|
99
|
+
translateScripts(out);
|
|
100
|
+
return out;
|
|
101
|
+
};
|
|
102
|
+
exports.translateRequestConfig = translateRequestConfig;
|
|
103
|
+
// ── convenience fixes the backend would reject or ignore ──────────────
|
|
104
|
+
const withRuleIds = (node) => {
|
|
105
|
+
if (!isRecord(node))
|
|
106
|
+
return node;
|
|
107
|
+
if (Array.isArray(node.children)) {
|
|
108
|
+
return { id: node.id ?? newId("g"), type: node.type ?? "group", ...node, children: node.children.map(withRuleIds) };
|
|
109
|
+
}
|
|
110
|
+
if ("left" in node && "operator" in node)
|
|
111
|
+
return { id: node.id ?? newId("r"), type: node.type ?? "rule", ...node };
|
|
112
|
+
return node;
|
|
113
|
+
};
|
|
114
|
+
const normalizeNodeConfig = (type, config) => {
|
|
115
|
+
const notes = [];
|
|
116
|
+
let out = { ...config };
|
|
117
|
+
if (type === "request")
|
|
118
|
+
out = (0, exports.translateRequestConfig)(out);
|
|
119
|
+
if (type === "poll" && isRecord(out.request))
|
|
120
|
+
out.request = (0, exports.translateRequestConfig)(out.request);
|
|
121
|
+
if (type === "condition" || type === "poll") {
|
|
122
|
+
const target = type === "poll" && isRecord(out.successCondition) ? { ...out.successCondition } : out;
|
|
123
|
+
if (typeof target.expertExpression === "string" && target.expertExpression.trim() && target.mode === undefined) {
|
|
124
|
+
target.mode = "expert";
|
|
125
|
+
notes.push("expertExpression given without mode — set mode: \"expert\" (builder mode ignores it)");
|
|
126
|
+
}
|
|
127
|
+
if (target.rootGroup !== undefined)
|
|
128
|
+
target.rootGroup = withRuleIds(target.rootGroup);
|
|
129
|
+
if (type === "poll" && isRecord(out.successCondition))
|
|
130
|
+
out.successCondition = target;
|
|
131
|
+
}
|
|
132
|
+
if (type === "assertion" && Array.isArray(out.checks)) {
|
|
133
|
+
out.checks = out.checks.map((check) => {
|
|
134
|
+
if (!isRecord(check))
|
|
135
|
+
return check;
|
|
136
|
+
const id = typeof check.id === "string" ? check.id : newId("c");
|
|
137
|
+
return { id, name: check.name ?? "", enabled: check.enabled ?? true, ...check, rule: withRuleIds(check.rule) };
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return { config: out, notes };
|
|
141
|
+
};
|
|
142
|
+
exports.normalizeNodeConfig = normalizeNodeConfig;
|
|
143
|
+
// ── what the model must supply (defaults are placeholders) ────────────
|
|
144
|
+
const missingRequiredFields = (type, userConfig, base) => {
|
|
145
|
+
const has = (key) => {
|
|
146
|
+
const value = userConfig[key] ?? base?.[key];
|
|
147
|
+
return typeof value === "string" ? value.trim().length > 0 : Array.isArray(value) ? value.length > 0 : value !== undefined && value !== null;
|
|
148
|
+
};
|
|
149
|
+
const missing = [];
|
|
150
|
+
switch (type) {
|
|
151
|
+
case "request":
|
|
152
|
+
if (!has("url"))
|
|
153
|
+
missing.push("url");
|
|
154
|
+
break;
|
|
155
|
+
case "db":
|
|
156
|
+
// The default query is a placeholder (SELECT * FROM users …) — never run it by accident.
|
|
157
|
+
if (!(typeof userConfig.query === "string" && userConfig.query.trim()) && !(base && typeof base.query === "string" && base.query.trim()))
|
|
158
|
+
missing.push("query");
|
|
159
|
+
break;
|
|
160
|
+
case "ai":
|
|
161
|
+
if (!has("connectionId"))
|
|
162
|
+
missing.push("connectionId (list_connections type ai)");
|
|
163
|
+
break;
|
|
164
|
+
case "assertion":
|
|
165
|
+
if (!has("checks"))
|
|
166
|
+
missing.push("checks");
|
|
167
|
+
break;
|
|
168
|
+
default:
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
return missing;
|
|
172
|
+
};
|
|
173
|
+
exports.missingRequiredFields = missingRequiredFields;
|
|
174
|
+
// ── masked / clipped values ───────────────────────────────────────────
|
|
175
|
+
const findUnwritableValues = (value, path = "config", out = []) => {
|
|
176
|
+
if (typeof value === "string" && (0, mask_js_1.isMasked)(value))
|
|
177
|
+
out.push(`${path} (masked ••••)`);
|
|
178
|
+
else if (Array.isArray(value))
|
|
179
|
+
value.forEach((item, index) => (0, exports.findUnwritableValues)(item, `${path}[${index}]`, out));
|
|
180
|
+
else if (isRecord(value)) {
|
|
181
|
+
if (value._truncated === true)
|
|
182
|
+
out.push(`${path} (clipped preview — read it with get_scenario node: <id>)`);
|
|
183
|
+
else
|
|
184
|
+
for (const [key, child] of Object.entries(value))
|
|
185
|
+
(0, exports.findUnwritableValues)(child, `${path}.${key}`, out);
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
};
|
|
189
|
+
exports.findUnwritableValues = findUnwritableValues;
|
|
190
|
+
// A masked string equal to the mask of the stored value at the same path
|
|
191
|
+
// means "keep what is stored".
|
|
192
|
+
const restoreMasked = (value, stored) => {
|
|
193
|
+
if (typeof value === "string") {
|
|
194
|
+
return (0, mask_js_1.isMasked)(value) && typeof stored === "string" && stored && (0, mask_js_1.maskSecret)(stored) === value ? stored : value;
|
|
195
|
+
}
|
|
196
|
+
if (Array.isArray(value)) {
|
|
197
|
+
return value.map((item, index) => {
|
|
198
|
+
const storedItem = Array.isArray(stored)
|
|
199
|
+
? (isRecord(item) && typeof item.key === "string" ? stored.find((entry) => isRecord(entry) && entry.key === item.key) : stored[index])
|
|
200
|
+
: undefined;
|
|
201
|
+
return (0, exports.restoreMasked)(item, storedItem);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (isRecord(value)) {
|
|
205
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, (0, exports.restoreMasked)(child, isRecord(stored) ? stored[key] : undefined)]));
|
|
206
|
+
}
|
|
207
|
+
return value;
|
|
208
|
+
};
|
|
209
|
+
exports.restoreMasked = restoreMasked;
|
|
210
|
+
// ── reading: mask secrets without changing the shape ─────────────────
|
|
211
|
+
const SECRET_FIELDS = new Set([
|
|
212
|
+
"bearerToken",
|
|
213
|
+
"basicPassword",
|
|
214
|
+
"apiKeyValue",
|
|
215
|
+
"clientSecret",
|
|
216
|
+
"password",
|
|
217
|
+
"refreshToken",
|
|
218
|
+
"pfxBase64",
|
|
219
|
+
"keyPem",
|
|
220
|
+
"passphrase",
|
|
221
|
+
"secret",
|
|
222
|
+
"token",
|
|
223
|
+
"apiKey",
|
|
224
|
+
]);
|
|
225
|
+
const maskConfigInPlace = (value, keyName = "") => {
|
|
226
|
+
if (typeof value === "string")
|
|
227
|
+
return SECRET_FIELDS.has(keyName) && value && !value.startsWith("{{") ? (0, mask_js_1.maskSecret)(value) : value;
|
|
228
|
+
if (Array.isArray(value)) {
|
|
229
|
+
return value.map((item) => {
|
|
230
|
+
// Header rows: { key: "Authorization", value: "…" }.
|
|
231
|
+
if (isRecord(item) && typeof item.key === "string" && typeof item.value === "string" && (0, mask_js_1.isSensitiveHeader)(item.key) && item.value && !item.value.startsWith("{{")) {
|
|
232
|
+
return { ...item, value: (0, mask_js_1.maskSecret)(item.value) };
|
|
233
|
+
}
|
|
234
|
+
return (0, exports.maskConfigInPlace)(item);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (isRecord(value))
|
|
238
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, (0, exports.maskConfigInPlace)(child, key)]));
|
|
239
|
+
return value;
|
|
240
|
+
};
|
|
241
|
+
exports.maskConfigInPlace = maskConfigInPlace;
|
|
242
|
+
// ── sent vs stored ────────────────────────────────────────────────────
|
|
243
|
+
// Keys present in what we sent but absent from what the server stored —
|
|
244
|
+
// the backend's node schemas strip unknown keys without an error.
|
|
245
|
+
const droppedPaths = (sent, stored, path = "") => {
|
|
246
|
+
if (!isRecord(sent))
|
|
247
|
+
return [];
|
|
248
|
+
if (!isRecord(stored))
|
|
249
|
+
return [];
|
|
250
|
+
const out = [];
|
|
251
|
+
for (const [key, value] of Object.entries(sent)) {
|
|
252
|
+
const here = path ? `${path}.${key}` : key;
|
|
253
|
+
if (!(key in stored))
|
|
254
|
+
out.push(here);
|
|
255
|
+
else if (isRecord(value))
|
|
256
|
+
out.push(...(0, exports.droppedPaths)(value, stored[key], here));
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
};
|
|
260
|
+
exports.droppedPaths = droppedPaths;
|