@salesforce/graphiti 10.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_GUIDE.md +424 -0
- package/CHANGELOG.md +448 -0
- package/LICENSE.txt +82 -0
- package/README.md +204 -0
- package/TASK.md +249 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +683 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/args.d.ts +13 -0
- package/dist/commands/args.js +207 -0
- package/dist/commands/args.js.map +1 -0
- package/dist/commands/build.d.ts +11 -0
- package/dist/commands/build.js +209 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/connect.d.ts +8 -0
- package/dist/commands/connect.js +55 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/describe.d.ts +6 -0
- package/dist/commands/describe.js +229 -0
- package/dist/commands/describe.js.map +1 -0
- package/dist/commands/meta.d.ts +9 -0
- package/dist/commands/meta.js +140 -0
- package/dist/commands/meta.js.map +1 -0
- package/dist/commands/navigate.d.ts +14 -0
- package/dist/commands/navigate.js +105 -0
- package/dist/commands/navigate.js.map +1 -0
- package/dist/commands/query-helpers.d.ts +80 -0
- package/dist/commands/query-helpers.js +865 -0
- package/dist/commands/query-helpers.js.map +1 -0
- package/dist/commands/query.d.ts +26 -0
- package/dist/commands/query.js +901 -0
- package/dist/commands/query.js.map +1 -0
- package/dist/commands/review.d.ts +18 -0
- package/dist/commands/review.js +533 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/session-mgmt.d.ts +25 -0
- package/dist/commands/session-mgmt.js +206 -0
- package/dist/commands/session-mgmt.js.map +1 -0
- package/dist/commands/type.d.ts +6 -0
- package/dist/commands/type.js +342 -0
- package/dist/commands/type.js.map +1 -0
- package/dist/commands/validate-input.d.ts +6 -0
- package/dist/commands/validate-input.js +32 -0
- package/dist/commands/validate-input.js.map +1 -0
- package/dist/intent/build-aggregate.d.ts +33 -0
- package/dist/intent/build-aggregate.js +134 -0
- package/dist/intent/build-aggregate.js.map +1 -0
- package/dist/intent/build-create.d.ts +14 -0
- package/dist/intent/build-create.js +16 -0
- package/dist/intent/build-create.js.map +1 -0
- package/dist/intent/build-delete.d.ts +30 -0
- package/dist/intent/build-delete.js +53 -0
- package/dist/intent/build-delete.js.map +1 -0
- package/dist/intent/build-detail.d.ts +32 -0
- package/dist/intent/build-detail.js +80 -0
- package/dist/intent/build-detail.js.map +1 -0
- package/dist/intent/build-discover.d.ts +30 -0
- package/dist/intent/build-discover.js +149 -0
- package/dist/intent/build-discover.js.map +1 -0
- package/dist/intent/build-list.d.ts +28 -0
- package/dist/intent/build-list.js +75 -0
- package/dist/intent/build-list.js.map +1 -0
- package/dist/intent/build-mutation.d.ts +23 -0
- package/dist/intent/build-mutation.js +54 -0
- package/dist/intent/build-mutation.js.map +1 -0
- package/dist/intent/build-output.d.ts +27 -0
- package/dist/intent/build-output.js +60 -0
- package/dist/intent/build-output.js.map +1 -0
- package/dist/intent/build-raw.d.ts +23 -0
- package/dist/intent/build-raw.js +54 -0
- package/dist/intent/build-raw.js.map +1 -0
- package/dist/intent/build-update.d.ts +14 -0
- package/dist/intent/build-update.js +16 -0
- package/dist/intent/build-update.js.map +1 -0
- package/dist/intent/get-schema-with-priming.d.ts +26 -0
- package/dist/intent/get-schema-with-priming.js +32 -0
- package/dist/intent/get-schema-with-priming.js.map +1 -0
- package/dist/intent/select-child-relationship.d.ts +19 -0
- package/dist/intent/select-child-relationship.js +38 -0
- package/dist/intent/select-child-relationship.js.map +1 -0
- package/dist/intent/types.d.ts +159 -0
- package/dist/intent/types.js +21 -0
- package/dist/intent/types.js.map +1 -0
- package/dist/lib/apply-command.d.ts +15 -0
- package/dist/lib/apply-command.js +238 -0
- package/dist/lib/apply-command.js.map +1 -0
- package/dist/lib/auth.d.ts +38 -0
- package/dist/lib/auth.js +113 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/codegen.d.ts +32 -0
- package/dist/lib/codegen.js +700 -0
- package/dist/lib/codegen.js.map +1 -0
- package/dist/lib/command-registry.d.ts +59 -0
- package/dist/lib/command-registry.js +366 -0
- package/dist/lib/command-registry.js.map +1 -0
- package/dist/lib/formatter.d.ts +76 -0
- package/dist/lib/formatter.js +419 -0
- package/dist/lib/formatter.js.map +1 -0
- package/dist/lib/fs-utils.d.ts +23 -0
- package/dist/lib/fs-utils.js +46 -0
- package/dist/lib/fs-utils.js.map +1 -0
- package/dist/lib/graphql-name.d.ts +27 -0
- package/dist/lib/graphql-name.js +32 -0
- package/dist/lib/graphql-name.js.map +1 -0
- package/dist/lib/interactive.d.ts +6 -0
- package/dist/lib/interactive.js +562 -0
- package/dist/lib/interactive.js.map +1 -0
- package/dist/lib/introspect.d.ts +40 -0
- package/dist/lib/introspect.js +280 -0
- package/dist/lib/introspect.js.map +1 -0
- package/dist/lib/object-info.d.ts +79 -0
- package/dist/lib/object-info.js +313 -0
- package/dist/lib/object-info.js.map +1 -0
- package/dist/lib/path-selection.d.ts +50 -0
- package/dist/lib/path-selection.js +146 -0
- package/dist/lib/path-selection.js.map +1 -0
- package/dist/lib/prime-schema.d.ts +59 -0
- package/dist/lib/prime-schema.js +158 -0
- package/dist/lib/prime-schema.js.map +1 -0
- package/dist/lib/query-builder.d.ts +10 -0
- package/dist/lib/query-builder.js +168 -0
- package/dist/lib/query-builder.js.map +1 -0
- package/dist/lib/query-commands.d.ts +19 -0
- package/dist/lib/query-commands.js +262 -0
- package/dist/lib/query-commands.js.map +1 -0
- package/dist/lib/session.d.ts +205 -0
- package/dist/lib/session.js +1075 -0
- package/dist/lib/session.js.map +1 -0
- package/dist/lib/tokenize.d.ts +12 -0
- package/dist/lib/tokenize.js +48 -0
- package/dist/lib/tokenize.js.map +1 -0
- package/dist/lib/uiapi.d.ts +109 -0
- package/dist/lib/uiapi.js +159 -0
- package/dist/lib/uiapi.js.map +1 -0
- package/dist/lib/validator.d.ts +27 -0
- package/dist/lib/validator.js +100 -0
- package/dist/lib/validator.js.map +1 -0
- package/dist/lib/variable-promotion.d.ts +69 -0
- package/dist/lib/variable-promotion.js +95 -0
- package/dist/lib/variable-promotion.js.map +1 -0
- package/dist/lib/walker.d.ts +147 -0
- package/dist/lib/walker.js +723 -0
- package/dist/lib/walker.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +34 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.d.ts +7 -0
- package/dist/mcp/stdio.js +25 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/mcp/tools/echo.d.ts +7 -0
- package/dist/mcp/tools/echo.js +17 -0
- package/dist/mcp/tools/echo.js.map +1 -0
- package/dist/mcp/tools/sf-gql-aggregate.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-aggregate.js +75 -0
- package/dist/mcp/tools/sf-gql-aggregate.js.map +1 -0
- package/dist/mcp/tools/sf-gql-create.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-create.js +35 -0
- package/dist/mcp/tools/sf-gql-create.js.map +1 -0
- package/dist/mcp/tools/sf-gql-delete.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-delete.js +31 -0
- package/dist/mcp/tools/sf-gql-delete.js.map +1 -0
- package/dist/mcp/tools/sf-gql-detail.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-detail.js +58 -0
- package/dist/mcp/tools/sf-gql-detail.js.map +1 -0
- package/dist/mcp/tools/sf-gql-discover.d.ts +9 -0
- package/dist/mcp/tools/sf-gql-discover.js +51 -0
- package/dist/mcp/tools/sf-gql-discover.js.map +1 -0
- package/dist/mcp/tools/sf-gql-list.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-list.js +53 -0
- package/dist/mcp/tools/sf-gql-list.js.map +1 -0
- package/dist/mcp/tools/sf-gql-raw.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-raw.js +39 -0
- package/dist/mcp/tools/sf-gql-raw.js.map +1 -0
- package/dist/mcp/tools/sf-gql-update.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-update.js +35 -0
- package/dist/mcp/tools/sf-gql-update.js.map +1 -0
- package/dist/mcp/tools/shared/zod-schemas.d.ts +49 -0
- package/dist/mcp/tools/shared/zod-schemas.js +46 -0
- package/dist/mcp/tools/shared/zod-schemas.js.map +1 -0
- package/package.json +36 -0
- package/ralph-loop.sh +120 -0
- package/scripts/smoke-orderby.sh +190 -0
- package/src/__tests__/helpers/prime-deps.ts +46 -0
- package/src/__tests__/helpers/schema.ts +73 -0
- package/src/__tests__/helpers/stdout.ts +33 -0
- package/src/__tests__/setup.ts +19 -0
- package/src/cli.ts +764 -0
- package/src/commands/__tests__/query.spec.ts +137 -0
- package/src/commands/args.ts +306 -0
- package/src/commands/build.ts +288 -0
- package/src/commands/connect.ts +60 -0
- package/src/commands/describe.ts +246 -0
- package/src/commands/meta.ts +171 -0
- package/src/commands/navigate.ts +134 -0
- package/src/commands/query-helpers.ts +1202 -0
- package/src/commands/query.ts +1085 -0
- package/src/commands/review.ts +670 -0
- package/src/commands/session-mgmt.ts +256 -0
- package/src/commands/type.ts +437 -0
- package/src/commands/validate-input.ts +38 -0
- package/src/intent/__tests__/build-aggregate.spec.ts +931 -0
- package/src/intent/__tests__/build-create-validation.spec.ts +135 -0
- package/src/intent/__tests__/build-delete.spec.ts +121 -0
- package/src/intent/__tests__/build-detail.spec.ts +333 -0
- package/src/intent/__tests__/build-discover.spec.ts +432 -0
- package/src/intent/__tests__/build-list.spec.ts +284 -0
- package/src/intent/__tests__/build-mutation.spec.ts +108 -0
- package/src/intent/__tests__/build-output.spec.ts +55 -0
- package/src/intent/__tests__/build-raw.spec.ts +153 -0
- package/src/intent/__tests__/build-update-validation.spec.ts +134 -0
- package/src/intent/build-aggregate.ts +182 -0
- package/src/intent/build-create.ts +19 -0
- package/src/intent/build-delete.ts +62 -0
- package/src/intent/build-detail.ts +95 -0
- package/src/intent/build-discover.ts +199 -0
- package/src/intent/build-list.ts +91 -0
- package/src/intent/build-mutation.ts +75 -0
- package/src/intent/build-output.ts +72 -0
- package/src/intent/build-raw.ts +61 -0
- package/src/intent/build-update.ts +19 -0
- package/src/intent/get-schema-with-priming.ts +43 -0
- package/src/intent/select-child-relationship.ts +48 -0
- package/src/intent/types.ts +181 -0
- package/src/lib/__tests__/apply-command.parity.spec.ts +97 -0
- package/src/lib/__tests__/apply-command.spec.ts +171 -0
- package/src/lib/__tests__/auth.spec.ts +292 -0
- package/src/lib/__tests__/formatter.spec.ts +86 -0
- package/src/lib/__tests__/graphql-name.spec.ts +64 -0
- package/src/lib/__tests__/introspect.spec.ts +399 -0
- package/src/lib/__tests__/object-info.spec.ts +124 -0
- package/src/lib/__tests__/path-selection.spec.ts +219 -0
- package/src/lib/__tests__/prime-schema.spec.ts +269 -0
- package/src/lib/__tests__/query-builder.spec.ts +95 -0
- package/src/lib/__tests__/query-commands.spec.ts +74 -0
- package/src/lib/__tests__/session.spec.ts +292 -0
- package/src/lib/__tests__/tokenize.spec.ts +33 -0
- package/src/lib/__tests__/uiapi.spec.ts +192 -0
- package/src/lib/__tests__/variable-promotion.spec.ts +211 -0
- package/src/lib/__tests__/walker.spec.ts +250 -0
- package/src/lib/apply-command.ts +286 -0
- package/src/lib/auth.ts +157 -0
- package/src/lib/codegen.ts +899 -0
- package/src/lib/command-registry.ts +434 -0
- package/src/lib/formatter.ts +587 -0
- package/src/lib/fs-utils.ts +46 -0
- package/src/lib/graphql-name.ts +35 -0
- package/src/lib/interactive.ts +634 -0
- package/src/lib/introspect.ts +320 -0
- package/src/lib/object-info.ts +409 -0
- package/src/lib/path-selection.ts +162 -0
- package/src/lib/prime-schema.ts +195 -0
- package/src/lib/query-builder.ts +193 -0
- package/src/lib/query-commands.ts +290 -0
- package/src/lib/session.ts +1304 -0
- package/src/lib/tokenize.ts +43 -0
- package/src/lib/uiapi.ts +176 -0
- package/src/lib/validator.ts +143 -0
- package/src/lib/variable-promotion.ts +145 -0
- package/src/lib/walker.ts +975 -0
- package/src/mcp/__tests__/server.spec.ts +155 -0
- package/src/mcp/server.ts +38 -0
- package/src/mcp/stdio.ts +29 -0
- package/src/mcp/tools/__tests__/sf-gql-aggregate.spec.ts +173 -0
- package/src/mcp/tools/__tests__/sf-gql-create.spec.ts +235 -0
- package/src/mcp/tools/__tests__/sf-gql-delete.spec.ts +194 -0
- package/src/mcp/tools/__tests__/sf-gql-detail.spec.ts +246 -0
- package/src/mcp/tools/__tests__/sf-gql-discover.spec.ts +320 -0
- package/src/mcp/tools/__tests__/sf-gql-list.spec.ts +128 -0
- package/src/mcp/tools/__tests__/sf-gql-raw.spec.ts +141 -0
- package/src/mcp/tools/__tests__/sf-gql-update.spec.ts +207 -0
- package/src/mcp/tools/echo.ts +24 -0
- package/src/mcp/tools/sf-gql-aggregate.ts +102 -0
- package/src/mcp/tools/sf-gql-create.ts +55 -0
- package/src/mcp/tools/sf-gql-delete.ts +49 -0
- package/src/mcp/tools/sf-gql-detail.ts +85 -0
- package/src/mcp/tools/sf-gql-discover.ts +67 -0
- package/src/mcp/tools/sf-gql-list.ts +73 -0
- package/src/mcp/tools/sf-gql-raw.ts +56 -0
- package/src/mcp/tools/sf-gql-update.ts +55 -0
- package/src/mcp/tools/shared/zod-schemas.ts +55 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { buildSchema } from "graphql";
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { createSession, type QuerySession } from "../session.js";
|
|
10
|
+
import { normalizeOrderBy, promoteVariables } from "../variable-promotion.js";
|
|
11
|
+
|
|
12
|
+
// Mimics the relevant slice of the Salesforce UIAPI schema: a connection
|
|
13
|
+
// field with `where`, `orderBy`, and `scope` args, where `where` is a
|
|
14
|
+
// nested input type with operator wrappers.
|
|
15
|
+
const schema = buildSchema(`
|
|
16
|
+
type Query {
|
|
17
|
+
uiapi: UIAPI!
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type UIAPI {
|
|
21
|
+
query: RecordQuery!
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type RecordQuery {
|
|
25
|
+
Account(
|
|
26
|
+
first: Int
|
|
27
|
+
where: Account_Filter
|
|
28
|
+
orderBy: Account_OrderBy
|
|
29
|
+
scope: RecordScope
|
|
30
|
+
): AccountConnection!
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type AccountConnection { edges: [AccountEdge!]! }
|
|
34
|
+
type AccountEdge { node: Account! }
|
|
35
|
+
|
|
36
|
+
type Account {
|
|
37
|
+
Id: ID!
|
|
38
|
+
Name: StringValue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type StringValue { value: String }
|
|
42
|
+
|
|
43
|
+
input Account_Filter {
|
|
44
|
+
Status: PicklistOperators
|
|
45
|
+
Industry: StringOperators
|
|
46
|
+
Or: [Account_Filter!]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
input PicklistOperators {
|
|
50
|
+
eq: Picklist
|
|
51
|
+
ne: Picklist
|
|
52
|
+
in: [Picklist!]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
input StringOperators {
|
|
56
|
+
eq: String
|
|
57
|
+
ne: String
|
|
58
|
+
like: String
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
input Account_OrderBy {
|
|
62
|
+
Name: OrderByEnum
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
enum OrderByEnum { ASC DESC }
|
|
66
|
+
enum RecordScope { MINE TEAM }
|
|
67
|
+
|
|
68
|
+
scalar Picklist
|
|
69
|
+
`);
|
|
70
|
+
|
|
71
|
+
const accountFieldPath = ["uiapi", "query", "Account"];
|
|
72
|
+
|
|
73
|
+
function makeSession(): QuerySession {
|
|
74
|
+
return createSession("test-org", "query");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe("variable-promotion", () => {
|
|
78
|
+
describe("promoteVariables: where filter (nested)", () => {
|
|
79
|
+
it("registers a $var leaf inside a nested where filter", () => {
|
|
80
|
+
const session = makeSession();
|
|
81
|
+
|
|
82
|
+
promoteVariables(session, schema, accountFieldPath, "where", {
|
|
83
|
+
Status: { eq: "$status" },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(session.variables).toHaveLength(1);
|
|
87
|
+
expect(session.variables[0]!.name).toBe("status");
|
|
88
|
+
// PicklistOperators.eq has type Picklist; promoted vars drop the trailing `!`.
|
|
89
|
+
expect(session.variables[0]!.type).toBe("Picklist");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("registers multiple $vars at sibling paths", () => {
|
|
93
|
+
const session = makeSession();
|
|
94
|
+
|
|
95
|
+
promoteVariables(session, schema, accountFieldPath, "where", {
|
|
96
|
+
Status: { eq: "$status" },
|
|
97
|
+
Industry: { like: "$industry" },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const names = session.variables.map((v) => v.name).sort();
|
|
101
|
+
expect(names).toEqual(["industry", "status"]);
|
|
102
|
+
const byName = Object.fromEntries(session.variables.map((v) => [v.name, v.type]));
|
|
103
|
+
expect(byName.status).toBe("Picklist");
|
|
104
|
+
expect(byName.industry).toBe("String");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("walks into arrays inside the value", () => {
|
|
108
|
+
const session = makeSession();
|
|
109
|
+
|
|
110
|
+
// Or is `[Account_Filter!]` — array of filter objects. The walker must
|
|
111
|
+
// descend into the array and find $vars in each element.
|
|
112
|
+
promoteVariables(session, schema, accountFieldPath, "where", {
|
|
113
|
+
Or: [{ Status: { eq: "$a" } }, { Industry: { eq: "$b" } }],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const names = session.variables.map((v) => v.name).sort();
|
|
117
|
+
expect(names).toEqual(["a", "b"]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("promoteVariables: orderBy and scope", () => {
|
|
122
|
+
it("works on orderBy values", () => {
|
|
123
|
+
const session = makeSession();
|
|
124
|
+
|
|
125
|
+
promoteVariables(session, schema, accountFieldPath, "orderBy", {
|
|
126
|
+
Name: "$direction",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(session.variables).toHaveLength(1);
|
|
130
|
+
expect(session.variables[0]!.name).toBe("direction");
|
|
131
|
+
expect(session.variables[0]!.type).toBe("OrderByEnum");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("works on scope (top-level $var leaf)", () => {
|
|
135
|
+
const session = makeSession();
|
|
136
|
+
|
|
137
|
+
// `scope` is a single enum arg, not a nested object. The walker should
|
|
138
|
+
// treat the value itself as the leaf.
|
|
139
|
+
promoteVariables(session, schema, accountFieldPath, "scope", "$myScope");
|
|
140
|
+
|
|
141
|
+
expect(session.variables).toHaveLength(1);
|
|
142
|
+
expect(session.variables[0]!.name).toBe("myScope");
|
|
143
|
+
expect(session.variables[0]!.type).toBe("RecordScope");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("promoteVariables: error tolerance", () => {
|
|
148
|
+
it("falls back to String when type inference fails", () => {
|
|
149
|
+
const session = makeSession();
|
|
150
|
+
|
|
151
|
+
// Path that doesn't exist on the schema — inference will throw. The
|
|
152
|
+
// helper must still register the variable (a wrong type is more useful
|
|
153
|
+
// than a build that fails before the user sees their query).
|
|
154
|
+
promoteVariables(session, schema, accountFieldPath, "where", {
|
|
155
|
+
NotARealField: { eq: "$mystery" },
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(session.variables).toHaveLength(1);
|
|
159
|
+
expect(session.variables[0]!.name).toBe("mystery");
|
|
160
|
+
expect(session.variables[0]!.type).toBe("String");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("ignores plain string values that are not $var placeholders", () => {
|
|
164
|
+
const session = makeSession();
|
|
165
|
+
|
|
166
|
+
promoteVariables(session, schema, accountFieldPath, "where", {
|
|
167
|
+
Status: { eq: "Active" },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(session.variables).toHaveLength(0);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("strips a trailing ! when the inferred type is non-null", () => {
|
|
174
|
+
// The schema has Account_Filter.Or: [Account_OrderBy!] — the nested
|
|
175
|
+
// element is non-null. If a $var is placed where a non-null is required,
|
|
176
|
+
// the promoted variable type must drop the `!` (query variables are
|
|
177
|
+
// nullable by convention; callers add `!` explicitly via `var` if they
|
|
178
|
+
// need it).
|
|
179
|
+
const session = makeSession();
|
|
180
|
+
|
|
181
|
+
promoteVariables(session, schema, accountFieldPath, "where", {
|
|
182
|
+
Or: ["$first"],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(session.variables).toHaveLength(1);
|
|
186
|
+
// Or's inner type is `Account_Filter!`; we strip the `!` and keep the
|
|
187
|
+
// list wrapping that inferTypeFromArgsPath already applied.
|
|
188
|
+
expect(session.variables[0]!.type.endsWith("!")).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("normalizeOrderBy", () => {
|
|
193
|
+
it("collapses an array to its first element", () => {
|
|
194
|
+
const result = normalizeOrderBy([{ Name: "ASC" }, { Industry: "DESC" }]);
|
|
195
|
+
expect(result).toEqual({ Name: "ASC" });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("returns undefined for an empty array", () => {
|
|
199
|
+
expect(normalizeOrderBy([])).toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("passes through a singleton object unchanged", () => {
|
|
203
|
+
const input = { Name: "ASC" };
|
|
204
|
+
expect(normalizeOrderBy(input)).toBe(input);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("returns undefined for undefined input", () => {
|
|
208
|
+
expect(normalizeOrderBy(undefined)).toBeUndefined();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
buildSchema,
|
|
11
|
+
getIntrospectionQuery,
|
|
12
|
+
graphqlSync,
|
|
13
|
+
type IntrospectionQuery,
|
|
14
|
+
validateSchema,
|
|
15
|
+
} from "graphql";
|
|
16
|
+
import { describe, expect, it } from "vitest";
|
|
17
|
+
import { TEST_SCHEMA } from "../../__tests__/helpers/schema.js";
|
|
18
|
+
import { selectLeafInSession } from "../../commands/query.js";
|
|
19
|
+
import { clearOrgAuthCache } from "../auth.js";
|
|
20
|
+
import { getSchemaFilePath } from "../introspect.js";
|
|
21
|
+
import { createSession } from "../session.js";
|
|
22
|
+
import { validateQuery } from "../validator.js";
|
|
23
|
+
import {
|
|
24
|
+
clearSchemaCache,
|
|
25
|
+
filterDataCloudFields,
|
|
26
|
+
type FieldInfo,
|
|
27
|
+
getSchema,
|
|
28
|
+
isDataCloudField,
|
|
29
|
+
primeSchemaCache,
|
|
30
|
+
resolveInputPath,
|
|
31
|
+
resolvePath,
|
|
32
|
+
} from "../walker.js";
|
|
33
|
+
|
|
34
|
+
describe("walker", () => {
|
|
35
|
+
describe("schema cache", () => {
|
|
36
|
+
it("uses instanceUrl from session as the schema cache key", () => {
|
|
37
|
+
const alias = `test_${Math.random().toString(16).slice(2, 8)}`;
|
|
38
|
+
const instanceUrl = "https://test.my.salesforce.com";
|
|
39
|
+
|
|
40
|
+
// Prime schema under the normalized URL key so getSchema(instanceUrl) hits cache.
|
|
41
|
+
primeSchemaCache(instanceUrl.toLowerCase(), TEST_SCHEMA);
|
|
42
|
+
clearOrgAuthCache();
|
|
43
|
+
|
|
44
|
+
// Session carries instanceUrl — getSessionSchema should use it directly,
|
|
45
|
+
// hitting the in-process cache without calling sf auth.
|
|
46
|
+
const session = createSession(alias, "query", instanceUrl);
|
|
47
|
+
session.navigationPath = ["query", "viewer"];
|
|
48
|
+
|
|
49
|
+
expect(() => selectLeafInSession(session, "id")).not.toThrow();
|
|
50
|
+
expect(session.nodes.some((n) => n.kind === "field" && n.fieldName === "id")).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("uses cached schema within a process", () => {
|
|
54
|
+
const alias = `test_${Math.random().toString(16).slice(2, 8)}`;
|
|
55
|
+
const instanceUrl = `https://${alias}.my.salesforce.com`;
|
|
56
|
+
primeSchemaCache(instanceUrl, TEST_SCHEMA);
|
|
57
|
+
|
|
58
|
+
const session1 = createSession(alias, "query", instanceUrl);
|
|
59
|
+
const session2 = createSession(alias, "query", instanceUrl);
|
|
60
|
+
|
|
61
|
+
session1.navigationPath = ["query", "viewer"];
|
|
62
|
+
expect(() => selectLeafInSession(session1, "name")).not.toThrow();
|
|
63
|
+
|
|
64
|
+
// Leaf enforcement works on the same cached schema.
|
|
65
|
+
session2.navigationPath = ["query", "accounts"];
|
|
66
|
+
expect(() => selectLeafInSession(session2, "edges")).toThrow(/Cannot select/);
|
|
67
|
+
|
|
68
|
+
// Navigating deeper into a non-leaf and selecting a leaf also works.
|
|
69
|
+
session2.navigationPath = ["query", "accounts", "edges", "node"];
|
|
70
|
+
expect(() => selectLeafInSession(session2, "id")).not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("getSchema returns a primed schema by instance URL", () => {
|
|
74
|
+
const url = "https://example.my.salesforce.com";
|
|
75
|
+
clearSchemaCache();
|
|
76
|
+
primeSchemaCache(url, TEST_SCHEMA);
|
|
77
|
+
expect(getSchema(url)).toBe(TEST_SCHEMA);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("resolveInputPath", () => {
|
|
82
|
+
it("walks INPUT_OBJECT fields", () => {
|
|
83
|
+
const result = resolveInputPath(TEST_SCHEMA, "AccountFilter", []);
|
|
84
|
+
expect(result.typeName).toBe("AccountFilter");
|
|
85
|
+
expect(result.kind).toBe("INPUT_OBJECT");
|
|
86
|
+
expect(result.isLeaf).toBe(false);
|
|
87
|
+
expect(result.inputFields.length).toBeGreaterThan(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("reaches scalar leaf", () => {
|
|
91
|
+
const result = resolveInputPath(TEST_SCHEMA, "AccountFilter", ["minRevenue"]);
|
|
92
|
+
expect(result.typeName).toBe("Int");
|
|
93
|
+
expect(result.isLeaf).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("walks nested input types", () => {
|
|
97
|
+
const result = resolveInputPath(TEST_SCHEMA, "AccountFilter", ["name"]);
|
|
98
|
+
expect(result.typeName).toBe("StringFilter");
|
|
99
|
+
expect(result.isLeaf).toBe(false);
|
|
100
|
+
|
|
101
|
+
const leafResult = resolveInputPath(TEST_SCHEMA, "AccountFilter", ["name", "like"]);
|
|
102
|
+
expect(leafResult.typeName).toBe("String");
|
|
103
|
+
expect(leafResult.isLeaf).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("throws for invalid field", () => {
|
|
107
|
+
expect(() => resolveInputPath(TEST_SCHEMA, "AccountFilter", ["nonExistent"])).toThrow(
|
|
108
|
+
/Field "nonExistent" not found on input type AccountFilter/,
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("Data Cloud field detection & filtering", () => {
|
|
114
|
+
function mockField(name: string): FieldInfo {
|
|
115
|
+
return {
|
|
116
|
+
name,
|
|
117
|
+
typeName: `${name}Connection`,
|
|
118
|
+
typeKind: "OBJECT",
|
|
119
|
+
isNonNull: false,
|
|
120
|
+
isList: false,
|
|
121
|
+
description: null,
|
|
122
|
+
args: [],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
it("isDataCloudField detects __dlm suffix", () => {
|
|
127
|
+
expect(isDataCloudField(mockField("ssot__Account__dlm"))).toBe(true);
|
|
128
|
+
expect(isDataCloudField(mockField("IndividualGDPRState__dlm"))).toBe(true);
|
|
129
|
+
expect(isDataCloudField(mockField("Account"))).toBe(false);
|
|
130
|
+
expect(isDataCloudField(mockField("ssot__Account"))).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("filterDataCloudFields hides __dlm fields by default", () => {
|
|
134
|
+
const fields = [
|
|
135
|
+
mockField("Account"),
|
|
136
|
+
mockField("ssot__Account__dlm"),
|
|
137
|
+
mockField("Case"),
|
|
138
|
+
mockField("ssot__Contact__dlm"),
|
|
139
|
+
];
|
|
140
|
+
const filtered = filterDataCloudFields(fields, false);
|
|
141
|
+
expect(filtered).toHaveLength(2);
|
|
142
|
+
expect(filtered.map((f) => f.name)).toEqual(["Account", "Case"]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("filterDataCloudFields shows all when includeDataCloud is true", () => {
|
|
146
|
+
const fields = [mockField("Account"), mockField("ssot__Account__dlm"), mockField("Case")];
|
|
147
|
+
const filtered = filterDataCloudFields(fields, true);
|
|
148
|
+
expect(filtered).toHaveLength(3);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// resolvePath is referenced indirectly by query-builder.spec.ts; import retained
|
|
153
|
+
// to verify the export still exists.
|
|
154
|
+
it("resolvePath is exported", () => {
|
|
155
|
+
expect(resolvePath).toBeInstanceOf(Function);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Regression test for: Salesforce orgs that emit `INPUT_OBJECT` types with
|
|
159
|
+
// empty `inputFields: []` (e.g. `*_SearchOrderBy` types and a few mutation
|
|
160
|
+
// Representation types) used to crash `check`/`validate(schema, document)`
|
|
161
|
+
// with "Input Object type X must define one or more fields" before the
|
|
162
|
+
// user's query was ever inspected. The fix in
|
|
163
|
+
// `lib/walker.ts:patchEmptyInputObjects` patches the introspection
|
|
164
|
+
// in-memory by adding a synthetic `_placeholder: String` field.
|
|
165
|
+
describe("empty input type patching", () => {
|
|
166
|
+
function buildIntrospectionWithEmptyInput(): IntrospectionQuery {
|
|
167
|
+
const schema = buildSchema(`
|
|
168
|
+
type Query {
|
|
169
|
+
account(filter: AccountFilter): Account
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
input AccountFilter {
|
|
173
|
+
# Mirrors a real-world Salesforce shape: this field references
|
|
174
|
+
# an empty input type (replicates *_SearchOrderBy emission).
|
|
175
|
+
orderBy: AccountSearchOrderBy
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
input AccountSearchOrderBy {
|
|
179
|
+
placeholder: String
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
type Account {
|
|
183
|
+
id: ID!
|
|
184
|
+
name: String
|
|
185
|
+
}
|
|
186
|
+
`);
|
|
187
|
+
|
|
188
|
+
const result = graphqlSync({ schema, source: getIntrospectionQuery() });
|
|
189
|
+
const introspection = result.data as unknown as IntrospectionQuery;
|
|
190
|
+
|
|
191
|
+
// Mutate the introspection to drop AccountSearchOrderBy's fields,
|
|
192
|
+
// reproducing the broken Salesforce shape. Cast through `unknown` to
|
|
193
|
+
// bypass the readonly array type from graphql-js.
|
|
194
|
+
const types = introspection.__schema.types as unknown as {
|
|
195
|
+
kind: string;
|
|
196
|
+
name: string;
|
|
197
|
+
inputFields?: unknown[];
|
|
198
|
+
}[];
|
|
199
|
+
for (const t of types) {
|
|
200
|
+
if (t.kind === "INPUT_OBJECT" && t.name === "AccountSearchOrderBy") {
|
|
201
|
+
t.inputFields = [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return introspection;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function seedFixtureSchema(instanceUrl: string): void {
|
|
209
|
+
const introspection = buildIntrospectionWithEmptyInput();
|
|
210
|
+
const filePath = getSchemaFilePath(instanceUrl);
|
|
211
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
212
|
+
fs.writeFileSync(filePath, JSON.stringify({ data: introspection }), "utf-8");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
it("getSchema patches INPUT_OBJECT types with no fields (regression)", () => {
|
|
216
|
+
const instanceUrl = "https://empty-input-fixture.test.salesforce.com";
|
|
217
|
+
seedFixtureSchema(instanceUrl);
|
|
218
|
+
clearSchemaCache();
|
|
219
|
+
|
|
220
|
+
const schema = getSchema(instanceUrl);
|
|
221
|
+
|
|
222
|
+
// Without the fix, validateSchema would report:
|
|
223
|
+
// "Input Object type AccountSearchOrderBy must define one or more fields."
|
|
224
|
+
const errors = validateSchema(schema);
|
|
225
|
+
expect(errors.map((e) => e.message)).toEqual([]);
|
|
226
|
+
|
|
227
|
+
// And validateQuery (which is what `check` calls) must work without
|
|
228
|
+
// throwing on the schema before it even reaches the user's query.
|
|
229
|
+
const queryErrors = validateQuery(
|
|
230
|
+
schema,
|
|
231
|
+
`query { account(filter: { orderBy: {} }) { id name } }`,
|
|
232
|
+
);
|
|
233
|
+
// We don't assert the query is error-free — only that the schema
|
|
234
|
+
// validation step didn't bail. (Querying with `{}` against the patched
|
|
235
|
+
// type may itself report 0 errors since the placeholder is optional.)
|
|
236
|
+
expect(Array.isArray(queryErrors)).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("patched empty input type retains its name and is referenceable", () => {
|
|
240
|
+
const instanceUrl = "https://empty-input-fixture-2.test.salesforce.com";
|
|
241
|
+
seedFixtureSchema(instanceUrl);
|
|
242
|
+
clearSchemaCache();
|
|
243
|
+
|
|
244
|
+
const schema = getSchema(instanceUrl);
|
|
245
|
+
const t = schema.getType("AccountSearchOrderBy");
|
|
246
|
+
expect(t).toBeDefined();
|
|
247
|
+
expect(t?.name).toBe("AccountSearchOrderBy");
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|