@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/intent/build-detail.js +3 -1
  3. package/dist/intent/build-detail.js.map +1 -1
  4. package/dist/intent/build-list.js +8 -1
  5. package/dist/intent/build-list.js.map +1 -1
  6. package/dist/intent/build-mutation.js +6 -1
  7. package/dist/intent/build-mutation.js.map +1 -1
  8. package/dist/intent/select-child-relationship.js +9 -0
  9. package/dist/intent/select-child-relationship.js.map +1 -1
  10. package/dist/lib/graphql-name.d.ts +18 -0
  11. package/dist/lib/graphql-name.js +22 -0
  12. package/dist/lib/graphql-name.js.map +1 -1
  13. package/dist/lib/variable-promotion.js +10 -0
  14. package/dist/lib/variable-promotion.js.map +1 -1
  15. package/dist/schemas/fields.d.ts +10 -0
  16. package/dist/schemas/fields.js +21 -2
  17. package/dist/schemas/fields.js.map +1 -1
  18. package/dist/schemas/input-schemas.js +14 -14
  19. package/dist/schemas/input-schemas.js.map +1 -1
  20. package/package.json +1 -1
  21. package/src/intent/__tests__/build-aggregate.spec.ts +29 -0
  22. package/src/intent/__tests__/build-create-validation.spec.ts +37 -0
  23. package/src/intent/__tests__/build-detail.spec.ts +96 -0
  24. package/src/intent/__tests__/build-list.spec.ts +162 -1
  25. package/src/intent/__tests__/build-update-validation.spec.ts +20 -0
  26. package/src/intent/build-detail.ts +3 -1
  27. package/src/intent/build-list.ts +8 -1
  28. package/src/intent/build-mutation.ts +6 -1
  29. package/src/intent/select-child-relationship.ts +10 -0
  30. package/src/lib/__tests__/graphql-name.spec.ts +72 -7
  31. package/src/lib/graphql-name.ts +26 -0
  32. package/src/lib/variable-promotion.ts +12 -0
  33. package/src/mcp/tools/__tests__/error-surface.contract.spec.ts +23 -13
  34. package/src/schemas/__tests__/input-schemas.spec.ts +153 -0
  35. package/src/schemas/fields.ts +29 -2
  36. 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
+ });
@@ -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(z.string()),
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(z.string())
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(z.string())
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: z.string().optional().describe('Scope enum (e.g. "MINE", "EVERYTHING") or $varName.'),
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(z.string())
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(z.string())
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: z.string().optional().describe('SObject field API name. Defaults to "Id" when omitted.'),
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: z.string().describe("SObject field API name. Required for sum/avg/min/max."),
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
- z.string(),
186
+ graphqlName("SObject field API name to group by (flat, non-dotted)."),
185
187
  z.object({
186
- field: z.string().describe("SObject field API name (DateTime/Date 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(z.string())
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 not supported in mutation results; passing them produces a warning and the field is skipped.',
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(z.string())
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 not supported in mutation results; passing them produces a warning and the field is skipped.',
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()