@salesforce/graphiti 11.31.15 → 11.32.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 +6 -0
- package/dist/intent/build-detail.js +3 -1
- package/dist/intent/build-detail.js.map +1 -1
- package/dist/intent/build-list.js +8 -1
- package/dist/intent/build-list.js.map +1 -1
- package/dist/intent/build-mutation.js +6 -1
- package/dist/intent/build-mutation.js.map +1 -1
- package/dist/intent/select-child-relationship.js +9 -0
- package/dist/intent/select-child-relationship.js.map +1 -1
- package/dist/lib/graphql-name.d.ts +18 -0
- package/dist/lib/graphql-name.js +22 -0
- package/dist/lib/graphql-name.js.map +1 -1
- package/dist/lib/variable-promotion.js +10 -0
- package/dist/lib/variable-promotion.js.map +1 -1
- package/dist/schemas/fields.d.ts +10 -0
- package/dist/schemas/fields.js +21 -2
- package/dist/schemas/fields.js.map +1 -1
- package/dist/schemas/input-schemas.js +14 -14
- package/dist/schemas/input-schemas.js.map +1 -1
- package/package.json +1 -1
- package/src/intent/__tests__/build-aggregate.spec.ts +29 -0
- package/src/intent/__tests__/build-create-validation.spec.ts +37 -0
- package/src/intent/__tests__/build-detail.spec.ts +96 -0
- package/src/intent/__tests__/build-list.spec.ts +162 -1
- package/src/intent/__tests__/build-update-validation.spec.ts +20 -0
- package/src/intent/build-detail.ts +3 -1
- package/src/intent/build-list.ts +8 -1
- package/src/intent/build-mutation.ts +6 -1
- package/src/intent/select-child-relationship.ts +10 -0
- package/src/lib/__tests__/graphql-name.spec.ts +72 -7
- package/src/lib/graphql-name.ts +26 -0
- package/src/lib/variable-promotion.ts +12 -0
- package/src/mcp/tools/__tests__/error-surface.contract.spec.ts +23 -13
- package/src/schemas/__tests__/input-schemas.spec.ts +153 -0
- package/src/schemas/fields.ts +29 -2
- package/src/schemas/input-schemas.ts +15 -13
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
AGGREGATE_INPUT,
|
|
10
|
+
CREATE_INPUT,
|
|
11
|
+
DETAIL_INPUT,
|
|
12
|
+
LIST_INPUT,
|
|
13
|
+
UPDATE_INPUT,
|
|
14
|
+
} from "../input-schemas.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* W-22735537 — zod-boundary charset validation for caller-supplied field-path /
|
|
18
|
+
* scope inputs. These guard the layered defense: a silent revert of a
|
|
19
|
+
* `dottedGraphqlName()` / `scopeArg()` field back to `z.string()` would reopen
|
|
20
|
+
* the selection-set injection at the MCP boundary and these tests would fail.
|
|
21
|
+
* (filter/orderBy object-KEY validation is enforced in the builder layer, not
|
|
22
|
+
* zod, so it is covered by the build-* specs, not here.)
|
|
23
|
+
*/
|
|
24
|
+
const BREAKOUT = "Id } injectedAlias: Name { value";
|
|
25
|
+
|
|
26
|
+
describe("schemas/input-schemas — field-path charset validation (W-22735537)", () => {
|
|
27
|
+
it("CREATE_INPUT rejects a returnFields breakout; accepts a dotted path", () => {
|
|
28
|
+
expect(
|
|
29
|
+
CREATE_INPUT.safeParse({ org: "o", object: "Account", returnFields: [BREAKOUT] }).success,
|
|
30
|
+
).toBe(false);
|
|
31
|
+
expect(
|
|
32
|
+
CREATE_INPUT.safeParse({ org: "o", object: "Account", returnFields: ["Id", "Owner.Name"] })
|
|
33
|
+
.success,
|
|
34
|
+
).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("UPDATE_INPUT rejects a returnFields breakout", () => {
|
|
38
|
+
expect(
|
|
39
|
+
UPDATE_INPUT.safeParse({ org: "o", object: "Account", returnFields: [BREAKOUT] }).success,
|
|
40
|
+
).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("LIST_INPUT rejects fields/parentFields/scope breakouts; accepts legit", () => {
|
|
44
|
+
expect(LIST_INPUT.safeParse({ org: "o", object: "Account", fields: [BREAKOUT] }).success).toBe(
|
|
45
|
+
false,
|
|
46
|
+
);
|
|
47
|
+
expect(
|
|
48
|
+
LIST_INPUT.safeParse({
|
|
49
|
+
org: "o",
|
|
50
|
+
object: "Account",
|
|
51
|
+
fields: ["Id"],
|
|
52
|
+
parentFields: [BREAKOUT],
|
|
53
|
+
}).success,
|
|
54
|
+
).toBe(false);
|
|
55
|
+
expect(
|
|
56
|
+
LIST_INPUT.safeParse({
|
|
57
|
+
org: "o",
|
|
58
|
+
object: "Account",
|
|
59
|
+
fields: ["Id"],
|
|
60
|
+
scope: "{}) { Id } x: Account(scope: MINE",
|
|
61
|
+
}).success,
|
|
62
|
+
).toBe(false);
|
|
63
|
+
expect(
|
|
64
|
+
LIST_INPUT.safeParse({
|
|
65
|
+
org: "o",
|
|
66
|
+
object: "Account",
|
|
67
|
+
fields: ["Id", "Owner.Name"],
|
|
68
|
+
scope: "MINE",
|
|
69
|
+
}).success,
|
|
70
|
+
).toBe(true);
|
|
71
|
+
expect(
|
|
72
|
+
LIST_INPUT.safeParse({ org: "o", object: "Account", fields: ["Id"], scope: "$myScope" })
|
|
73
|
+
.success,
|
|
74
|
+
).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("LIST_INPUT rejects a childRelationships[].fields breakout at the boundary", () => {
|
|
78
|
+
expect(
|
|
79
|
+
LIST_INPUT.safeParse({
|
|
80
|
+
org: "o",
|
|
81
|
+
object: "Account",
|
|
82
|
+
fields: ["Id"],
|
|
83
|
+
childRelationships: [{ relationshipName: "Contacts", fields: [BREAKOUT] }],
|
|
84
|
+
}).success,
|
|
85
|
+
).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("DETAIL_INPUT rejects a fields breakout; accepts a dotted path", () => {
|
|
89
|
+
expect(
|
|
90
|
+
DETAIL_INPUT.safeParse({ org: "myorg", object: "Account", fields: [BREAKOUT] }).success,
|
|
91
|
+
).toBe(false);
|
|
92
|
+
expect(
|
|
93
|
+
DETAIL_INPUT.safeParse({ org: "myorg", object: "Account", fields: ["Id", "Owner.Name"] })
|
|
94
|
+
.success,
|
|
95
|
+
).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("AGGREGATE_INPUT rejects an aggregations[].field breakout; accepts a single field", () => {
|
|
99
|
+
expect(
|
|
100
|
+
AGGREGATE_INPUT.safeParse({
|
|
101
|
+
org: "o",
|
|
102
|
+
object: "Account",
|
|
103
|
+
aggregations: [{ function: "sum", field: "Amount } evil { value" }],
|
|
104
|
+
}).success,
|
|
105
|
+
).toBe(false);
|
|
106
|
+
expect(
|
|
107
|
+
AGGREGATE_INPUT.safeParse({
|
|
108
|
+
org: "o",
|
|
109
|
+
object: "Account",
|
|
110
|
+
aggregations: [{ function: "sum", field: "Amount" }],
|
|
111
|
+
}).success,
|
|
112
|
+
).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// PR #678 review (Ciaran Hannigan): Salesforce custom-field __c syntax must be
|
|
116
|
+
// accepted. The dotted-path / Name regexes permit it today; this pins it so a
|
|
117
|
+
// future "no consecutive underscores" tweak can't silently start rejecting real
|
|
118
|
+
// fields across any caller-supplied field-path input.
|
|
119
|
+
it("accepts Salesforce custom/namespaced __c field syntax across every dotted-path input", () => {
|
|
120
|
+
const returnFields = ["Amount__c", "Owner.Custom__c", "MyNS__Field__c"];
|
|
121
|
+
expect(CREATE_INPUT.safeParse({ org: "o", object: "Account", returnFields }).success).toBe(
|
|
122
|
+
true,
|
|
123
|
+
);
|
|
124
|
+
expect(UPDATE_INPUT.safeParse({ org: "o", object: "Account", returnFields }).success).toBe(
|
|
125
|
+
true,
|
|
126
|
+
);
|
|
127
|
+
expect(
|
|
128
|
+
LIST_INPUT.safeParse({
|
|
129
|
+
org: "o",
|
|
130
|
+
object: "Account",
|
|
131
|
+
fields: ["Amount__c"],
|
|
132
|
+
parentFields: ["Owner.Custom__c"],
|
|
133
|
+
childRelationships: [{ relationshipName: "Contacts", fields: ["MyNS__Field__c"] }],
|
|
134
|
+
}).success,
|
|
135
|
+
).toBe(true);
|
|
136
|
+
expect(
|
|
137
|
+
DETAIL_INPUT.safeParse({
|
|
138
|
+
org: "o",
|
|
139
|
+
object: "Account",
|
|
140
|
+
fields: ["Amount__c"],
|
|
141
|
+
parentFields: ["Owner.Custom__c"],
|
|
142
|
+
}).success,
|
|
143
|
+
).toBe(true);
|
|
144
|
+
expect(
|
|
145
|
+
AGGREGATE_INPUT.safeParse({
|
|
146
|
+
org: "o",
|
|
147
|
+
object: "Account",
|
|
148
|
+
aggregations: [{ function: "sum", field: "Amount__c" }],
|
|
149
|
+
groupBy: ["MyNS__Field__c"],
|
|
150
|
+
}).success,
|
|
151
|
+
).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
});
|
package/src/schemas/fields.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { z } from "zod";
|
|
8
|
-
import { GRAPHQL_NAME_RE } from "../lib/graphql-name.js";
|
|
8
|
+
import { DOTTED_GRAPHQL_NAME_RE, GRAPHQL_NAME_RE } from "../lib/graphql-name.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Some MCP clients/models JSON-stringify complex tool arguments (e.g. send
|
|
@@ -59,6 +59,29 @@ export const quotedString = () => z.string().regex(/^".*"$/s);
|
|
|
59
59
|
export const graphqlName = (description: string): z.ZodString =>
|
|
60
60
|
z.string().regex(GRAPHQL_NAME_RE, "must be a valid GraphQL Name").describe(description);
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Zod validator for a dotted field path (`Id`, `Owner.Name`, …). Each
|
|
64
|
+
* `.`-separated segment must be a valid GraphQL Name. Applied to every
|
|
65
|
+
* caller-supplied field-selection input (`returnFields`, `fields`,
|
|
66
|
+
* `parentFields`, child-relationship `fields`) so a selection-set breakout is
|
|
67
|
+
* rejected at the MCP boundary (W-22735537); the builders carry the matching
|
|
68
|
+
* `assertDottedGraphqlName` guard for direct (CLI / eval) callers.
|
|
69
|
+
*/
|
|
70
|
+
export const dottedGraphqlName = (description: string): z.ZodString =>
|
|
71
|
+
z
|
|
72
|
+
.string()
|
|
73
|
+
.regex(DOTTED_GRAPHQL_NAME_RE, "must be a valid field path (dot-separated GraphQL Names)")
|
|
74
|
+
.describe(description);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* `scope` argument validator: a bare enum token (e.g. `MINE`, `EVERYTHING`) or a
|
|
78
|
+
* `$varName` placeholder. `scope` renders into an argument position, so an
|
|
79
|
+
* unconstrained string is a selection-set / argument breakout (W-22735537).
|
|
80
|
+
*/
|
|
81
|
+
const SCOPE_RE = /^\$?[A-Za-z_][A-Za-z0-9_]*$/;
|
|
82
|
+
export const scopeArg = (description: string): z.ZodString =>
|
|
83
|
+
z.string().regex(SCOPE_RE, "must be a Scope enum token or $varName").describe(description);
|
|
84
|
+
|
|
62
85
|
/**
|
|
63
86
|
* Org alias / username charset + length, matching `auth.ts`'s
|
|
64
87
|
* `assertValidOrgAlias`. Defense-in-depth at the MCP boundary so the
|
|
@@ -101,7 +124,11 @@ export const childRelationshipSchema = (orderBy: z.ZodTypeAny) =>
|
|
|
101
124
|
relationshipName: graphqlName(
|
|
102
125
|
'Child relationship API name, e.g. "Contacts", "Opportunities". Must be a valid GraphQL Name.',
|
|
103
126
|
),
|
|
104
|
-
fields: z.array(
|
|
127
|
+
fields: z.array(
|
|
128
|
+
dottedGraphqlName(
|
|
129
|
+
'Scalar field API names on the child record; dot-paths like "Owner.Name" allowed.',
|
|
130
|
+
),
|
|
131
|
+
),
|
|
105
132
|
first: jsonCoercible(
|
|
106
133
|
z.union([z.number().int().positive(), varPlaceholder(), intLiteralString(), quotedString()]),
|
|
107
134
|
).optional(),
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { z } from "zod";
|
|
20
20
|
import {
|
|
21
21
|
childRelationshipSchema,
|
|
22
|
+
dottedGraphqlName,
|
|
22
23
|
graphqlName,
|
|
23
24
|
intLiteralString,
|
|
24
25
|
jsonCoercible,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
orderByObject,
|
|
27
28
|
orgAlias,
|
|
28
29
|
quotedString,
|
|
30
|
+
scopeArg,
|
|
29
31
|
varPlaceholder,
|
|
30
32
|
} from "./fields.js";
|
|
31
33
|
import { GROUP_BY_FUNCTIONS } from "../intent/types.js";
|
|
@@ -48,12 +50,12 @@ export const LIST_INPUT = z.object({
|
|
|
48
50
|
org: z.string().describe("Org alias resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
|
|
49
51
|
object: graphqlName('SObject API name, e.g. "Account", "Case". Must be a valid GraphQL Name.'),
|
|
50
52
|
fields: z
|
|
51
|
-
.array(
|
|
53
|
+
.array(dottedGraphqlName('Scalar field API name; dot-paths like "Owner.Name" allowed.'))
|
|
52
54
|
.describe(
|
|
53
55
|
'Scalar field API names to select on the node. Dot-paths like "Owner.Name" are allowed.',
|
|
54
56
|
),
|
|
55
57
|
parentFields: z
|
|
56
|
-
.array(
|
|
58
|
+
.array(dottedGraphqlName('Dotted parent-relationship path, e.g. "Account.Name".'))
|
|
57
59
|
.optional()
|
|
58
60
|
.describe('Dotted parent-relationship paths, e.g. "Account.Name".'),
|
|
59
61
|
childRelationships: z.array(childRelationshipSchema(listOrderBySchema())).optional(),
|
|
@@ -76,7 +78,7 @@ export const LIST_INPUT = z.object({
|
|
|
76
78
|
.describe(
|
|
77
79
|
'Top-level connection page size as a NUMBER (e.g. 25, not "25"); defaults to 10. Pass a single "$varName" string to promote it to an optional Int variable.',
|
|
78
80
|
),
|
|
79
|
-
scope:
|
|
81
|
+
scope: scopeArg('Scope enum (e.g. "MINE", "EVERYTHING") or $varName.').optional(),
|
|
80
82
|
operationName: graphqlName(
|
|
81
83
|
"Override the GraphQL operation name. Defaults to <Object>List.",
|
|
82
84
|
).optional(),
|
|
@@ -104,12 +106,12 @@ export const DETAIL_INPUT = z
|
|
|
104
106
|
org: orgAlias("Org alias resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
|
|
105
107
|
object: graphqlName('SObject API name, e.g. "Account", "Case". Must be a valid GraphQL Name.'),
|
|
106
108
|
fields: z
|
|
107
|
-
.array(
|
|
109
|
+
.array(dottedGraphqlName('Scalar field API name; dot-paths like "Owner.Name" allowed.'))
|
|
108
110
|
.describe(
|
|
109
111
|
'Scalar field API names to select on the node. Dot-paths like "Owner.Name" are allowed.',
|
|
110
112
|
),
|
|
111
113
|
parentFields: z
|
|
112
|
-
.array(
|
|
114
|
+
.array(dottedGraphqlName('Dotted parent-relationship path, e.g. "Account.Name".'))
|
|
113
115
|
.optional()
|
|
114
116
|
.describe('Dotted parent-relationship paths, e.g. "Account.Name".'),
|
|
115
117
|
childRelationships: z.array(childRelationshipSchema(detailOrderBySchema())).optional(),
|
|
@@ -170,20 +172,20 @@ const aliasField = z
|
|
|
170
172
|
const aggregationSchema = z.discriminatedUnion("function", [
|
|
171
173
|
z.object({
|
|
172
174
|
function: z.enum(["count", "countDistinct"]),
|
|
173
|
-
field:
|
|
175
|
+
field: graphqlName('SObject field API name. Defaults to "Id" when omitted.').optional(),
|
|
174
176
|
alias: aliasField,
|
|
175
177
|
}),
|
|
176
178
|
z.object({
|
|
177
179
|
function: z.enum(["sum", "avg", "min", "max"]),
|
|
178
|
-
field:
|
|
180
|
+
field: graphqlName("SObject field API name. Required for sum/avg/min/max."),
|
|
179
181
|
alias: aliasField,
|
|
180
182
|
}),
|
|
181
183
|
]);
|
|
182
184
|
|
|
183
185
|
const groupByElementSchema = z.union([
|
|
184
|
-
|
|
186
|
+
graphqlName("SObject field API name to group by (flat, non-dotted)."),
|
|
185
187
|
z.object({
|
|
186
|
-
field:
|
|
188
|
+
field: graphqlName("SObject field API name (DateTime/Date field)."),
|
|
187
189
|
function: z
|
|
188
190
|
.enum(GROUP_BY_FUNCTIONS)
|
|
189
191
|
.describe("Date bucketing function from UIAPI GroupByFunction enum."),
|
|
@@ -269,10 +271,10 @@ export const CREATE_INPUT = z.object({
|
|
|
269
271
|
org: z.string().describe("Org alias resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
|
|
270
272
|
object: graphqlName('SObject API name, e.g. "Account", "Order". Must be a valid GraphQL Name.'),
|
|
271
273
|
returnFields: z
|
|
272
|
-
.array(
|
|
274
|
+
.array(dottedGraphqlName("Scalar field API name to read back on the created Record."))
|
|
273
275
|
.optional()
|
|
274
276
|
.describe(
|
|
275
|
-
'Scalar field API names to read back on the created Record. Defaults to ["Id"]. Dot-paths like Owner.Name are
|
|
277
|
+
'Scalar field API names to read back on the created Record. Defaults to ["Id"]. Each entry must be a valid field name (dot-paths allowed); invalid entries are rejected. Dot-paths like Owner.Name are unsupported in mutation results and are skipped with a warning.',
|
|
276
278
|
),
|
|
277
279
|
inputVariable: z
|
|
278
280
|
.string()
|
|
@@ -291,10 +293,10 @@ export const UPDATE_INPUT = z.object({
|
|
|
291
293
|
org: z.string().describe("Org alias resolved via local Salesforce CLI auth (~/.sf, ~/.sfdx)."),
|
|
292
294
|
object: graphqlName('SObject API name, e.g. "Account", "Order". Must be a valid GraphQL Name.'),
|
|
293
295
|
returnFields: z
|
|
294
|
-
.array(
|
|
296
|
+
.array(dottedGraphqlName("Scalar field API name to read back on the updated Record."))
|
|
295
297
|
.optional()
|
|
296
298
|
.describe(
|
|
297
|
-
'Scalar field API names to read back on the updated Record. Defaults to ["Id"]. Dot-paths like Owner.Name are
|
|
299
|
+
'Scalar field API names to read back on the updated Record. Defaults to ["Id"]. Each entry must be a valid field name (dot-paths allowed); invalid entries are rejected. Dot-paths like Owner.Name are unsupported in mutation results and are skipped with a warning.',
|
|
298
300
|
),
|
|
299
301
|
inputVariable: z
|
|
300
302
|
.string()
|