@salesforce/graphiti 10.10.2
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/AGENT_GUIDE.md +424 -0
- package/CHANGELOG.md +448 -0
- package/LICENSE.txt +82 -0
- package/README.md +204 -0
- package/TASK.md +249 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +683 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/args.d.ts +13 -0
- package/dist/commands/args.js +207 -0
- package/dist/commands/args.js.map +1 -0
- package/dist/commands/build.d.ts +11 -0
- package/dist/commands/build.js +209 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/connect.d.ts +8 -0
- package/dist/commands/connect.js +55 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/describe.d.ts +6 -0
- package/dist/commands/describe.js +229 -0
- package/dist/commands/describe.js.map +1 -0
- package/dist/commands/meta.d.ts +9 -0
- package/dist/commands/meta.js +140 -0
- package/dist/commands/meta.js.map +1 -0
- package/dist/commands/navigate.d.ts +14 -0
- package/dist/commands/navigate.js +105 -0
- package/dist/commands/navigate.js.map +1 -0
- package/dist/commands/query-helpers.d.ts +80 -0
- package/dist/commands/query-helpers.js +865 -0
- package/dist/commands/query-helpers.js.map +1 -0
- package/dist/commands/query.d.ts +26 -0
- package/dist/commands/query.js +901 -0
- package/dist/commands/query.js.map +1 -0
- package/dist/commands/review.d.ts +18 -0
- package/dist/commands/review.js +533 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/session-mgmt.d.ts +25 -0
- package/dist/commands/session-mgmt.js +206 -0
- package/dist/commands/session-mgmt.js.map +1 -0
- package/dist/commands/type.d.ts +6 -0
- package/dist/commands/type.js +342 -0
- package/dist/commands/type.js.map +1 -0
- package/dist/commands/validate-input.d.ts +6 -0
- package/dist/commands/validate-input.js +32 -0
- package/dist/commands/validate-input.js.map +1 -0
- package/dist/intent/build-aggregate.d.ts +33 -0
- package/dist/intent/build-aggregate.js +134 -0
- package/dist/intent/build-aggregate.js.map +1 -0
- package/dist/intent/build-create.d.ts +14 -0
- package/dist/intent/build-create.js +16 -0
- package/dist/intent/build-create.js.map +1 -0
- package/dist/intent/build-delete.d.ts +30 -0
- package/dist/intent/build-delete.js +53 -0
- package/dist/intent/build-delete.js.map +1 -0
- package/dist/intent/build-detail.d.ts +32 -0
- package/dist/intent/build-detail.js +80 -0
- package/dist/intent/build-detail.js.map +1 -0
- package/dist/intent/build-discover.d.ts +30 -0
- package/dist/intent/build-discover.js +149 -0
- package/dist/intent/build-discover.js.map +1 -0
- package/dist/intent/build-list.d.ts +28 -0
- package/dist/intent/build-list.js +75 -0
- package/dist/intent/build-list.js.map +1 -0
- package/dist/intent/build-mutation.d.ts +23 -0
- package/dist/intent/build-mutation.js +54 -0
- package/dist/intent/build-mutation.js.map +1 -0
- package/dist/intent/build-output.d.ts +27 -0
- package/dist/intent/build-output.js +60 -0
- package/dist/intent/build-output.js.map +1 -0
- package/dist/intent/build-raw.d.ts +23 -0
- package/dist/intent/build-raw.js +54 -0
- package/dist/intent/build-raw.js.map +1 -0
- package/dist/intent/build-update.d.ts +14 -0
- package/dist/intent/build-update.js +16 -0
- package/dist/intent/build-update.js.map +1 -0
- package/dist/intent/get-schema-with-priming.d.ts +26 -0
- package/dist/intent/get-schema-with-priming.js +32 -0
- package/dist/intent/get-schema-with-priming.js.map +1 -0
- package/dist/intent/select-child-relationship.d.ts +19 -0
- package/dist/intent/select-child-relationship.js +38 -0
- package/dist/intent/select-child-relationship.js.map +1 -0
- package/dist/intent/types.d.ts +159 -0
- package/dist/intent/types.js +21 -0
- package/dist/intent/types.js.map +1 -0
- package/dist/lib/apply-command.d.ts +15 -0
- package/dist/lib/apply-command.js +238 -0
- package/dist/lib/apply-command.js.map +1 -0
- package/dist/lib/auth.d.ts +38 -0
- package/dist/lib/auth.js +113 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/codegen.d.ts +32 -0
- package/dist/lib/codegen.js +700 -0
- package/dist/lib/codegen.js.map +1 -0
- package/dist/lib/command-registry.d.ts +59 -0
- package/dist/lib/command-registry.js +366 -0
- package/dist/lib/command-registry.js.map +1 -0
- package/dist/lib/formatter.d.ts +76 -0
- package/dist/lib/formatter.js +419 -0
- package/dist/lib/formatter.js.map +1 -0
- package/dist/lib/fs-utils.d.ts +23 -0
- package/dist/lib/fs-utils.js +46 -0
- package/dist/lib/fs-utils.js.map +1 -0
- package/dist/lib/graphql-name.d.ts +27 -0
- package/dist/lib/graphql-name.js +32 -0
- package/dist/lib/graphql-name.js.map +1 -0
- package/dist/lib/interactive.d.ts +6 -0
- package/dist/lib/interactive.js +562 -0
- package/dist/lib/interactive.js.map +1 -0
- package/dist/lib/introspect.d.ts +40 -0
- package/dist/lib/introspect.js +280 -0
- package/dist/lib/introspect.js.map +1 -0
- package/dist/lib/object-info.d.ts +79 -0
- package/dist/lib/object-info.js +313 -0
- package/dist/lib/object-info.js.map +1 -0
- package/dist/lib/path-selection.d.ts +50 -0
- package/dist/lib/path-selection.js +146 -0
- package/dist/lib/path-selection.js.map +1 -0
- package/dist/lib/prime-schema.d.ts +59 -0
- package/dist/lib/prime-schema.js +158 -0
- package/dist/lib/prime-schema.js.map +1 -0
- package/dist/lib/query-builder.d.ts +10 -0
- package/dist/lib/query-builder.js +168 -0
- package/dist/lib/query-builder.js.map +1 -0
- package/dist/lib/query-commands.d.ts +19 -0
- package/dist/lib/query-commands.js +262 -0
- package/dist/lib/query-commands.js.map +1 -0
- package/dist/lib/session.d.ts +205 -0
- package/dist/lib/session.js +1075 -0
- package/dist/lib/session.js.map +1 -0
- package/dist/lib/tokenize.d.ts +12 -0
- package/dist/lib/tokenize.js +48 -0
- package/dist/lib/tokenize.js.map +1 -0
- package/dist/lib/uiapi.d.ts +109 -0
- package/dist/lib/uiapi.js +159 -0
- package/dist/lib/uiapi.js.map +1 -0
- package/dist/lib/validator.d.ts +27 -0
- package/dist/lib/validator.js +100 -0
- package/dist/lib/validator.js.map +1 -0
- package/dist/lib/variable-promotion.d.ts +69 -0
- package/dist/lib/variable-promotion.js +95 -0
- package/dist/lib/variable-promotion.js.map +1 -0
- package/dist/lib/walker.d.ts +147 -0
- package/dist/lib/walker.js +723 -0
- package/dist/lib/walker.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +34 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.d.ts +7 -0
- package/dist/mcp/stdio.js +25 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/mcp/tools/echo.d.ts +7 -0
- package/dist/mcp/tools/echo.js +17 -0
- package/dist/mcp/tools/echo.js.map +1 -0
- package/dist/mcp/tools/sf-gql-aggregate.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-aggregate.js +75 -0
- package/dist/mcp/tools/sf-gql-aggregate.js.map +1 -0
- package/dist/mcp/tools/sf-gql-create.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-create.js +35 -0
- package/dist/mcp/tools/sf-gql-create.js.map +1 -0
- package/dist/mcp/tools/sf-gql-delete.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-delete.js +31 -0
- package/dist/mcp/tools/sf-gql-delete.js.map +1 -0
- package/dist/mcp/tools/sf-gql-detail.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-detail.js +58 -0
- package/dist/mcp/tools/sf-gql-detail.js.map +1 -0
- package/dist/mcp/tools/sf-gql-discover.d.ts +9 -0
- package/dist/mcp/tools/sf-gql-discover.js +51 -0
- package/dist/mcp/tools/sf-gql-discover.js.map +1 -0
- package/dist/mcp/tools/sf-gql-list.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-list.js +53 -0
- package/dist/mcp/tools/sf-gql-list.js.map +1 -0
- package/dist/mcp/tools/sf-gql-raw.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-raw.js +39 -0
- package/dist/mcp/tools/sf-gql-raw.js.map +1 -0
- package/dist/mcp/tools/sf-gql-update.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-update.js +35 -0
- package/dist/mcp/tools/sf-gql-update.js.map +1 -0
- package/dist/mcp/tools/shared/zod-schemas.d.ts +49 -0
- package/dist/mcp/tools/shared/zod-schemas.js +46 -0
- package/dist/mcp/tools/shared/zod-schemas.js.map +1 -0
- package/package.json +36 -0
- package/ralph-loop.sh +120 -0
- package/scripts/smoke-orderby.sh +190 -0
- package/src/__tests__/helpers/prime-deps.ts +46 -0
- package/src/__tests__/helpers/schema.ts +73 -0
- package/src/__tests__/helpers/stdout.ts +33 -0
- package/src/__tests__/setup.ts +19 -0
- package/src/cli.ts +764 -0
- package/src/commands/__tests__/query.spec.ts +137 -0
- package/src/commands/args.ts +306 -0
- package/src/commands/build.ts +288 -0
- package/src/commands/connect.ts +60 -0
- package/src/commands/describe.ts +246 -0
- package/src/commands/meta.ts +171 -0
- package/src/commands/navigate.ts +134 -0
- package/src/commands/query-helpers.ts +1202 -0
- package/src/commands/query.ts +1085 -0
- package/src/commands/review.ts +670 -0
- package/src/commands/session-mgmt.ts +256 -0
- package/src/commands/type.ts +437 -0
- package/src/commands/validate-input.ts +38 -0
- package/src/intent/__tests__/build-aggregate.spec.ts +931 -0
- package/src/intent/__tests__/build-create-validation.spec.ts +135 -0
- package/src/intent/__tests__/build-delete.spec.ts +121 -0
- package/src/intent/__tests__/build-detail.spec.ts +333 -0
- package/src/intent/__tests__/build-discover.spec.ts +432 -0
- package/src/intent/__tests__/build-list.spec.ts +284 -0
- package/src/intent/__tests__/build-mutation.spec.ts +108 -0
- package/src/intent/__tests__/build-output.spec.ts +55 -0
- package/src/intent/__tests__/build-raw.spec.ts +153 -0
- package/src/intent/__tests__/build-update-validation.spec.ts +134 -0
- package/src/intent/build-aggregate.ts +182 -0
- package/src/intent/build-create.ts +19 -0
- package/src/intent/build-delete.ts +62 -0
- package/src/intent/build-detail.ts +95 -0
- package/src/intent/build-discover.ts +199 -0
- package/src/intent/build-list.ts +91 -0
- package/src/intent/build-mutation.ts +75 -0
- package/src/intent/build-output.ts +72 -0
- package/src/intent/build-raw.ts +61 -0
- package/src/intent/build-update.ts +19 -0
- package/src/intent/get-schema-with-priming.ts +43 -0
- package/src/intent/select-child-relationship.ts +48 -0
- package/src/intent/types.ts +181 -0
- package/src/lib/__tests__/apply-command.parity.spec.ts +97 -0
- package/src/lib/__tests__/apply-command.spec.ts +171 -0
- package/src/lib/__tests__/auth.spec.ts +292 -0
- package/src/lib/__tests__/formatter.spec.ts +86 -0
- package/src/lib/__tests__/graphql-name.spec.ts +64 -0
- package/src/lib/__tests__/introspect.spec.ts +399 -0
- package/src/lib/__tests__/object-info.spec.ts +124 -0
- package/src/lib/__tests__/path-selection.spec.ts +219 -0
- package/src/lib/__tests__/prime-schema.spec.ts +269 -0
- package/src/lib/__tests__/query-builder.spec.ts +95 -0
- package/src/lib/__tests__/query-commands.spec.ts +74 -0
- package/src/lib/__tests__/session.spec.ts +292 -0
- package/src/lib/__tests__/tokenize.spec.ts +33 -0
- package/src/lib/__tests__/uiapi.spec.ts +192 -0
- package/src/lib/__tests__/variable-promotion.spec.ts +211 -0
- package/src/lib/__tests__/walker.spec.ts +250 -0
- package/src/lib/apply-command.ts +286 -0
- package/src/lib/auth.ts +157 -0
- package/src/lib/codegen.ts +899 -0
- package/src/lib/command-registry.ts +434 -0
- package/src/lib/formatter.ts +587 -0
- package/src/lib/fs-utils.ts +46 -0
- package/src/lib/graphql-name.ts +35 -0
- package/src/lib/interactive.ts +634 -0
- package/src/lib/introspect.ts +320 -0
- package/src/lib/object-info.ts +409 -0
- package/src/lib/path-selection.ts +162 -0
- package/src/lib/prime-schema.ts +195 -0
- package/src/lib/query-builder.ts +193 -0
- package/src/lib/query-commands.ts +290 -0
- package/src/lib/session.ts +1304 -0
- package/src/lib/tokenize.ts +43 -0
- package/src/lib/uiapi.ts +176 -0
- package/src/lib/validator.ts +143 -0
- package/src/lib/variable-promotion.ts +145 -0
- package/src/lib/walker.ts +975 -0
- package/src/mcp/__tests__/server.spec.ts +155 -0
- package/src/mcp/server.ts +38 -0
- package/src/mcp/stdio.ts +29 -0
- package/src/mcp/tools/__tests__/sf-gql-aggregate.spec.ts +173 -0
- package/src/mcp/tools/__tests__/sf-gql-create.spec.ts +235 -0
- package/src/mcp/tools/__tests__/sf-gql-delete.spec.ts +194 -0
- package/src/mcp/tools/__tests__/sf-gql-detail.spec.ts +246 -0
- package/src/mcp/tools/__tests__/sf-gql-discover.spec.ts +320 -0
- package/src/mcp/tools/__tests__/sf-gql-list.spec.ts +128 -0
- package/src/mcp/tools/__tests__/sf-gql-raw.spec.ts +141 -0
- package/src/mcp/tools/__tests__/sf-gql-update.spec.ts +207 -0
- package/src/mcp/tools/echo.ts +24 -0
- package/src/mcp/tools/sf-gql-aggregate.ts +102 -0
- package/src/mcp/tools/sf-gql-create.ts +55 -0
- package/src/mcp/tools/sf-gql-delete.ts +49 -0
- package/src/mcp/tools/sf-gql-detail.ts +85 -0
- package/src/mcp/tools/sf-gql-discover.ts +67 -0
- package/src/mcp/tools/sf-gql-list.ts +73 -0
- package/src/mcp/tools/sf-gql-raw.ts +56 -0
- package/src/mcp/tools/sf-gql-update.ts +55 -0
- package/src/mcp/tools/shared/zod-schemas.ts +55 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Split a command line into tokens, honoring single and double quotes.
|
|
9
|
+
* A quote only opens a quoted span at a token boundary (when `current` is
|
|
10
|
+
* empty); a quote char mid-token is kept literally. Shared by the CLI
|
|
11
|
+
* `chain` command and the MCP `sf_gql_raw` parser.
|
|
12
|
+
*/
|
|
13
|
+
export function tokenizeCommand(input: string): string[] {
|
|
14
|
+
const tokens: string[] = [];
|
|
15
|
+
let current = "";
|
|
16
|
+
let inSingle = false;
|
|
17
|
+
let inDouble = false;
|
|
18
|
+
|
|
19
|
+
for (const ch of input) {
|
|
20
|
+
if (ch === "'" && !inDouble) {
|
|
21
|
+
if (!inSingle && current.length > 0) {
|
|
22
|
+
current += ch;
|
|
23
|
+
} else {
|
|
24
|
+
inSingle = !inSingle;
|
|
25
|
+
}
|
|
26
|
+
} else if (ch === '"' && !inSingle) {
|
|
27
|
+
if (!inDouble && current.length > 0) {
|
|
28
|
+
current += ch;
|
|
29
|
+
} else {
|
|
30
|
+
inDouble = !inDouble;
|
|
31
|
+
}
|
|
32
|
+
} else if (ch === " " && !inSingle && !inDouble) {
|
|
33
|
+
if (current) {
|
|
34
|
+
tokens.push(current);
|
|
35
|
+
current = "";
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
current += ch;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (current) tokens.push(current);
|
|
42
|
+
return tokens;
|
|
43
|
+
}
|
package/src/lib/uiapi.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Salesforce UIAPI shape facts as a public library surface.
|
|
9
|
+
*
|
|
10
|
+
* The graphiti library below this module is GraphQL-generic — it knows
|
|
11
|
+
* about sessions, projection nodes, schema walking, and validation, but
|
|
12
|
+
* not about Salesforce. The MCP server above this module composes
|
|
13
|
+
* intent-shaped builders (`buildList`, `buildDetail`, etc.) that bake
|
|
14
|
+
* in product opinions like pagination defaults and mutation return
|
|
15
|
+
* shapes.
|
|
16
|
+
*
|
|
17
|
+
* In between, every consumer that targets Salesforce UIAPI ends up
|
|
18
|
+
* needing the same handful of facts: where the connection field lives,
|
|
19
|
+
* how mutation results are nested, when to unwrap a value wrapper,
|
|
20
|
+
* what the filter / orderBy / input type names look like. This module
|
|
21
|
+
* collects those facts so the MCP, the CLI, the eval harness, and any
|
|
22
|
+
* future surface can call into one place instead of re-discovering
|
|
23
|
+
* each convention.
|
|
24
|
+
*
|
|
25
|
+
* What belongs here:
|
|
26
|
+
* - Path constructors for UIAPI's nested shapes.
|
|
27
|
+
* - Type-shape detection (value wrappers).
|
|
28
|
+
* - Naming conventions for filter / orderBy / mutation input types.
|
|
29
|
+
*
|
|
30
|
+
* What does NOT belong here:
|
|
31
|
+
* - Pagination defaults, "always select Id on mutations", "$id: ID!"
|
|
32
|
+
* conventions — those are MCP-specific opinions.
|
|
33
|
+
* - Spec types (`ListSpec`, `DetailSpec`, etc.) — those are MCP tool
|
|
34
|
+
* surface, not graphiti API.
|
|
35
|
+
* - Schema priming, codegen packaging, warning filtering — those
|
|
36
|
+
* wrap library output for the MCP's `ToolOutput` and live in MCP.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { isInterfaceType, isObjectType, type GraphQLSchema } from "graphql";
|
|
40
|
+
import { type OperationType } from "./session.js";
|
|
41
|
+
import { resolvePath } from "./walker.js";
|
|
42
|
+
|
|
43
|
+
// ── Path constructors ─────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/** `["uiapi", "query", <object>]` — the connection field for an SObject. */
|
|
46
|
+
export function connectionPath(object: string): string[] {
|
|
47
|
+
return ["uiapi", "query", object];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Given any UIAPI connection-field path, returns the per-record selection
|
|
52
|
+
* scope inside it (`[...connection, "edges", "node"]`). Works for top-level
|
|
53
|
+
* connections AND for nested child-relationship connections.
|
|
54
|
+
*/
|
|
55
|
+
export function connectionNodePath(connection: string[]): string[] {
|
|
56
|
+
return [...connection, "edges", "node"];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* `["uiapi", "query", <object>, "edges", "node"]` — the per-record selection
|
|
61
|
+
* scope for a top-level list query. Composition of `connectionNodePath` +
|
|
62
|
+
* `connectionPath` for the common case.
|
|
63
|
+
*/
|
|
64
|
+
export function nodePath(object: string): string[] {
|
|
65
|
+
return connectionNodePath(connectionPath(object));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* `[...connection, "pageInfo"]` — the cursor-pagination metadata path on
|
|
70
|
+
* any UIAPI connection field. Selecting fields here is a caller opinion;
|
|
71
|
+
* the path itself is a UIAPI fact.
|
|
72
|
+
*/
|
|
73
|
+
export function pageInfoPath(connection: string[]): string[] {
|
|
74
|
+
return [...connection, "pageInfo"];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** `["uiapi", <object><Op>]` — the mutation field at the root of the operation. */
|
|
78
|
+
export function mutationFieldPath(object: string, op: "Create" | "Update" | "Delete"): string[] {
|
|
79
|
+
return ["uiapi", `${object}${op}`];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `["uiapi", <object><Op>, "Record"]` — the per-record selection scope inside
|
|
84
|
+
* a mutation result. Delete returns no record, so this only applies to
|
|
85
|
+
* Create / Update.
|
|
86
|
+
*/
|
|
87
|
+
export function mutationRecordPath(object: string, op: "Create" | "Update"): string[] {
|
|
88
|
+
return [...mutationFieldPath(object, op), "Record"];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** `["uiapi", "aggregate", <object>]` — the aggregate root for an SObject. */
|
|
92
|
+
export function aggregatePath(object: string): string[] {
|
|
93
|
+
return ["uiapi", "aggregate", object];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Type-shape detection ──────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns true when the named type is a UIAPI value wrapper — an object
|
|
100
|
+
* (or interface) type whose name ends in `Value` and that exposes a
|
|
101
|
+
* `value` field (e.g. `StringValue`, `PicklistValue`, `DateTimeValue`).
|
|
102
|
+
*
|
|
103
|
+
* Callers use this to decide whether to select `field` directly or
|
|
104
|
+
* `field { value }`. The check is permissive enough to catch the full
|
|
105
|
+
* Salesforce wrapper family without false positives on unrelated
|
|
106
|
+
* `*Value`-named types — those would also need a `value` field to
|
|
107
|
+
* satisfy the second condition.
|
|
108
|
+
*/
|
|
109
|
+
export function isValueWrapperType(schema: GraphQLSchema, typeName: string): boolean {
|
|
110
|
+
if (!typeName.endsWith("Value")) return false;
|
|
111
|
+
const t = schema.getType(typeName);
|
|
112
|
+
if (!t) return false;
|
|
113
|
+
if (!isObjectType(t) && !isInterfaceType(t)) return false;
|
|
114
|
+
return Object.prototype.hasOwnProperty.call(t.getFields(), "value");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolves the path that should actually be selected for a single
|
|
119
|
+
* (non-dotted) scalar field on `basePath`. Encapsulates two UIAPI
|
|
120
|
+
* facts in one call:
|
|
121
|
+
*
|
|
122
|
+
* - `Id` is a real scalar — return the path as-is.
|
|
123
|
+
* - `Name`-shaped fields resolve to a value wrapper — append "value".
|
|
124
|
+
* - Other shapes (raw scalars, leaves) — return the path as-is.
|
|
125
|
+
*
|
|
126
|
+
* Returns `null` when the path can't be resolved against the schema,
|
|
127
|
+
* letting the caller decide whether to throw, warn, or fall back. The
|
|
128
|
+
* helper deliberately does NOT call `selectLeaf` on the session — it
|
|
129
|
+
* returns the path so callers stay in control of the projection
|
|
130
|
+
* (alias handling, error reporting, etc.).
|
|
131
|
+
*
|
|
132
|
+
* For dotted paths that may traverse a polymorphic union, callers
|
|
133
|
+
* should use `selectDottedFieldPath` from `./path-selection.js`
|
|
134
|
+
* instead — that helper handles the union expansion AND calls into
|
|
135
|
+
* the equivalent of this resolver internally.
|
|
136
|
+
*/
|
|
137
|
+
export function resolveScalarSelectionPath(
|
|
138
|
+
schema: GraphQLSchema,
|
|
139
|
+
operation: OperationType,
|
|
140
|
+
basePath: string[],
|
|
141
|
+
fieldName: string,
|
|
142
|
+
): string[] | null {
|
|
143
|
+
const fullPath = [...basePath, fieldName];
|
|
144
|
+
if (fieldName === "Id") return fullPath;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const wr = resolvePath(schema, operation, fullPath);
|
|
148
|
+
if (wr.isLeaf) return fullPath;
|
|
149
|
+
if (isValueWrapperType(schema, wr.typeName)) return [...fullPath, "value"];
|
|
150
|
+
return fullPath;
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Naming conventions ────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
/** `<Object>_Filter` — the connection field's `where` input type name. */
|
|
159
|
+
export function filterTypeName(object: string): string {
|
|
160
|
+
return `${object}_Filter`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** `<Object>_OrderBy` — the connection field's `orderBy` singleton input type name. */
|
|
164
|
+
export function orderByTypeName(object: string): string {
|
|
165
|
+
return `${object}_OrderBy`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** `<Object>CreateInput` — the create mutation's `input` argument type name. */
|
|
169
|
+
export function createInputTypeName(object: string): string {
|
|
170
|
+
return `${object}CreateInput`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** `<Object>UpdateInput` — the update mutation's `input` argument type name. */
|
|
174
|
+
export function updateInputTypeName(object: string): string {
|
|
175
|
+
return `${object}UpdateInput`;
|
|
176
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- graphiti traverses untyped schema/introspection JSON; see follow-up to replace with `unknown` + narrowing */
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type GraphQLSchema,
|
|
10
|
+
type GraphQLInputObjectType,
|
|
11
|
+
parse,
|
|
12
|
+
validate,
|
|
13
|
+
isInputObjectType,
|
|
14
|
+
isEnumType,
|
|
15
|
+
isNonNullType,
|
|
16
|
+
getNamedType,
|
|
17
|
+
} from "graphql";
|
|
18
|
+
|
|
19
|
+
// ── Full query validation ────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface ValidationError {
|
|
22
|
+
message: string;
|
|
23
|
+
locations?: { line: number; column: number }[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validates a complete GraphQL query string against a schema.
|
|
28
|
+
* Uses graphql-js's parse() + validate() for comprehensive checking.
|
|
29
|
+
*/
|
|
30
|
+
export function validateQuery(schema: GraphQLSchema, queryString: string): ValidationError[] {
|
|
31
|
+
let document;
|
|
32
|
+
try {
|
|
33
|
+
document = parse(queryString);
|
|
34
|
+
} catch (err: any) {
|
|
35
|
+
return [{ message: `Parse error: ${err.message}` }];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const errors = validate(schema, document);
|
|
39
|
+
return errors.map((e) => ({
|
|
40
|
+
message: e.message,
|
|
41
|
+
locations: e.locations?.map((l) => ({ line: l.line, column: l.column })),
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Input type validation ────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export interface InputValidationError {
|
|
48
|
+
path: string;
|
|
49
|
+
message: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validates a JSON value against a named input type from the schema.
|
|
54
|
+
* Recursively checks field names, nested types, and basic type compatibility.
|
|
55
|
+
*/
|
|
56
|
+
export function validateInputValue(
|
|
57
|
+
schema: GraphQLSchema,
|
|
58
|
+
typeName: string,
|
|
59
|
+
value: unknown,
|
|
60
|
+
): InputValidationError[] {
|
|
61
|
+
const type = schema.getType(typeName);
|
|
62
|
+
if (!type) {
|
|
63
|
+
return [{ path: "", message: `Type "${typeName}" not found in schema` }];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!isInputObjectType(type)) {
|
|
67
|
+
return [
|
|
68
|
+
{
|
|
69
|
+
path: "",
|
|
70
|
+
message: `"${typeName}" is not an input type (it's a ${type.astNode?.kind ?? "unknown"})`,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const errors: InputValidationError[] = [];
|
|
76
|
+
validateInputObject(schema, type, value, "", errors);
|
|
77
|
+
return errors;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function validateInputObject(
|
|
81
|
+
schema: GraphQLSchema,
|
|
82
|
+
type: GraphQLInputObjectType,
|
|
83
|
+
value: unknown,
|
|
84
|
+
path: string,
|
|
85
|
+
errors: InputValidationError[],
|
|
86
|
+
): void {
|
|
87
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
88
|
+
errors.push({
|
|
89
|
+
path: path || "(root)",
|
|
90
|
+
message: `Expected an object for ${type.name}, got ${typeof value}`,
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const fields = type.getFields();
|
|
96
|
+
const valueObj = value as Record<string, unknown>;
|
|
97
|
+
|
|
98
|
+
for (const key of Object.keys(valueObj)) {
|
|
99
|
+
if (!fields[key]) {
|
|
100
|
+
const available = Object.keys(fields).slice(0, 10).join(", ");
|
|
101
|
+
errors.push({
|
|
102
|
+
path: path ? `${path}.${key}` : key,
|
|
103
|
+
message: `Unknown field "${key}" on ${type.name}. Available: ${available}${Object.keys(fields).length > 10 ? "..." : ""}`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
109
|
+
const fieldPath = path ? `${path}.${fieldName}` : fieldName;
|
|
110
|
+
const fieldValue = valueObj[fieldName];
|
|
111
|
+
|
|
112
|
+
if (fieldValue === undefined) {
|
|
113
|
+
if (isNonNullType(field.type) && field.defaultValue === undefined) {
|
|
114
|
+
errors.push({ path: fieldPath, message: `Required field "${fieldName}" is missing` });
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const namedType = getNamedType(field.type);
|
|
120
|
+
if (
|
|
121
|
+
namedType &&
|
|
122
|
+
isInputObjectType(namedType) &&
|
|
123
|
+
typeof fieldValue === "object" &&
|
|
124
|
+
fieldValue !== null
|
|
125
|
+
) {
|
|
126
|
+
if (Array.isArray(fieldValue)) {
|
|
127
|
+
fieldValue.forEach((item, i) => {
|
|
128
|
+
validateInputObject(schema, namedType, item, `${fieldPath}[${i}]`, errors);
|
|
129
|
+
});
|
|
130
|
+
} else {
|
|
131
|
+
validateInputObject(schema, namedType, fieldValue, fieldPath, errors);
|
|
132
|
+
}
|
|
133
|
+
} else if (namedType && isEnumType(namedType) && typeof fieldValue === "string") {
|
|
134
|
+
const validValues = namedType.getValues().map((v) => v.name);
|
|
135
|
+
if (!validValues.includes(fieldValue)) {
|
|
136
|
+
errors.push({
|
|
137
|
+
path: fieldPath,
|
|
138
|
+
message: `Invalid enum value "${fieldValue}" for ${namedType.name}. Valid: ${validValues.join(", ")}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Library helpers that close two correctness gaps in any consumer
|
|
9
|
+
* (CLI, MCP intent layer, eval harness) that turns a typed JSON spec
|
|
10
|
+
* into a GraphQL operation:
|
|
11
|
+
*
|
|
12
|
+
* - `promoteVariables` finds `$varName` string leaves anywhere in
|
|
13
|
+
* a filter / orderBy / scope value, infers each one's GraphQL
|
|
14
|
+
* type from the schema, and registers it on the session so the
|
|
15
|
+
* rendered operation declares the variables it references.
|
|
16
|
+
*
|
|
17
|
+
* - `normalizeOrderBy` collapses an array-shaped orderBy input to
|
|
18
|
+
* a singleton object, matching Salesforce's connection-field
|
|
19
|
+
* `<Object>_OrderBy` schema. Tolerating the array shape is
|
|
20
|
+
* necessary for clients (and earlier MCP versions) that inferred
|
|
21
|
+
* a list type.
|
|
22
|
+
*
|
|
23
|
+
* Both are pure (or session-mutating but otherwise pure) and have no
|
|
24
|
+
* dependency on the MCP server, the CLI, or any I/O. They live next
|
|
25
|
+
* to `path-selection.ts` as the second piece of the "what every
|
|
26
|
+
* graphiti consumer needs" library surface.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { type GraphQLSchema } from "graphql";
|
|
30
|
+
import { addVariable, type QuerySession } from "./session.js";
|
|
31
|
+
import { inferTypeFromArgsPath } from "../commands/query-helpers.js";
|
|
32
|
+
|
|
33
|
+
const VAR_PLACEHOLDER_RE = /^\$([A-Za-z_][\w]*)$/;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Recursively walks `value` looking for `$varName` string leaves. For
|
|
37
|
+
* each one, infers the GraphQL type at that input path via
|
|
38
|
+
* `inferTypeFromArgsPath` and calls `addVariable(session, name, type)`.
|
|
39
|
+
* Promoted variables strip a trailing `!` (query variables are
|
|
40
|
+
* nullable by convention; callers requiring NonNull use the `var`
|
|
41
|
+
* command directly).
|
|
42
|
+
*
|
|
43
|
+
* If type inference throws — for example because the input path
|
|
44
|
+
* doesn't exist on the schema — the helper falls back to `String`
|
|
45
|
+
* rather than failing the whole render. A wrong-type variable is
|
|
46
|
+
* easier for a user to debug than a hard error before they see their
|
|
47
|
+
* query.
|
|
48
|
+
*
|
|
49
|
+
* @param session Session to add variables to.
|
|
50
|
+
* @param schema The loaded GraphQL schema.
|
|
51
|
+
* @param fieldSchemaPath Path of the field whose argument we are
|
|
52
|
+
* promoting against (e.g. `["uiapi", "query",
|
|
53
|
+
* "Account"]`).
|
|
54
|
+
* @param argName The argument name (`"where"`, `"orderBy"`,
|
|
55
|
+
* `"scope"`).
|
|
56
|
+
* @param value The argument value — can be a primitive,
|
|
57
|
+
* array, or nested object.
|
|
58
|
+
* @param warnings Optional warnings sink. Strings starting with
|
|
59
|
+
* `$` that don't form a valid GraphQL Name
|
|
60
|
+
* (e.g. `$1var`, `$foo-bar`) push a warning
|
|
61
|
+
* here so callers see the typo instead of
|
|
62
|
+
* silently rendering the literal.
|
|
63
|
+
*/
|
|
64
|
+
export function promoteVariables(
|
|
65
|
+
session: QuerySession,
|
|
66
|
+
schema: GraphQLSchema,
|
|
67
|
+
fieldSchemaPath: string[],
|
|
68
|
+
argName: string,
|
|
69
|
+
value: unknown,
|
|
70
|
+
warnings?: string[],
|
|
71
|
+
): void {
|
|
72
|
+
walk(session, schema, fieldSchemaPath, argName, value, [], warnings);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function walk(
|
|
76
|
+
session: QuerySession,
|
|
77
|
+
schema: GraphQLSchema,
|
|
78
|
+
fieldSchemaPath: string[],
|
|
79
|
+
argName: string,
|
|
80
|
+
value: unknown,
|
|
81
|
+
pathInsideArg: string[],
|
|
82
|
+
warnings: string[] | undefined,
|
|
83
|
+
): void {
|
|
84
|
+
if (typeof value === "string") {
|
|
85
|
+
const m = VAR_PLACEHOLDER_RE.exec(value);
|
|
86
|
+
if (!m || !m[1]) {
|
|
87
|
+
if (value.startsWith("$") && warnings) {
|
|
88
|
+
const argPath = [argName, ...pathInsideArg].join(".");
|
|
89
|
+
warnings.push(
|
|
90
|
+
`Variable: '${value}' at '${argPath}' starts with '$' but is not a valid variable placeholder; rendered as a literal string. Variable names must match /^[A-Za-z_][A-Za-z0-9_]*$/.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const varName = m[1];
|
|
96
|
+
try {
|
|
97
|
+
const { inferredType } = inferTypeFromArgsPath(schema, session.operation, fieldSchemaPath, [
|
|
98
|
+
argName,
|
|
99
|
+
...pathInsideArg,
|
|
100
|
+
]);
|
|
101
|
+
const declared = inferredType.endsWith("!") ? inferredType.slice(0, -1) : inferredType;
|
|
102
|
+
addVariable(session, varName, declared);
|
|
103
|
+
} catch {
|
|
104
|
+
addVariable(session, varName, "String");
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (Array.isArray(value)) {
|
|
109
|
+
for (let i = 0; i < value.length; i++) {
|
|
110
|
+
walk(
|
|
111
|
+
session,
|
|
112
|
+
schema,
|
|
113
|
+
fieldSchemaPath,
|
|
114
|
+
argName,
|
|
115
|
+
value[i],
|
|
116
|
+
[...pathInsideArg, String(i)],
|
|
117
|
+
warnings,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (value && typeof value === "object") {
|
|
123
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
124
|
+
walk(session, schema, fieldSchemaPath, argName, v, [...pathInsideArg, k], warnings);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Collapses an array-shaped orderBy input to its first element, so
|
|
131
|
+
* callers can pass either shape without knowing Salesforce's
|
|
132
|
+
* connection-field schema requires a singleton `<Object>_OrderBy`.
|
|
133
|
+
*
|
|
134
|
+
* - `[{Name: "ASC"}, ...]` → `{Name: "ASC"}`
|
|
135
|
+
* - `[]` → `undefined`
|
|
136
|
+
* - `{Name: "ASC"}` → unchanged
|
|
137
|
+
* - `undefined` → `undefined`
|
|
138
|
+
*/
|
|
139
|
+
export function normalizeOrderBy<T>(orderBy: T | T[] | undefined): T | undefined {
|
|
140
|
+
if (orderBy === undefined) return undefined;
|
|
141
|
+
if (Array.isArray(orderBy)) {
|
|
142
|
+
return orderBy.length > 0 ? orderBy[0] : undefined;
|
|
143
|
+
}
|
|
144
|
+
return orderBy;
|
|
145
|
+
}
|