@salesforce/graphiti 10.18.3 → 10.19.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-output.js +16 -0
- package/dist/intent/build-output.js.map +1 -1
- package/dist/lib/optional-fields.d.ts +47 -0
- package/dist/lib/optional-fields.js +177 -0
- package/dist/lib/optional-fields.js.map +1 -0
- package/dist/schemas/input-schemas.js +1 -1
- package/dist/schemas/input-schemas.js.map +1 -1
- package/package.json +1 -1
- package/src/intent/__tests__/build-delete.spec.ts +3 -1
- package/src/intent/__tests__/build-detail.spec.ts +11 -6
- package/src/intent/__tests__/build-list.spec.ts +23 -6
- package/src/intent/__tests__/build-mutation.spec.ts +19 -2
- package/src/intent/__tests__/build-raw.spec.ts +1 -1
- package/src/intent/build-output.ts +17 -0
- package/src/lib/__tests__/optional-fields.spec.ts +202 -0
- package/src/lib/optional-fields.ts +198 -0
- package/src/mcp/tools/__tests__/sf-gql-create.spec.ts +5 -2
- package/src/mcp/tools/__tests__/sf-gql-detail.spec.ts +1 -1
- package/src/mcp/tools/__tests__/sf-gql-list.spec.ts +1 -1
- package/src/mcp/tools/__tests__/sf-gql-raw.spec.ts +1 -1
- package/src/mcp/tools/__tests__/sf-gql-update.spec.ts +5 -2
- package/src/schemas/input-schemas.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
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.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v10.18.3...v10.19.0) (2026-06-19)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- **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))
|
|
11
|
+
|
|
6
12
|
## [10.18.3](https://github.com/salesforce-experience-platform-emu/webapps/compare/v10.18.2...v10.18.3) (2026-06-19)
|
|
7
13
|
|
|
8
14
|
**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":"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"}
|
package/package.json
CHANGED
|
@@ -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
|
|
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
|
-
|
|
166
|
-
expect(out.query).toMatch(
|
|
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
|
-
|
|
103
|
-
expect(out.query).toMatch(
|
|
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
|
-
|
|
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
|
-
|
|
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"])
|