@salesforce/graphiti 10.18.3 → 10.19.1

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 CHANGED
@@ -3,6 +3,16 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [10.19.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v10.19.0...v10.19.1) (2026-06-22)
7
+
8
+ **Note:** Version bump only for package @salesforce/graphiti
9
+
10
+ ## [10.19.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v10.18.3...v10.19.0) (2026-06-19)
11
+
12
+ ### Features
13
+
14
+ - **graphiti:** @W-22818723@ default [@optional](https://github.com/optional) + displayValue on declarative sf*gql*\* tools ([#648](https://github.com/salesforce-experience-platform-emu/webapps/issues/648)) ([4c1199e](https://github.com/salesforce-experience-platform-emu/webapps/commit/4c1199e851eb8308d378b4ee1ac60d5b2d7735ca))
15
+
6
16
  ## [10.18.3](https://github.com/salesforce-experience-platform-emu/webapps/compare/v10.18.2...v10.18.3) (2026-06-19)
7
17
 
8
18
  **Note:** Version bump only for package @salesforce/graphiti
@@ -4,9 +4,18 @@
4
4
  * For full license text, see the LICENSE.txt file
5
5
  */
6
6
  import { generateTypes } from "../lib/codegen.js";
7
+ import { applyGlobalSchemaPolicies } from "../lib/optional-fields.js";
7
8
  import { renderQuery } from "../lib/query-builder.js";
8
9
  import { validateQuery } from "../lib/validator.js";
9
10
  const SCHEMA_LEVEL_ERROR_MARKERS = ["must define one or more fields"];
11
+ // W-22818723: the declarative tools emit `@optional` on selected record fields
12
+ // (those FLS can gate) for graceful degradation. Live UIAPI advertises the
13
+ // directive, but the
14
+ // minimal schemas used in tests (and any introspection that omits directive
15
+ // defs) don't — graphql-js then raises `Unknown directive "@optional".`. That
16
+ // is an artifact of our own default, not a defect in the user's query, so it is
17
+ // filtered out of `warnings[]` exactly like the schema-level markers above.
18
+ const OPTIONAL_DIRECTIVE_UNKNOWN_MARKER = 'Unknown directive "@optional"';
10
19
  /**
11
20
  * Render → validate → codegen → assemble. Shared finalizer for every typed
12
21
  * intent function (`buildList`, `buildDetail`, …).
@@ -26,6 +35,11 @@ const SCHEMA_LEVEL_ERROR_MARKERS = ["must define one or more fields"];
26
35
  * surface non-validator findings (e.g. malformed `$var` placeholders).
27
36
  */
28
37
  export function buildOutput(session, schema, primingNote, extraWarnings = []) {
38
+ // Apply the global schema policies (@optional on FLS-gateable record fields +
39
+ // displayValue where exposed) before render/validate/codegen so all six
40
+ // declarative tools share one policy and the CLI's manual `optional` verb is
41
+ // unaffected (W-22818723).
42
+ applyGlobalSchemaPolicies(session, schema);
29
43
  const query = renderQuery(session);
30
44
  const warnings = primingNote ? [primingNote] : [];
31
45
  try {
@@ -33,6 +47,8 @@ export function buildOutput(session, schema, primingNote, extraWarnings = []) {
33
47
  for (const err of errors) {
34
48
  if (SCHEMA_LEVEL_ERROR_MARKERS.some((m) => err.message.includes(m)))
35
49
  continue;
50
+ if (err.message.includes(OPTIONAL_DIRECTIVE_UNKNOWN_MARKER))
51
+ continue;
36
52
  warnings.push(`Validation: ${err.message}`);
37
53
  }
38
54
  }
@@ -1 +1 @@
1
- {"version":3,"file":"build-output.js","sourceRoot":"","sources":["../../src/intent/build-output.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,MAAM,0BAA0B,GAAG,CAAC,gCAAgC,CAAC,CAAC;AAEtE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,WAAW,CAC1B,OAAqB,EACrB,MAAqB,EACrB,WAAoB,EACpB,gBAA0B,EAAE;IAE5B,MAAM,KAAK,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IACnC,MAAM,QAAQ,GAAa,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAE5D,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC5C,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;YAC1B,IAAI,0BAA0B,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;gBAAE,SAAS;YAC9E,QAAQ,CAAC,IAAI,CAAC,eAAe,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7C,CAAC;IACF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,QAAQ,CAAC,IAAI,CAAC,qCAAqC,OAAO,GAAG,CAAC,CAAC;IAChE,CAAC;IAED,IAAI,KAAa,CAAC;IAClB,IAAI,CAAC;QACJ,KAAK,GAAG,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,KAAK,GAAG,8BAA8B,OAAO,EAAE,CAAC;QAChD,QAAQ,CAAC,IAAI,CAAC,YAAY,OAAO,EAAE,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,SAAS,GAAmB,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC/D,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;KAC9B,CAAC,CAAC,CAAC;IAEJ,QAAQ,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,CAAC;IAEhC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;AAC9C,CAAC"}
1
+ {"version":3,"file":"build-output.js","sourceRoot":"","sources":["../../src/intent/build-output.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,MAAM,0BAA0B,GAAG,CAAC,gCAAgC,CAAC,CAAC;AAEtE,+EAA+E;AAC/E,2EAA2E;AAC3E,qBAAqB;AACrB,4EAA4E;AAC5E,8EAA8E;AAC9E,gFAAgF;AAChF,4EAA4E;AAC5E,MAAM,iCAAiC,GAAG,+BAA+B,CAAC;AAE1E;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,WAAW,CAC1B,OAAqB,EACrB,MAAqB,EACrB,WAAoB,EACpB,gBAA0B,EAAE;IAE5B,8EAA8E;IAC9E,wEAAwE;IACxE,6EAA6E;IAC7E,2BAA2B;IAC3B,yBAAyB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAE3C,MAAM,KAAK,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IACnC,MAAM,QAAQ,GAAa,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAE5D,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC5C,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;YAC1B,IAAI,0BAA0B,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;gBAAE,SAAS;YAC9E,IAAI,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,iCAAiC,CAAC;gBAAE,SAAS;YACtE,QAAQ,CAAC,IAAI,CAAC,eAAe,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7C,CAAC;IACF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,QAAQ,CAAC,IAAI,CAAC,qCAAqC,OAAO,GAAG,CAAC,CAAC;IAChE,CAAC;IAED,IAAI,KAAa,CAAC;IAClB,IAAI,CAAC;QACJ,KAAK,GAAG,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,KAAK,GAAG,8BAA8B,OAAO,EAAE,CAAC;QAChD,QAAQ,CAAC,IAAI,CAAC,YAAY,OAAO,EAAE,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,SAAS,GAAmB,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC/D,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;KAC9B,CAAC,CAAC,CAAC;IAEJ,QAAQ,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,CAAC;IAEhC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;AAC9C,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ /**
7
+ * FLS-safe field selection policy for the declarative MCP tools (W-22818723).
8
+ *
9
+ * Salesforce UIAPI hard-fails an ENTIRE GraphQL query when it selects a field
10
+ * the running user lacks FLS for ("FieldUndefined … Field 'Gold__c'", data:{}).
11
+ * The `@optional` directive is UIAPI's graceful-degradation mechanism: a field
12
+ * marked `@optional` is silently omitted from the response instead of killing
13
+ * the query. Proven at runtime against `Hero__c.Gold__c` in the W-22800643 QA.
14
+ *
15
+ * Decision (W-22818723): the declarative tools default to applying `@optional`
16
+ * to every selected SObject field that FLS can actually gate, and select both
17
+ * `value` and `displayValue` on value-wrapper fields wherever the wrapper
18
+ * exposes a `displayValue`. There is no opt-in flag — degradation-by-default is
19
+ * the safe behavior for an LLM driving queries across multi-user orgs where FLS
20
+ * varies per user. Fields FLS can never hide (`Id`) are exempt — see
21
+ * `FLS_EXEMPT_FIELDS`.
22
+ *
23
+ * This policy runs once on the assembled session inside `buildOutput` (the
24
+ * shared finalizer for `buildList`/`buildDetail`/`buildAggregate`/
25
+ * `buildMutation`/`buildRaw`/`buildDelete`), so all six tools inherit it
26
+ * uniformly. The interactive CLI does NOT route through `buildOutput`, so its
27
+ * explicit `optional` verb keeps full manual control — this default is
28
+ * MCP-surface-only.
29
+ */
30
+ import { type GraphQLSchema } from "graphql";
31
+ import { type QuerySession } from "./session.js";
32
+ /** The UIAPI FLS-degradation directive name (matches the CLI `optional` verb). */
33
+ export declare const OPTIONAL_DIRECTIVE = "optional";
34
+ /**
35
+ * Applies the global declarative-tool schema policies to a fully-assembled
36
+ * session in place — both the FLS-safe `@optional` default and `displayValue`
37
+ * selection on value wrappers:
38
+ * - value-wrapper fields → `@optional` + `value` + `displayValue` (where exposed)
39
+ * - bare record scalars → `@optional` (except `FLS_EXEMPT_FIELDS`, e.g. `Id`)
40
+ * - structural plumbing → untouched (`pageInfo`, `edges`, `node`, cursors,
41
+ * aggregate envelopes, the `value`/`displayValue`
42
+ * leaves themselves)
43
+ *
44
+ * Unresolvable nodes are left as-is — the renderer/validator surfaces a clearer
45
+ * error than this pass could. Never throws.
46
+ */
47
+ export declare function applyGlobalSchemaPolicies(session: QuerySession, schema: GraphQLSchema): void;
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ /**
7
+ * FLS-safe field selection policy for the declarative MCP tools (W-22818723).
8
+ *
9
+ * Salesforce UIAPI hard-fails an ENTIRE GraphQL query when it selects a field
10
+ * the running user lacks FLS for ("FieldUndefined … Field 'Gold__c'", data:{}).
11
+ * The `@optional` directive is UIAPI's graceful-degradation mechanism: a field
12
+ * marked `@optional` is silently omitted from the response instead of killing
13
+ * the query. Proven at runtime against `Hero__c.Gold__c` in the W-22800643 QA.
14
+ *
15
+ * Decision (W-22818723): the declarative tools default to applying `@optional`
16
+ * to every selected SObject field that FLS can actually gate, and select both
17
+ * `value` and `displayValue` on value-wrapper fields wherever the wrapper
18
+ * exposes a `displayValue`. There is no opt-in flag — degradation-by-default is
19
+ * the safe behavior for an LLM driving queries across multi-user orgs where FLS
20
+ * varies per user. Fields FLS can never hide (`Id`) are exempt — see
21
+ * `FLS_EXEMPT_FIELDS`.
22
+ *
23
+ * This policy runs once on the assembled session inside `buildOutput` (the
24
+ * shared finalizer for `buildList`/`buildDetail`/`buildAggregate`/
25
+ * `buildMutation`/`buildRaw`/`buildDelete`), so all six tools inherit it
26
+ * uniformly. The interactive CLI does NOT route through `buildOutput`, so its
27
+ * explicit `optional` verb keeps full manual control — this default is
28
+ * MCP-surface-only.
29
+ */
30
+ import { isInterfaceType, isObjectType } from "graphql";
31
+ import { getChildren, getNodeById, selectLeaf, } from "./session.js";
32
+ import { isValueWrapperType } from "./uiapi.js";
33
+ import { resolvePath } from "./walker.js";
34
+ /** The UIAPI FLS-degradation directive name (matches the CLI `optional` verb). */
35
+ export const OPTIONAL_DIRECTIVE = "optional";
36
+ /** The companion display leaf selected alongside `value` on value wrappers. */
37
+ const DISPLAY_VALUE_FIELD = "displayValue";
38
+ /**
39
+ * Record fields FLS can never hide from a user who can read the record, so
40
+ * `@optional` on them is a guaranteed no-op for degradation. Marking them would
41
+ * only weaken the generated type (`Id?: string | undefined` instead of the
42
+ * always-present `Id: string`), so the policy skips them. `Id` is the one such
43
+ * field today; broader system/audit fields are a deliberate follow-up
44
+ * (see W-22818723 PR #648).
45
+ */
46
+ const FLS_EXEMPT_FIELDS = new Set(["Id"]);
47
+ /**
48
+ * Type-name suffixes / names that are UIAPI *structure* rather than record
49
+ * fields. A bare scalar whose parent resolves to one of these is plumbing
50
+ * (cursor pagination, connection wrappers, aggregate result envelopes), not an
51
+ * FLS-gated SObject field, so it must NOT receive `@optional`.
52
+ */
53
+ function isStructuralScope(schema, typeName) {
54
+ if (!typeName)
55
+ return true;
56
+ if (typeName === "PageInfo")
57
+ return true;
58
+ if (typeName.endsWith("Connection") ||
59
+ typeName.endsWith("Edge") ||
60
+ typeName.endsWith("Aggregate")) {
61
+ return true;
62
+ }
63
+ // Value wrappers are handled by the wrapper branch; their `value` /
64
+ // `displayValue` leaves must not be independently marked.
65
+ if (isValueWrapperType(schema, typeName))
66
+ return true;
67
+ return false;
68
+ }
69
+ /**
70
+ * A "record scope" is an object/interface type that represents a Salesforce
71
+ * record (the `node` inside a connection, a mutation `Record`, a parent
72
+ * relationship object, a polymorphic union member fragment) — i.e. somewhere a
73
+ * directly-selected scalar like `Id` is an FLS-gated field. Structural
74
+ * envelopes (`*Connection` / `*Edge` / `*Aggregate` / `PageInfo`) and value
75
+ * wrappers are NOT record scopes: their children are plumbing (cursors,
76
+ * aggregation functions, `value` / `displayValue` leaves), which FLS does not
77
+ * gate and `@optional` must not touch.
78
+ *
79
+ * Resolving by the PARENT scope — rather than the field's own type — is what
80
+ * keeps the aggregate subtree clean: `count`/`sum`/… resolve to value-wrapper
81
+ * types (`LongValue`, …) but hang off an `*Aggregate` parent, so they are
82
+ * correctly excluded.
83
+ */
84
+ function isRecordScope(session, schema, parent) {
85
+ if (!parent)
86
+ return false;
87
+ let typeName;
88
+ if (parent.kind === "fragment") {
89
+ // `... on User { … }` — the union member type IS the record scope.
90
+ typeName = parent.onType;
91
+ }
92
+ else {
93
+ try {
94
+ typeName = resolvePath(schema, session.operation, parent.schemaPath).typeName;
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ }
100
+ if (isStructuralScope(schema, typeName))
101
+ return false;
102
+ const t = schema.getType(typeName);
103
+ return isObjectType(t) || isInterfaceType(t);
104
+ }
105
+ /** Idempotently attaches the `@optional` directive to a field node. */
106
+ function markOptional(node) {
107
+ if (!node.directives.some((d) => d.name === OPTIONAL_DIRECTIVE)) {
108
+ node.directives.push({ name: OPTIONAL_DIRECTIVE, args: {} });
109
+ }
110
+ }
111
+ /**
112
+ * Selects `displayValue` under a value-wrapper field when (a) the wrapper type
113
+ * actually exposes a `displayValue` field in the schema and (b) it is not
114
+ * already selected. This is the "include display value where it can be
115
+ * included" half of the AC — minimal test schemas whose wrapper is `{ value }`
116
+ * only are left untouched; real UIAPI wrappers (`StringValue`,
117
+ * `PicklistValue`, …) gain it.
118
+ */
119
+ function ensureDisplayValue(session, schema, wrapperNode, wrapperTypeName) {
120
+ const t = schema.getType(wrapperTypeName);
121
+ if (!isObjectType(t) && !isInterfaceType(t))
122
+ return;
123
+ if (!Object.prototype.hasOwnProperty.call(t.getFields(), DISPLAY_VALUE_FIELD))
124
+ return;
125
+ const alreadySelected = getChildren(session, wrapperNode.id).some((c) => c.kind === "field" && c.fieldName === DISPLAY_VALUE_FIELD);
126
+ if (alreadySelected)
127
+ return;
128
+ selectLeaf(session, [...wrapperNode.schemaPath, DISPLAY_VALUE_FIELD]);
129
+ }
130
+ /**
131
+ * Applies the global declarative-tool schema policies to a fully-assembled
132
+ * session in place — both the FLS-safe `@optional` default and `displayValue`
133
+ * selection on value wrappers:
134
+ * - value-wrapper fields → `@optional` + `value` + `displayValue` (where exposed)
135
+ * - bare record scalars → `@optional` (except `FLS_EXEMPT_FIELDS`, e.g. `Id`)
136
+ * - structural plumbing → untouched (`pageInfo`, `edges`, `node`, cursors,
137
+ * aggregate envelopes, the `value`/`displayValue`
138
+ * leaves themselves)
139
+ *
140
+ * Unresolvable nodes are left as-is — the renderer/validator surfaces a clearer
141
+ * error than this pass could. Never throws.
142
+ */
143
+ export function applyGlobalSchemaPolicies(session, schema) {
144
+ // Snapshot field nodes up front: `ensureDisplayValue` appends new leaves to
145
+ // `session.nodes`, and a freshly-added `displayValue` must not be reprocessed.
146
+ const fieldNodes = session.nodes.filter((n) => n.kind === "field");
147
+ for (const node of fieldNodes) {
148
+ let typeName;
149
+ let isLeaf;
150
+ try {
151
+ const wr = resolvePath(schema, session.operation, node.schemaPath);
152
+ typeName = wr.typeName;
153
+ isLeaf = wr.isLeaf;
154
+ }
155
+ catch {
156
+ continue;
157
+ }
158
+ // A field is FLS-gated — and thus a policy target — only when it is a
159
+ // direct field of a record scope. This single gate covers both shapes:
160
+ // value-wrapper fields (`Name { value }`) and bare record scalars
161
+ // (`Id`). It is what excludes the aggregate function-wrappers
162
+ // (`count`/`sum`/…), whose parent is an `*Aggregate` structural scope.
163
+ const parent = getNodeById(session, node.parentId);
164
+ if (!isRecordScope(session, schema, parent))
165
+ continue;
166
+ if (isValueWrapperType(schema, typeName)) {
167
+ markOptional(node);
168
+ ensureDisplayValue(session, schema, node, typeName);
169
+ }
170
+ else if (isLeaf && !FLS_EXEMPT_FIELDS.has(node.fieldName)) {
171
+ // Bare record scalar. Skip fields FLS can never gate (`Id`): marking
172
+ // them is a no-op that would only weaken the generated type.
173
+ markOptional(node);
174
+ }
175
+ }
176
+ }
177
+ //# sourceMappingURL=optional-fields.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optional-fields.js","sourceRoot":"","sources":["../../src/lib/optional-fields.ts"],"names":[],"mappings":"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"}
@@ -174,7 +174,7 @@ export const RAW_INPUT = z.object({
174
174
  " set [<path>] <key>=<value> e.g. set uiapi/query/Case first=10 | set uiapi/query/Case where.Status=New\n" +
175
175
  " var $name <path> [default] e.g. var $id uiapi/query/Case/@args/where/Id/eq (type inferred from path)\n" +
176
176
  'Each command is tokenized on spaces and a value MUST NOT contain a space — quoting does not help once a token has started (key=\'a b\' still splits). A filter value that contains a space (e.g. "New York", "In Progress") cannot be expressed via set in v1; use sf_gql_list with a JSON filter, or pass the value through a variable bound with var.\n' +
177
- "Fails fast: a bad command aborts the whole call. Other CLI verbs (cd, drop, alias, optional, unset) are NOT supported in v1."),
177
+ "Fails fast: a bad command aborts the whole call. Other CLI verbs (cd, drop, alias, optional, unset) are NOT supported in v1 — and the `optional` verb is unnecessary here: like every declarative tool, sf_gql_raw emits all selected record fields with the @optional directive automatically, so a field the running user lacks FLS for is omitted gracefully instead of failing the whole query."),
178
178
  operation: z
179
179
  .enum(["query", "mutation", "aggregate"])
180
180
  .optional()
@@ -1 +1 @@
1
- {"version":3,"file":"input-schemas.js","sourceRoot":"","sources":["../../src/schemas/input-schemas.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"input-schemas.js","sourceRoot":"","sources":["../../src/schemas/input-schemas.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5F,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,+EAA+E;AAE/E,wEAAwE;AACxE,oEAAoE;AACpE,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;AAE3E,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,oEAAoE,CAAC;IAC9F,MAAM,EAAE,WAAW,CAAC,yEAAyE,CAAC;IAC9F,MAAM,EAAE,CAAC;SACP,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACjB,QAAQ,CACR,wFAAwF,CACxF;IACF,YAAY,EAAE,CAAC;SACb,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACjB,QAAQ,EAAE;SACV,QAAQ,CAAC,wDAAwD,CAAC;IACpE,kBAAkB,EAAE,CAAC,CAAC,KAAK,CAAC,uBAAuB,CAAC,iBAAiB,CAAC,CAAC,CAAC,QAAQ,EAAE;IAClF,MAAM,EAAE,CAAC;SACP,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;SACnB,QAAQ,EAAE;SACV,QAAQ,CACR,0FAA0F,CAC1F;IACF,OAAO,EAAE,iBAAiB;SACxB,QAAQ,EAAE;SACV,QAAQ,CACR,wFAAwF,CACxF;IACF,KAAK,EAAE,CAAC;SACN,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,QAAQ,EAAE;SACV,QAAQ,CAAC,iDAAiD,CAAC;IAC7D,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qDAAqD,CAAC;IAC5F,aAAa,EAAE,WAAW,CACzB,gEAAgE,CAChE,CAAC,QAAQ,EAAE;CACZ,CAAC,CAAC;AAEH,+EAA+E;AAE/E,6EAA6E;AAC7E,yEAAyE;AACzE,8EAA8E;AAC9E,kEAAkE;AAClE,MAAM,mBAAmB,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC;AAE9F,+EAA+E;AAC/E,6EAA6E;AAC7E,2DAA2D;AAC3D,EAAE;AACF,2EAA2E;AAC3E,wEAAwE;AACxE,8DAA8D;AAC9D,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC;KAC3B,MAAM,CAAC;IACP,GAAG,EAAE,QAAQ,CAAC,oEAAoE,CAAC;IACnF,MAAM,EAAE,WAAW,CAAC,yEAAyE,CAAC;IAC9F,MAAM,EAAE,CAAC;SACP,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACjB,QAAQ,CACR,wFAAwF,CACxF;IACF,YAAY,EAAE,CAAC;SACb,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACjB,QAAQ,EAAE;SACV,QAAQ,CAAC,wDAAwD,CAAC;IACpE,kBAAkB,EAAE,CAAC,CAAC,KAAK,CAAC,uBAAuB,CAAC,mBAAmB,CAAC,CAAC,CAAC,QAAQ,EAAE;IACpF,UAAU,EAAE,WAAW,CACtB,wQAAwQ,CACxQ,CAAC,QAAQ,EAAE;IACZ,aAAa,EAAE,WAAW,CACzB,kEAAkE,CAClE,CAAC,QAAQ,EAAE;CACZ,CAAC;KACD,MAAM,EAAE,CAAC;AAEX,+EAA+E;AAE/E,uEAAuE;AACvE,6EAA6E;AAC7E,iEAAiE;AACjE,MAAM,qBAAqB,GAAG,uBAAuB,CAAC;AACtD,MAAM,wBAAwB,GAAG,8BAA8B,CAAC;AAChE,mFAAmF;AACnF,MAAM,kBAAkB,GAAG,qBAAqB,CAAC;AAEjD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,GAAG,EAAE,CAAC;SACJ,MAAM,EAAE;SACR,KAAK,CAAC,qBAAqB,EAAE,wCAAwC,CAAC;SACtE,QAAQ,CAAC,oEAAoE,CAAC;IAChF,IAAI,EAAE,CAAC;SACL,IAAI,CAAC,CAAC,cAAc,EAAE,iBAAiB,EAAE,gBAAgB,CAAC,CAAC;SAC3D,QAAQ,CACR,kIAAkI,CAClI;IACF,MAAM,EAAE,CAAC;SACP,MAAM,EAAE;SACR,KAAK,CAAC,wBAAwB,EAAE,kDAAkD,CAAC;SACnF,QAAQ,EAAE;SACV,QAAQ,CAAC,wEAAwE,CAAC;IACpF,KAAK,EAAE,CAAC;SACN,MAAM,EAAE;SACR,KAAK,CAAC,wBAAwB,EAAE,iDAAiD,CAAC;SAClF,QAAQ,EAAE;SACV,QAAQ,CAAC,gDAAgD,CAAC;IAC5D,MAAM,EAAE,CAAC;SACP,MAAM,EAAE;SACR,GAAG,CAAC,GAAG,EAAE,wCAAwC,CAAC;SAClD,KAAK,CAAC,kBAAkB,EAAE,4CAA4C,CAAC;SACvE,QAAQ,EAAE;SACV,QAAQ,CAAC,8DAA8D,CAAC;CAC1E,CAAC,CAAC;AAEH,+EAA+E;AAE/E,MAAM,UAAU,GAAG,CAAC;KAClB,MAAM,EAAE;KACR,QAAQ,EAAE;KACV,QAAQ,CAAC,kDAAkD,CAAC,CAAC;AAE/D,MAAM,iBAAiB,GAAG,CAAC,CAAC,kBAAkB,CAAC,UAAU,EAAE;IAC1D,CAAC,CAAC,MAAM,CAAC;QACR,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;QAC5C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,wDAAwD,CAAC;QAC/F,KAAK,EAAE,UAAU;KACjB,CAAC;IACF,CAAC,CAAC,MAAM,CAAC;QACR,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;QAC9C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,uDAAuD,CAAC;QACnF,KAAK,EAAE,UAAU;KACjB,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC;IACpC,CAAC,CAAC,MAAM,EAAE;IACV,CAAC,CAAC,MAAM,CAAC;QACR,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,+CAA+C,CAAC;QAC3E,QAAQ,EAAE,CAAC;aACT,IAAI,CAAC,kBAAkB,CAAC;aACxB,QAAQ,CAAC,0DAA0D,CAAC;KACtE,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,oEAAoE,CAAC;IAC9F,MAAM,EAAE,WAAW,CAAC,0EAA0E,CAAC;IAC/F,OAAO,EAAE,CAAC;SACR,KAAK,CAAC,oBAAoB,CAAC;SAC3B,QAAQ,EAAE;SACV,QAAQ,CACR,oUAAoU,CACpU;IACF,YAAY,EAAE,CAAC;SACb,KAAK,CAAC,iBAAiB,CAAC;SACxB,QAAQ,EAAE;SACV,QAAQ,CACR,2HAA2H,CAC3H;IACF,MAAM,EAAE,CAAC;SACP,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;SACnB,QAAQ,EAAE;SACV,QAAQ,CACR,iHAAiH,CACjH;IACF,OAAO,EAAE,CAAC;SACR,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;SAC9D,QAAQ,EAAE;SACV,QAAQ,CACR,0JAA0J,CAC1J;IACF,KAAK,EAAE,CAAC;SACN,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,QAAQ,EAAE;SACV,QAAQ,CAAC,0EAA0E,CAAC;IACtF,aAAa,EAAE,WAAW,CACzB,qEAAqE,CACrE,CAAC,QAAQ,EAAE;CACZ,CAAC,CAAC;AAEH,+EAA+E;AAE/E,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC;IACjC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,oEAAoE,CAAC;IAC9F,QAAQ,EAAE,CAAC;SACT,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACjB,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,CACR,gFAAgF;QAC/E,sFAAsF;QACtF,6GAA6G;QAC7G,0GAA0G;QAC1G,2VAA2V;QAC3V,qYAAqY,CACtY;IACF,SAAS,EAAE,CAAC;SACV,IAAI,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;SACxC,QAAQ,EAAE;SACV,QAAQ,CACR,6GAA6G,CAC7G;IACF,QAAQ,EAAE,WAAW,CACpB,kEAAkE,CAClE,CAAC,QAAQ,EAAE;CACZ,CAAC,CAAC;AAEH,+EAA+E;AAE/E,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,oEAAoE,CAAC;IAC9F,MAAM,EAAE,WAAW,CAAC,0EAA0E,CAAC;IAC/F,YAAY,EAAE,CAAC;SACb,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACjB,QAAQ,EAAE;SACV,QAAQ,CACR,2MAA2M,CAC3M;IACF,aAAa,EAAE,CAAC;SACd,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACR,mHAAmH,CACnH;IACF,aAAa,EAAE,WAAW,CACzB,kEAAkE,CAClE,CAAC,QAAQ,EAAE;CACZ,CAAC,CAAC;AAEH,+EAA+E;AAE/E,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,oEAAoE,CAAC;IAC9F,MAAM,EAAE,WAAW,CAAC,0EAA0E,CAAC;IAC/F,YAAY,EAAE,CAAC;SACb,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACjB,QAAQ,EAAE;SACV,QAAQ,CACR,2MAA2M,CAC3M;IACF,aAAa,EAAE,CAAC;SACd,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACR,mHAAmH,CACnH;IACF,aAAa,EAAE,WAAW,CACzB,kEAAkE,CAClE,CAAC,QAAQ,EAAE;CACZ,CAAC,CAAC;AAEH,+EAA+E;AAE/E,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,oEAAoE,CAAC;IAC9F,MAAM,EAAE,WAAW,CAAC,0EAA0E,CAAC;IAC/F,aAAa,EAAE,CAAC;SACd,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACR,sMAAsM,CACtM;IACF,aAAa,EAAE,WAAW,CACzB,kEAAkE,CAClE,CAAC,QAAQ,EAAE;CACZ,CAAC,CAAC;AAEH,+EAA+E;AAE/E,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,GAAG,EAAE,QAAQ,CAAC,gFAAgF,CAAC;IAC/F,YAAY,EAAE,CAAC;SACb,OAAO,EAAE;SACT,QAAQ,EAAE;SACV,QAAQ,CACR,kRAAkR,CAClR;CACF,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/graphiti",
3
- "version": "10.18.3",
3
+ "version": "10.19.1",
4
4
  "description": "Progressive GraphQL query builder CLI for Salesforce orgs",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "type": "module",
@@ -48,8 +48,10 @@ describe("intent/build-delete", () => {
48
48
  expect(result.query).toContain("mutation DeleteAccount");
49
49
  expect(result.query).toContain("$input: RecordDeleteInput!");
50
50
  expect(result.query).toContain("AccountDelete(input: $input)");
51
- // Id is plain ID on the payload — selected directly, never `Id { value }`.
51
+ // Id is plain ID on the payload — selected bare, never `Id { value }`.
52
+ // Id is FLS-exempt, so it carries no @optional directive (W-22818723).
52
53
  expect(result.query).toMatch(/AccountDelete\(input: \$input\)\s*{\s*Id\s*}/);
54
+ expect(result.query).not.toMatch(/\bId\s+@optional\b/);
53
55
  // No `Record { ... }` sub-selection (delete payloads have no Record path).
54
56
  expect(result.query).not.toMatch(/\bRecord\s*{/);
55
57
  expect(result.query).not.toContain("Id { value }");
@@ -162,8 +162,9 @@ describe("intent/build-detail", () => {
162
162
  { org: ORG, object: "Account", fields: ["Id", "Name"] },
163
163
  noopPrimeDeps(),
164
164
  );
165
- expect(out.query).toMatch(/\bId\b/);
166
- expect(out.query).toMatch(/Name\s*\{\s*value\s*\}/);
165
+ // Id is FLS-exempt: selected bare, never @optional (W-22818723).
166
+ expect(out.query).not.toMatch(/\bId\s+@optional\b/);
167
+ expect(out.query).toMatch(/Name\s+@optional\s*\{\s*value\s*\}/);
167
168
  });
168
169
 
169
170
  it("default operationName is <Object>Detail", async () => {
@@ -199,7 +200,7 @@ describe("intent/build-detail", () => {
199
200
  { org: ORG, object: "Case", fields: ["Id"], parentFields: ["Account.Name"] },
200
201
  noopPrimeDeps(),
201
202
  );
202
- expect(out.query).toMatch(/Account\s*\{[^}]*Name\s*\{\s*value\s*\}/s);
203
+ expect(out.query).toMatch(/Account\s*\{[^}]*Name\s+@optional\s*\{\s*value\s*\}/s);
203
204
  });
204
205
 
205
206
  it("parentFields expands polymorphic union into inline fragments (FR-4.3)", async () => {
@@ -207,8 +208,8 @@ describe("intent/build-detail", () => {
207
208
  { org: ORG, object: "Case", fields: ["Id"], parentFields: ["Owner.Name"] },
208
209
  noopPrimeDeps(),
209
210
  );
210
- expect(out.query).toMatch(/\.\.\.\s+on\s+User\s*\{[^}]*Name\s*\{\s*value\s*\}/s);
211
- expect(out.query).toMatch(/\.\.\.\s+on\s+Group\s*\{[^}]*Name\s*\{\s*value\s*\}/s);
211
+ expect(out.query).toMatch(/\.\.\.\s+on\s+User\s*\{[^}]*Name\s+@optional\s*\{\s*value\s*\}/s);
212
+ expect(out.query).toMatch(/\.\.\.\s+on\s+Group\s*\{[^}]*Name\s+@optional\s*\{\s*value\s*\}/s);
212
213
  });
213
214
 
214
215
  it("parentFields skips union members lacking the field (FR-4.3)", async () => {
@@ -239,8 +240,12 @@ describe("intent/build-detail", () => {
239
240
  );
240
241
  expect(out.query).toMatch(/Contacts\s*\([^)]*first:\s*5/);
241
242
  expect(out.query).toMatch(/Contacts\s*\([^)]*orderBy:\s*\{\s*LastName/s);
243
+ // Id inside the child node is FLS-exempt — selected bare, never @optional.
244
+ expect(out.query).not.toMatch(
245
+ /Contacts\s*\([^)]*\)\s*\{\s*edges\s*\{\s*node\s*\{[^}]*Id\s+@optional\b/s,
246
+ );
242
247
  expect(out.query).toMatch(
243
- /Contacts\s*\([^)]*\)\s*\{\s*edges\s*\{\s*node\s*\{[^}]*LastName\s*\{\s*value\s*\}/s,
248
+ /Contacts\s*\([^)]*\)\s*\{\s*edges\s*\{\s*node\s*\{[^}]*LastName\s+@optional\s*\{\s*value\s*\}/s,
244
249
  );
245
250
  });
246
251
 
@@ -99,8 +99,25 @@ describe("intent/build-list", () => {
99
99
  { org: ORG, object: "Account", fields: ["Id", "Name"] },
100
100
  noopPrimeDeps(),
101
101
  );
102
- expect(out.query).toMatch(/\bId\b/);
103
- expect(out.query).toMatch(/Name\s*\{\s*value\s*\}/);
102
+ // Id is FLS-exempt: selected bare, never @optional (W-22818723).
103
+ expect(out.query).not.toMatch(/\bId\s+@optional\b/);
104
+ expect(out.query).toMatch(/Name\s+@optional\s*\{\s*value\s*\}/);
105
+ });
106
+
107
+ it("marks selected record fields @optional for FLS-safe degradation (W-22818723)", async () => {
108
+ const out = await buildList(
109
+ { org: ORG, object: "Account", fields: ["Id", "Name", "Industry"] },
110
+ noopPrimeDeps(),
111
+ );
112
+ // FLS-gateable record fields (fields on `node`) get @optional
113
+ expect(out.query).toMatch(/\bName\s+@optional\s*\{/);
114
+ expect(out.query).toMatch(/\bIndustry\s+@optional\s*\{/);
115
+ // Id is FLS-exempt — selected bare, never @optional
116
+ expect(out.query).not.toMatch(/\bId\s+@optional\b/);
117
+ // Structural plumbing (edges, node, pageInfo, etc.) does NOT get @optional
118
+ expect(out.query).not.toMatch(/edges\s+@optional/);
119
+ expect(out.query).not.toMatch(/node\s+@optional/);
120
+ expect(out.query).not.toMatch(/pageInfo\s+@optional/);
104
121
  });
105
122
 
106
123
  it("default operationName is <Object>List", async () => {
@@ -144,7 +161,7 @@ describe("intent/build-list", () => {
144
161
  { org: ORG, object: "Case", fields: ["Id"], parentFields: ["Account.Name"] },
145
162
  noopPrimeDeps(),
146
163
  );
147
- expect(out.query).toMatch(/Account\s*\{[^}]*Name\s*\{\s*value\s*\}/s);
164
+ expect(out.query).toMatch(/Account\s*\{[^}]*Name\s+@optional\s*\{\s*value\s*\}/s);
148
165
  });
149
166
 
150
167
  it("parentFields expands polymorphic union into inline fragments (FR-4.3)", async () => {
@@ -152,8 +169,8 @@ describe("intent/build-list", () => {
152
169
  { org: ORG, object: "Case", fields: ["Id"], parentFields: ["Owner.Name"] },
153
170
  noopPrimeDeps(),
154
171
  );
155
- expect(out.query).toMatch(/\.\.\.\s+on\s+User\s*\{[^}]*Name\s*\{\s*value\s*\}/s);
156
- expect(out.query).toMatch(/\.\.\.\s+on\s+Group\s*\{[^}]*Name\s*\{\s*value\s*\}/s);
172
+ expect(out.query).toMatch(/\.\.\.\s+on\s+User\s*\{[^}]*Name\s+@optional\s*\{\s*value\s*\}/s);
173
+ expect(out.query).toMatch(/\.\.\.\s+on\s+Group\s*\{[^}]*Name\s+@optional\s*\{\s*value\s*\}/s);
157
174
  });
158
175
 
159
176
  it("parentFields skips union members lacking the field (FR-4.3)", async () => {
@@ -179,7 +196,7 @@ describe("intent/build-list", () => {
179
196
  );
180
197
  expect(out.query).toMatch(/Contacts\s*\([^)]*first:\s*5/);
181
198
  expect(out.query).toMatch(
182
- /Contacts\s*\([^)]*\)\s*\{\s*edges\s*\{\s*node\s*\{[^}]*LastName\s*\{\s*value\s*\}/s,
199
+ /Contacts\s*\([^)]*\)\s*\{\s*edges\s*\{\s*node\s*\{[^}]*LastName\s+@optional\s*\{\s*value\s*\}/s,
183
200
  );
184
201
  });
185
202
 
@@ -52,7 +52,7 @@ describe("intent/build-mutation", () => {
52
52
  expect(result.query).toMatch(/mutation CreateAccount/);
53
53
  expect(result.query).toMatch(/\$input:\s*AccountCreateInput!/);
54
54
  expect(result.query).toMatch(/AccountCreate\(input:\s*\$input\)/);
55
- expect(result.query).toMatch(/Name\s*\{\s*value/s);
55
+ expect(result.query).toMatch(/Name\s+@optional\s*\{\s*value/s);
56
56
  expect(result.variables).toEqual([
57
57
  { name: "input", type: "AccountCreateInput!", required: true },
58
58
  ]);
@@ -67,7 +67,7 @@ describe("intent/build-mutation", () => {
67
67
  expect(result.query).toMatch(/mutation UpdateAccount/);
68
68
  expect(result.query).toMatch(/\$input:\s*AccountUpdateInput!/);
69
69
  expect(result.query).toMatch(/AccountUpdate\(input:\s*\$input\)/);
70
- expect(result.query).toMatch(/Name\s*\{\s*value/s);
70
+ expect(result.query).toMatch(/Name\s+@optional\s*\{\s*value/s);
71
71
  expect(result.variables).toEqual([
72
72
  { name: "input", type: "AccountUpdateInput!", required: true },
73
73
  ]);
@@ -75,10 +75,27 @@ describe("intent/build-mutation", () => {
75
75
 
76
76
  it('op=Update defaults returnFields to ["Id"]', async () => {
77
77
  const result = await buildMutation({ org: ORG, object: "Account" }, "Update", noopPrimeDeps());
78
+ // Id is FLS-exempt: selected bare, never @optional (W-22818723).
78
79
  expect(result.query).toMatch(/\bId\b/);
80
+ expect(result.query).not.toMatch(/\bId\s+@optional\b/);
79
81
  expect(result.query).not.toMatch(/Name\s*\{/);
80
82
  });
81
83
 
84
+ it("returned record fields carry @optional directive for FLS safety", async () => {
85
+ const result = await buildMutation(
86
+ { org: ORG, object: "Account", returnFields: ["Id", "Name"] },
87
+ "Create",
88
+ noopPrimeDeps(),
89
+ );
90
+ // FLS-gateable record fields get @optional; Id is exempt (selected bare)
91
+ expect(result.query).toMatch(/Name\s+@optional\s*\{/);
92
+ expect(result.query).not.toMatch(/\bId\s+@optional\b/);
93
+ // Structural plumbing (Record, uiapi, mutation name) never marked
94
+ expect(result.query).not.toMatch(/Record\s+@optional/);
95
+ expect(result.query).not.toMatch(/uiapi\s+@optional/);
96
+ expect(result.query).not.toMatch(/AccountCreate\s+@optional/);
97
+ });
98
+
82
99
  it("op=Update with custom inputVariable strips leading $ and declares it", async () => {
83
100
  const result = await buildMutation(
84
101
  { org: ORG, object: "Account", inputVariable: "$myInput" },
@@ -63,7 +63,7 @@ describe("intent/build-raw", () => {
63
63
  },
64
64
  deps(),
65
65
  );
66
- expect(out.query).toMatch(/Subject\s*\{\s*value\s*\}/);
66
+ expect(out.query).toMatch(/Subject\s+@optional\s*\{\s*value\s*\}/);
67
67
  expect(out.query).toMatch(/first:\s*5/);
68
68
  expect(out.query).toMatch(/\bquery\b/);
69
69
  });
@@ -7,12 +7,22 @@
7
7
  import { type GraphQLSchema } from "graphql";
8
8
  import { type ToolOutput, type VariableInfo } from "./types.js";
9
9
  import { generateTypes } from "../lib/codegen.js";
10
+ import { applyGlobalSchemaPolicies } from "../lib/optional-fields.js";
10
11
  import { renderQuery } from "../lib/query-builder.js";
11
12
  import { type QuerySession } from "../lib/session.js";
12
13
  import { validateQuery } from "../lib/validator.js";
13
14
 
14
15
  const SCHEMA_LEVEL_ERROR_MARKERS = ["must define one or more fields"];
15
16
 
17
+ // W-22818723: the declarative tools emit `@optional` on selected record fields
18
+ // (those FLS can gate) for graceful degradation. Live UIAPI advertises the
19
+ // directive, but the
20
+ // minimal schemas used in tests (and any introspection that omits directive
21
+ // defs) don't — graphql-js then raises `Unknown directive "@optional".`. That
22
+ // is an artifact of our own default, not a defect in the user's query, so it is
23
+ // filtered out of `warnings[]` exactly like the schema-level markers above.
24
+ const OPTIONAL_DIRECTIVE_UNKNOWN_MARKER = 'Unknown directive "@optional"';
25
+
16
26
  /**
17
27
  * Render → validate → codegen → assemble. Shared finalizer for every typed
18
28
  * intent function (`buildList`, `buildDetail`, …).
@@ -37,6 +47,12 @@ export function buildOutput(
37
47
  primingNote?: string,
38
48
  extraWarnings: string[] = [],
39
49
  ): ToolOutput {
50
+ // Apply the global schema policies (@optional on FLS-gateable record fields +
51
+ // displayValue where exposed) before render/validate/codegen so all six
52
+ // declarative tools share one policy and the CLI's manual `optional` verb is
53
+ // unaffected (W-22818723).
54
+ applyGlobalSchemaPolicies(session, schema);
55
+
40
56
  const query = renderQuery(session);
41
57
  const warnings: string[] = primingNote ? [primingNote] : [];
42
58
 
@@ -44,6 +60,7 @@ export function buildOutput(
44
60
  const errors = validateQuery(schema, query);
45
61
  for (const err of errors) {
46
62
  if (SCHEMA_LEVEL_ERROR_MARKERS.some((m) => err.message.includes(m))) continue;
63
+ if (err.message.includes(OPTIONAL_DIRECTIVE_UNKNOWN_MARKER)) continue;
47
64
  warnings.push(`Validation: ${err.message}`);
48
65
  }
49
66
  } catch (err) {
@@ -0,0 +1,202 @@
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
+ * W-22818723 — FLS-safe `@optional` field-selection policy.
9
+ *
10
+ * The sibling intent/mcp specs assert `@optional` against MINIMAL schemas whose
11
+ * value wrapper is `{ value }` only and which never declare the directive. This
12
+ * spec exercises the policy against a REALISTIC UIAPI-shaped schema — one that
13
+ * (a) declares `directive @optional` (so validation passes cleanly, as the live
14
+ * org does) and (b) exposes `displayValue`/`label` on its wrappers — to cover
15
+ * the two behaviors those minimal schemas structurally cannot:
16
+ * 1. `displayValue` IS selected when the wrapper exposes it.
17
+ * 2. `@optional` produces NO validation warning when the schema declares it.
18
+ *
19
+ * It also reproduces the headline FLS scenario (the `Gold__c`-style restricted
20
+ * field from the W-22800643 QA): a query that would hard-fail for a restricted
21
+ * user is rendered with `@optional` so the field degrades gracefully instead.
22
+ */
23
+
24
+ import { buildSchema } from "graphql";
25
+ import { describe, expect, it } from "vitest";
26
+ import { makeNoopPrimeDeps } from "../../__tests__/helpers/prime-deps.js";
27
+ import { buildAggregate } from "../../intent/build-aggregate.js";
28
+ import { buildList } from "../../intent/build-list.js";
29
+ import { applyGlobalSchemaPolicies, OPTIONAL_DIRECTIVE } from "../optional-fields.js";
30
+ import { renderQuery } from "../query-builder.js";
31
+ import { createSession, getChildren, selectLeaf } from "../session.js";
32
+ import { primeSchemaCache } from "../walker.js";
33
+
34
+ // A UIAPI-faithful schema: declares @optional, and wrappers carry value +
35
+ // displayValue (StringValue/PicklistValue) so the "display value where it can
36
+ // be included" branch is reachable. `Hero__c` mirrors the W-22800643 fixture.
37
+ const SCHEMA_SDL = `
38
+ directive @optional on FIELD
39
+
40
+ type Query { uiapi: UIAPI! }
41
+ type UIAPI { query: RecordQuery!, aggregate: RecordQueryAggregate! }
42
+
43
+ type RecordQuery {
44
+ Hero__c(first: Int, after: String, where: Hero__c_Filter, orderBy: Hero__c_OrderBy): Hero__cConnection!
45
+ }
46
+ type RecordQueryAggregate {
47
+ Hero__c(first: Int, after: String, groupBy: Hero__c_GroupBy): Hero__cAggregateConnection
48
+ }
49
+
50
+ input Hero__c_Filter { Gold__c: IntOperators }
51
+ input Hero__c_OrderBy { Name: OrderByClause }
52
+ input Hero__c_GroupBy { Gold__c: GroupByClause }
53
+ input IntOperators { eq: Int, gt: Int }
54
+ input OrderByClause { order: Order! }
55
+ input GroupByClause { group: Boolean }
56
+ enum Order { ASC DESC }
57
+
58
+ type Hero__cConnection { edges: [Hero__cEdge!]!, pageInfo: PageInfo! }
59
+ type Hero__cEdge { node: Hero__c! }
60
+ type PageInfo { hasNextPage: Boolean!, endCursor: String }
61
+
62
+ type Hero__c {
63
+ Id: ID!
64
+ ApiName: String
65
+ Name: StringValue
66
+ Gold__c: IntValue
67
+ }
68
+
69
+ type StringValue { value: String, displayValue: String, label: String }
70
+ type IntValue { value: Int, displayValue: String }
71
+ type LongValue { value: String }
72
+
73
+ type Hero__cAggregateConnection { edges: [Hero__cAggregateEdge!]!, pageInfo: PageInfo! }
74
+ type Hero__cAggregateEdge { node: Hero__cResult!, cursor: String! }
75
+ type Hero__cResult { aggregate: Hero__cAggregate }
76
+ type Hero__cAggregate { Id: IDAggregate, Gold__c: IntAggregate }
77
+ type IDAggregate { value: ID, count: LongValue }
78
+ type IntAggregate { value: Int, sum: LongValue, count: LongValue }
79
+ `;
80
+
81
+ const SCHEMA = buildSchema(SCHEMA_SDL);
82
+ const ORG = "hero-org";
83
+ const ORG_URL = "https://hero-org.my.salesforce.com";
84
+ primeSchemaCache(ORG, SCHEMA);
85
+ primeSchemaCache(ORG_URL, SCHEMA);
86
+ const noopPrimeDeps = () => makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
87
+
88
+ describe("lib/optional-fields — applyGlobalSchemaPolicies", () => {
89
+ it("marks bare record scalars @optional", async () => {
90
+ // ApiName is a bare scalar (not a value wrapper) on a record scope, so it
91
+ // gets @optional like any other FLS-gateable field.
92
+ const out = await buildList(
93
+ { org: ORG, object: "Hero__c", fields: ["ApiName"] },
94
+ noopPrimeDeps(),
95
+ );
96
+ expect(out.query).toMatch(/\bApiName\s+@optional\b/);
97
+ });
98
+
99
+ it("does NOT mark Id @optional — FLS can never gate it (W-22818723)", async () => {
100
+ // `@optional` on Id is a no-op for degradation and only weakens the type
101
+ // (`Id?: string | undefined`), so Id is exempt while its siblings are not.
102
+ const out = await buildList(
103
+ { org: ORG, object: "Hero__c", fields: ["Id", "Name"] },
104
+ noopPrimeDeps(),
105
+ );
106
+ expect(out.query).not.toMatch(/\bId\s+@optional\b/);
107
+ expect(out.query).toMatch(/\bId\b/); // still selected, just bare
108
+ expect(out.query).toMatch(/Name\s+@optional\s*\{/);
109
+ });
110
+
111
+ it("marks value-wrapper fields @optional and selects value + displayValue", async () => {
112
+ const out = await buildList({ org: ORG, object: "Hero__c", fields: ["Name"] }, noopPrimeDeps());
113
+ // @optional sits on the wrapper field, between the name and the body.
114
+ expect(out.query).toMatch(/Name\s+@optional\s*\{/);
115
+ // Both value AND displayValue are selected because the wrapper exposes them.
116
+ expect(out.query).toMatch(/Name\s+@optional\s*\{[^}]*\bvalue\b[^}]*\bdisplayValue\b[^}]*\}/s);
117
+ });
118
+
119
+ it("emits NO validation warning when the schema declares directive @optional", async () => {
120
+ const out = await buildList(
121
+ { org: ORG, object: "Hero__c", fields: ["Id", "Name", "Gold__c"] },
122
+ noopPrimeDeps(),
123
+ );
124
+ expect(out.warnings).toEqual([]);
125
+ });
126
+
127
+ it("codegen reflects @optional fields as optional `?: T | undefined` and includes displayValue (AC)", async () => {
128
+ const out = await buildList(
129
+ { org: ORG, object: "Hero__c", fields: ["Id", "Name", "Gold__c"] },
130
+ noopPrimeDeps(),
131
+ );
132
+ // @optional fields become optional TS properties unioned with undefined.
133
+ expect(out.types).toMatch(/\|\s*undefined/);
134
+ // The display value rides along on the wrapper.
135
+ expect(out.types).toMatch(/displayValue/);
136
+ });
137
+
138
+ it("FLS scenario: an FLS-restricted field (Gold__c) renders @optional so the query degrades gracefully", async () => {
139
+ // Per W-22800643: Gold__c WITHOUT @optional hard-fails the whole query for
140
+ // a restricted user; WITH @optional the field is omitted and the rest
141
+ // succeeds. The tool must emit the directive by default.
142
+ const out = await buildList(
143
+ { org: ORG, object: "Hero__c", fields: ["Id", "Gold__c"] },
144
+ noopPrimeDeps(),
145
+ );
146
+ expect(out.query).toMatch(/Gold__c\s+@optional\s*\{[^}]*\bvalue\b/s);
147
+ expect(out.warnings).toEqual([]);
148
+ });
149
+
150
+ it("does NOT mark structural plumbing (pageInfo / cursors)", async () => {
151
+ const out = await buildList({ org: ORG, object: "Hero__c", fields: ["Id"] }, noopPrimeDeps());
152
+ // pageInfo and its cursor leaves carry no @optional.
153
+ expect(out.query).not.toMatch(/pageInfo\s+@optional/);
154
+ expect(out.query).not.toMatch(/hasNextPage\s+@optional/);
155
+ expect(out.query).not.toMatch(/endCursor\s+@optional/);
156
+ });
157
+
158
+ it("does NOT mark aggregate function selections @optional", async () => {
159
+ const out = await buildAggregate(
160
+ {
161
+ org: ORG,
162
+ object: "Hero__c",
163
+ aggregations: [{ function: "sum", field: "Gold__c" }],
164
+ },
165
+ noopPrimeDeps(),
166
+ );
167
+ // The aggregation function leaf (sum { value }) is structural, not an
168
+ // FLS-gated record field — it must stay clean.
169
+ expect(out.query).not.toMatch(/sum\s+@optional/);
170
+ expect(out.query).not.toMatch(/count\s+@optional/);
171
+ });
172
+
173
+ it("is idempotent — running the policy twice does not double-mark", () => {
174
+ const session = createSession(ORG, "query", ORG_URL);
175
+ // Use a non-exempt bare scalar (ApiName); Id would never be marked at all.
176
+ selectLeaf(session, ["uiapi", "query", "Hero__c", "edges", "node", "ApiName"]);
177
+ applyGlobalSchemaPolicies(session, SCHEMA);
178
+ applyGlobalSchemaPolicies(session, SCHEMA);
179
+ const apiNameNode = session.nodes.find((n) => n.kind === "field" && n.fieldName === "ApiName");
180
+ const optionalDirectives =
181
+ apiNameNode?.kind === "field"
182
+ ? apiNameNode.directives.filter((d) => d.name === OPTIONAL_DIRECTIVE)
183
+ : [];
184
+ expect(optionalDirectives).toHaveLength(1);
185
+ });
186
+
187
+ it("does not duplicate displayValue when it is already selected", () => {
188
+ const session = createSession(ORG, "query", ORG_URL);
189
+ const base = ["uiapi", "query", "Hero__c", "edges", "node", "Name"];
190
+ selectLeaf(session, [...base, "value"]);
191
+ selectLeaf(session, [...base, "displayValue"]);
192
+ applyGlobalSchemaPolicies(session, SCHEMA);
193
+ const nameNode = session.nodes.find((n) => n.kind === "field" && n.fieldName === "Name");
194
+ const displayValueChildren = nameNode
195
+ ? getChildren(session, nameNode.id).filter(
196
+ (c) => c.kind === "field" && c.fieldName === "displayValue",
197
+ )
198
+ : [];
199
+ expect(displayValueChildren).toHaveLength(1);
200
+ expect(renderQuery(session)).toMatch(/Name\s+@optional\s*\{/);
201
+ });
202
+ });
@@ -0,0 +1,198 @@
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
+ * FLS-safe field selection policy for the declarative MCP tools (W-22818723).
9
+ *
10
+ * Salesforce UIAPI hard-fails an ENTIRE GraphQL query when it selects a field
11
+ * the running user lacks FLS for ("FieldUndefined … Field 'Gold__c'", data:{}).
12
+ * The `@optional` directive is UIAPI's graceful-degradation mechanism: a field
13
+ * marked `@optional` is silently omitted from the response instead of killing
14
+ * the query. Proven at runtime against `Hero__c.Gold__c` in the W-22800643 QA.
15
+ *
16
+ * Decision (W-22818723): the declarative tools default to applying `@optional`
17
+ * to every selected SObject field that FLS can actually gate, and select both
18
+ * `value` and `displayValue` on value-wrapper fields wherever the wrapper
19
+ * exposes a `displayValue`. There is no opt-in flag — degradation-by-default is
20
+ * the safe behavior for an LLM driving queries across multi-user orgs where FLS
21
+ * varies per user. Fields FLS can never hide (`Id`) are exempt — see
22
+ * `FLS_EXEMPT_FIELDS`.
23
+ *
24
+ * This policy runs once on the assembled session inside `buildOutput` (the
25
+ * shared finalizer for `buildList`/`buildDetail`/`buildAggregate`/
26
+ * `buildMutation`/`buildRaw`/`buildDelete`), so all six tools inherit it
27
+ * uniformly. The interactive CLI does NOT route through `buildOutput`, so its
28
+ * explicit `optional` verb keeps full manual control — this default is
29
+ * MCP-surface-only.
30
+ */
31
+
32
+ import { isInterfaceType, isObjectType, type GraphQLSchema } from "graphql";
33
+ import {
34
+ type FieldProjectionNode,
35
+ type ProjectionNode,
36
+ type QuerySession,
37
+ getChildren,
38
+ getNodeById,
39
+ selectLeaf,
40
+ } from "./session.js";
41
+ import { isValueWrapperType } from "./uiapi.js";
42
+ import { resolvePath } from "./walker.js";
43
+
44
+ /** The UIAPI FLS-degradation directive name (matches the CLI `optional` verb). */
45
+ export const OPTIONAL_DIRECTIVE = "optional";
46
+
47
+ /** The companion display leaf selected alongside `value` on value wrappers. */
48
+ const DISPLAY_VALUE_FIELD = "displayValue";
49
+
50
+ /**
51
+ * Record fields FLS can never hide from a user who can read the record, so
52
+ * `@optional` on them is a guaranteed no-op for degradation. Marking them would
53
+ * only weaken the generated type (`Id?: string | undefined` instead of the
54
+ * always-present `Id: string`), so the policy skips them. `Id` is the one such
55
+ * field today; broader system/audit fields are a deliberate follow-up
56
+ * (see W-22818723 PR #648).
57
+ */
58
+ const FLS_EXEMPT_FIELDS = new Set<string>(["Id"]);
59
+
60
+ /**
61
+ * Type-name suffixes / names that are UIAPI *structure* rather than record
62
+ * fields. A bare scalar whose parent resolves to one of these is plumbing
63
+ * (cursor pagination, connection wrappers, aggregate result envelopes), not an
64
+ * FLS-gated SObject field, so it must NOT receive `@optional`.
65
+ */
66
+ function isStructuralScope(schema: GraphQLSchema, typeName: string): boolean {
67
+ if (!typeName) return true;
68
+ if (typeName === "PageInfo") return true;
69
+ if (
70
+ typeName.endsWith("Connection") ||
71
+ typeName.endsWith("Edge") ||
72
+ typeName.endsWith("Aggregate")
73
+ ) {
74
+ return true;
75
+ }
76
+ // Value wrappers are handled by the wrapper branch; their `value` /
77
+ // `displayValue` leaves must not be independently marked.
78
+ if (isValueWrapperType(schema, typeName)) return true;
79
+ return false;
80
+ }
81
+
82
+ /**
83
+ * A "record scope" is an object/interface type that represents a Salesforce
84
+ * record (the `node` inside a connection, a mutation `Record`, a parent
85
+ * relationship object, a polymorphic union member fragment) — i.e. somewhere a
86
+ * directly-selected scalar like `Id` is an FLS-gated field. Structural
87
+ * envelopes (`*Connection` / `*Edge` / `*Aggregate` / `PageInfo`) and value
88
+ * wrappers are NOT record scopes: their children are plumbing (cursors,
89
+ * aggregation functions, `value` / `displayValue` leaves), which FLS does not
90
+ * gate and `@optional` must not touch.
91
+ *
92
+ * Resolving by the PARENT scope — rather than the field's own type — is what
93
+ * keeps the aggregate subtree clean: `count`/`sum`/… resolve to value-wrapper
94
+ * types (`LongValue`, …) but hang off an `*Aggregate` parent, so they are
95
+ * correctly excluded.
96
+ */
97
+ function isRecordScope(
98
+ session: QuerySession,
99
+ schema: GraphQLSchema,
100
+ parent: ProjectionNode | null,
101
+ ): boolean {
102
+ if (!parent) return false;
103
+ let typeName: string;
104
+ if (parent.kind === "fragment") {
105
+ // `... on User { … }` — the union member type IS the record scope.
106
+ typeName = parent.onType;
107
+ } else {
108
+ try {
109
+ typeName = resolvePath(schema, session.operation, parent.schemaPath).typeName;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+ if (isStructuralScope(schema, typeName)) return false;
115
+ const t = schema.getType(typeName);
116
+ return isObjectType(t) || isInterfaceType(t);
117
+ }
118
+
119
+ /** Idempotently attaches the `@optional` directive to a field node. */
120
+ function markOptional(node: FieldProjectionNode): void {
121
+ if (!node.directives.some((d) => d.name === OPTIONAL_DIRECTIVE)) {
122
+ node.directives.push({ name: OPTIONAL_DIRECTIVE, args: {} });
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Selects `displayValue` under a value-wrapper field when (a) the wrapper type
128
+ * actually exposes a `displayValue` field in the schema and (b) it is not
129
+ * already selected. This is the "include display value where it can be
130
+ * included" half of the AC — minimal test schemas whose wrapper is `{ value }`
131
+ * only are left untouched; real UIAPI wrappers (`StringValue`,
132
+ * `PicklistValue`, …) gain it.
133
+ */
134
+ function ensureDisplayValue(
135
+ session: QuerySession,
136
+ schema: GraphQLSchema,
137
+ wrapperNode: FieldProjectionNode,
138
+ wrapperTypeName: string,
139
+ ): void {
140
+ const t = schema.getType(wrapperTypeName);
141
+ if (!isObjectType(t) && !isInterfaceType(t)) return;
142
+ if (!Object.prototype.hasOwnProperty.call(t.getFields(), DISPLAY_VALUE_FIELD)) return;
143
+
144
+ const alreadySelected = getChildren(session, wrapperNode.id).some(
145
+ (c) => c.kind === "field" && c.fieldName === DISPLAY_VALUE_FIELD,
146
+ );
147
+ if (alreadySelected) return;
148
+
149
+ selectLeaf(session, [...wrapperNode.schemaPath, DISPLAY_VALUE_FIELD]);
150
+ }
151
+
152
+ /**
153
+ * Applies the global declarative-tool schema policies to a fully-assembled
154
+ * session in place — both the FLS-safe `@optional` default and `displayValue`
155
+ * selection on value wrappers:
156
+ * - value-wrapper fields → `@optional` + `value` + `displayValue` (where exposed)
157
+ * - bare record scalars → `@optional` (except `FLS_EXEMPT_FIELDS`, e.g. `Id`)
158
+ * - structural plumbing → untouched (`pageInfo`, `edges`, `node`, cursors,
159
+ * aggregate envelopes, the `value`/`displayValue`
160
+ * leaves themselves)
161
+ *
162
+ * Unresolvable nodes are left as-is — the renderer/validator surfaces a clearer
163
+ * error than this pass could. Never throws.
164
+ */
165
+ export function applyGlobalSchemaPolicies(session: QuerySession, schema: GraphQLSchema): void {
166
+ // Snapshot field nodes up front: `ensureDisplayValue` appends new leaves to
167
+ // `session.nodes`, and a freshly-added `displayValue` must not be reprocessed.
168
+ const fieldNodes = session.nodes.filter((n): n is FieldProjectionNode => n.kind === "field");
169
+
170
+ for (const node of fieldNodes) {
171
+ let typeName: string;
172
+ let isLeaf: boolean;
173
+ try {
174
+ const wr = resolvePath(schema, session.operation, node.schemaPath);
175
+ typeName = wr.typeName;
176
+ isLeaf = wr.isLeaf;
177
+ } catch {
178
+ continue;
179
+ }
180
+
181
+ // A field is FLS-gated — and thus a policy target — only when it is a
182
+ // direct field of a record scope. This single gate covers both shapes:
183
+ // value-wrapper fields (`Name { value }`) and bare record scalars
184
+ // (`Id`). It is what excludes the aggregate function-wrappers
185
+ // (`count`/`sum`/…), whose parent is an `*Aggregate` structural scope.
186
+ const parent = getNodeById(session, node.parentId);
187
+ if (!isRecordScope(session, schema, parent)) continue;
188
+
189
+ if (isValueWrapperType(schema, typeName)) {
190
+ markOptional(node);
191
+ ensureDisplayValue(session, schema, node, typeName);
192
+ } else if (isLeaf && !FLS_EXEMPT_FIELDS.has(node.fieldName)) {
193
+ // Bare record scalar. Skip fields FLS can never gate (`Id`): marking
194
+ // them is a no-op that would only weaken the generated type.
195
+ markOptional(node);
196
+ }
197
+ }
198
+ }
@@ -115,8 +115,9 @@ describe("mcp/tools/sf-gql-create", () => {
115
115
  expect(parsed.query).toMatch(/mutation CreateAccount/);
116
116
  expect(parsed.query).toMatch(/\$input:\s*AccountCreateInput!/);
117
117
  expect(parsed.query).toMatch(/AccountCreate\(input:\s*\$input\)/);
118
- expect(parsed.query).toMatch(/Name\s*\{\s*value/s);
119
- expect(parsed.query).toMatch(/\bId\b/);
118
+ expect(parsed.query).toMatch(/Name\s+@optional\s*\{\s*value/s);
119
+ // Id is FLS-exempt: selected bare, never @optional (W-22818723).
120
+ expect(parsed.query).not.toMatch(/\bId\s+@optional\b/);
120
121
  expect(parsed.variables).toHaveLength(1);
121
122
  expect(parsed.variables[0]).toEqual({
122
123
  name: "input",
@@ -139,7 +140,9 @@ describe("mcp/tools/sf-gql-create", () => {
139
140
  });
140
141
  const content = result.content as { type: string; text?: string }[];
141
142
  const parsed = JSON.parse(content[0]?.text ?? "{}");
143
+ // Id is FLS-exempt: selected bare, never @optional (W-22818723).
142
144
  expect(parsed.query).toMatch(/\bId\b/);
145
+ expect(parsed.query).not.toMatch(/\bId\s+@optional\b/);
143
146
  expect(parsed.query).not.toMatch(/Name\s*\{/);
144
147
  } finally {
145
148
  await client.close();
@@ -119,7 +119,7 @@ describe("mcp/tools/sf-gql-detail", () => {
119
119
  warnings: string[];
120
120
  };
121
121
  expect(parsed.query).toMatch(/\bAccountDetail\b/);
122
- expect(parsed.query).toMatch(/Name\s*\{\s*value\s*\}/);
122
+ expect(parsed.query).toMatch(/Name\s+@optional\s*\{\s*value\s*\}/);
123
123
  expect(parsed.query).toMatch(/first\s*:\s*1\b/);
124
124
  const id = parsed.variables.find((v) => v.name === "id");
125
125
  expect(id).toBeDefined();
@@ -106,7 +106,7 @@ describe("mcp/tools/sf-gql-list", () => {
106
106
  warnings: string[];
107
107
  };
108
108
  expect(parsed.query).toMatch(/\bAccountList\b/);
109
- expect(parsed.query).toMatch(/Name\s*\{\s*value\s*\}/);
109
+ expect(parsed.query).toMatch(/Name\s+@optional\s*\{\s*value\s*\}/);
110
110
  expect(Array.isArray(parsed.variables)).toBe(true);
111
111
  expect(typeof parsed.types).toBe("string");
112
112
  } finally {
@@ -102,7 +102,7 @@ describe("mcp/tools/sf-gql-raw", () => {
102
102
  expect(result.isError).toBeFalsy();
103
103
  const content = result.content as { type: string; text?: string }[];
104
104
  const parsed = JSON.parse(content[0]?.text ?? "{}") as { query: string };
105
- expect(parsed.query).toMatch(/Subject\s*\{\s*value\s*\}/);
105
+ expect(parsed.query).toMatch(/Subject\s+@optional\s*\{\s*value\s*\}/);
106
106
  } finally {
107
107
  await client.close();
108
108
  await server.close();
@@ -87,8 +87,9 @@ describe("mcp/tools/sf-gql-update", () => {
87
87
  expect(parsed.query).toMatch(/mutation UpdateAccount/);
88
88
  expect(parsed.query).toMatch(/\$input:\s*AccountUpdateInput!/);
89
89
  expect(parsed.query).toMatch(/AccountUpdate\(input:\s*\$input\)/);
90
- expect(parsed.query).toMatch(/Name\s*\{\s*value/s);
91
- expect(parsed.query).toMatch(/\bId\b/);
90
+ expect(parsed.query).toMatch(/Name\s+@optional\s*\{\s*value/s);
91
+ // Id is FLS-exempt: selected bare, never @optional (W-22818723).
92
+ expect(parsed.query).not.toMatch(/\bId\s+@optional\b/);
92
93
  expect(parsed.variables).toHaveLength(1);
93
94
  expect(parsed.variables[0]).toEqual({
94
95
  name: "input",
@@ -111,7 +112,9 @@ describe("mcp/tools/sf-gql-update", () => {
111
112
  });
112
113
  const content = result.content as { type: string; text?: string }[];
113
114
  const parsed = JSON.parse(content[0]?.text ?? "{}");
115
+ // Id is FLS-exempt: selected bare, never @optional (W-22818723).
114
116
  expect(parsed.query).toMatch(/\bId\b/);
117
+ expect(parsed.query).not.toMatch(/\bId\s+@optional\b/);
115
118
  expect(parsed.query).not.toMatch(/Name\s*\{/);
116
119
  } finally {
117
120
  await client.close();
@@ -219,7 +219,7 @@ export const RAW_INPUT = z.object({
219
219
  " set [<path>] <key>=<value> e.g. set uiapi/query/Case first=10 | set uiapi/query/Case where.Status=New\n" +
220
220
  " var $name <path> [default] e.g. var $id uiapi/query/Case/@args/where/Id/eq (type inferred from path)\n" +
221
221
  'Each command is tokenized on spaces and a value MUST NOT contain a space — quoting does not help once a token has started (key=\'a b\' still splits). A filter value that contains a space (e.g. "New York", "In Progress") cannot be expressed via set in v1; use sf_gql_list with a JSON filter, or pass the value through a variable bound with var.\n' +
222
- "Fails fast: a bad command aborts the whole call. Other CLI verbs (cd, drop, alias, optional, unset) are NOT supported in v1.",
222
+ "Fails fast: a bad command aborts the whole call. Other CLI verbs (cd, drop, alias, optional, unset) are NOT supported in v1 — and the `optional` verb is unnecessary here: like every declarative tool, sf_gql_raw emits all selected record fields with the @optional directive automatically, so a field the running user lacks FLS for is omitted gracefully instead of failing the whole query.",
223
223
  ),
224
224
  operation: z
225
225
  .enum(["query", "mutation", "aggregate"])