@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.
Files changed (282) hide show
  1. package/AGENT_GUIDE.md +424 -0
  2. package/CHANGELOG.md +448 -0
  3. package/LICENSE.txt +82 -0
  4. package/README.md +204 -0
  5. package/TASK.md +249 -0
  6. package/dist/cli.d.ts +7 -0
  7. package/dist/cli.js +683 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/commands/args.d.ts +13 -0
  10. package/dist/commands/args.js +207 -0
  11. package/dist/commands/args.js.map +1 -0
  12. package/dist/commands/build.d.ts +11 -0
  13. package/dist/commands/build.js +209 -0
  14. package/dist/commands/build.js.map +1 -0
  15. package/dist/commands/connect.d.ts +8 -0
  16. package/dist/commands/connect.js +55 -0
  17. package/dist/commands/connect.js.map +1 -0
  18. package/dist/commands/describe.d.ts +6 -0
  19. package/dist/commands/describe.js +229 -0
  20. package/dist/commands/describe.js.map +1 -0
  21. package/dist/commands/meta.d.ts +9 -0
  22. package/dist/commands/meta.js +140 -0
  23. package/dist/commands/meta.js.map +1 -0
  24. package/dist/commands/navigate.d.ts +14 -0
  25. package/dist/commands/navigate.js +105 -0
  26. package/dist/commands/navigate.js.map +1 -0
  27. package/dist/commands/query-helpers.d.ts +80 -0
  28. package/dist/commands/query-helpers.js +865 -0
  29. package/dist/commands/query-helpers.js.map +1 -0
  30. package/dist/commands/query.d.ts +26 -0
  31. package/dist/commands/query.js +901 -0
  32. package/dist/commands/query.js.map +1 -0
  33. package/dist/commands/review.d.ts +18 -0
  34. package/dist/commands/review.js +533 -0
  35. package/dist/commands/review.js.map +1 -0
  36. package/dist/commands/session-mgmt.d.ts +25 -0
  37. package/dist/commands/session-mgmt.js +206 -0
  38. package/dist/commands/session-mgmt.js.map +1 -0
  39. package/dist/commands/type.d.ts +6 -0
  40. package/dist/commands/type.js +342 -0
  41. package/dist/commands/type.js.map +1 -0
  42. package/dist/commands/validate-input.d.ts +6 -0
  43. package/dist/commands/validate-input.js +32 -0
  44. package/dist/commands/validate-input.js.map +1 -0
  45. package/dist/intent/build-aggregate.d.ts +33 -0
  46. package/dist/intent/build-aggregate.js +134 -0
  47. package/dist/intent/build-aggregate.js.map +1 -0
  48. package/dist/intent/build-create.d.ts +14 -0
  49. package/dist/intent/build-create.js +16 -0
  50. package/dist/intent/build-create.js.map +1 -0
  51. package/dist/intent/build-delete.d.ts +30 -0
  52. package/dist/intent/build-delete.js +53 -0
  53. package/dist/intent/build-delete.js.map +1 -0
  54. package/dist/intent/build-detail.d.ts +32 -0
  55. package/dist/intent/build-detail.js +80 -0
  56. package/dist/intent/build-detail.js.map +1 -0
  57. package/dist/intent/build-discover.d.ts +30 -0
  58. package/dist/intent/build-discover.js +149 -0
  59. package/dist/intent/build-discover.js.map +1 -0
  60. package/dist/intent/build-list.d.ts +28 -0
  61. package/dist/intent/build-list.js +75 -0
  62. package/dist/intent/build-list.js.map +1 -0
  63. package/dist/intent/build-mutation.d.ts +23 -0
  64. package/dist/intent/build-mutation.js +54 -0
  65. package/dist/intent/build-mutation.js.map +1 -0
  66. package/dist/intent/build-output.d.ts +27 -0
  67. package/dist/intent/build-output.js +60 -0
  68. package/dist/intent/build-output.js.map +1 -0
  69. package/dist/intent/build-raw.d.ts +23 -0
  70. package/dist/intent/build-raw.js +54 -0
  71. package/dist/intent/build-raw.js.map +1 -0
  72. package/dist/intent/build-update.d.ts +14 -0
  73. package/dist/intent/build-update.js +16 -0
  74. package/dist/intent/build-update.js.map +1 -0
  75. package/dist/intent/get-schema-with-priming.d.ts +26 -0
  76. package/dist/intent/get-schema-with-priming.js +32 -0
  77. package/dist/intent/get-schema-with-priming.js.map +1 -0
  78. package/dist/intent/select-child-relationship.d.ts +19 -0
  79. package/dist/intent/select-child-relationship.js +38 -0
  80. package/dist/intent/select-child-relationship.js.map +1 -0
  81. package/dist/intent/types.d.ts +159 -0
  82. package/dist/intent/types.js +21 -0
  83. package/dist/intent/types.js.map +1 -0
  84. package/dist/lib/apply-command.d.ts +15 -0
  85. package/dist/lib/apply-command.js +238 -0
  86. package/dist/lib/apply-command.js.map +1 -0
  87. package/dist/lib/auth.d.ts +38 -0
  88. package/dist/lib/auth.js +113 -0
  89. package/dist/lib/auth.js.map +1 -0
  90. package/dist/lib/codegen.d.ts +32 -0
  91. package/dist/lib/codegen.js +700 -0
  92. package/dist/lib/codegen.js.map +1 -0
  93. package/dist/lib/command-registry.d.ts +59 -0
  94. package/dist/lib/command-registry.js +366 -0
  95. package/dist/lib/command-registry.js.map +1 -0
  96. package/dist/lib/formatter.d.ts +76 -0
  97. package/dist/lib/formatter.js +419 -0
  98. package/dist/lib/formatter.js.map +1 -0
  99. package/dist/lib/fs-utils.d.ts +23 -0
  100. package/dist/lib/fs-utils.js +46 -0
  101. package/dist/lib/fs-utils.js.map +1 -0
  102. package/dist/lib/graphql-name.d.ts +27 -0
  103. package/dist/lib/graphql-name.js +32 -0
  104. package/dist/lib/graphql-name.js.map +1 -0
  105. package/dist/lib/interactive.d.ts +6 -0
  106. package/dist/lib/interactive.js +562 -0
  107. package/dist/lib/interactive.js.map +1 -0
  108. package/dist/lib/introspect.d.ts +40 -0
  109. package/dist/lib/introspect.js +280 -0
  110. package/dist/lib/introspect.js.map +1 -0
  111. package/dist/lib/object-info.d.ts +79 -0
  112. package/dist/lib/object-info.js +313 -0
  113. package/dist/lib/object-info.js.map +1 -0
  114. package/dist/lib/path-selection.d.ts +50 -0
  115. package/dist/lib/path-selection.js +146 -0
  116. package/dist/lib/path-selection.js.map +1 -0
  117. package/dist/lib/prime-schema.d.ts +59 -0
  118. package/dist/lib/prime-schema.js +158 -0
  119. package/dist/lib/prime-schema.js.map +1 -0
  120. package/dist/lib/query-builder.d.ts +10 -0
  121. package/dist/lib/query-builder.js +168 -0
  122. package/dist/lib/query-builder.js.map +1 -0
  123. package/dist/lib/query-commands.d.ts +19 -0
  124. package/dist/lib/query-commands.js +262 -0
  125. package/dist/lib/query-commands.js.map +1 -0
  126. package/dist/lib/session.d.ts +205 -0
  127. package/dist/lib/session.js +1075 -0
  128. package/dist/lib/session.js.map +1 -0
  129. package/dist/lib/tokenize.d.ts +12 -0
  130. package/dist/lib/tokenize.js +48 -0
  131. package/dist/lib/tokenize.js.map +1 -0
  132. package/dist/lib/uiapi.d.ts +109 -0
  133. package/dist/lib/uiapi.js +159 -0
  134. package/dist/lib/uiapi.js.map +1 -0
  135. package/dist/lib/validator.d.ts +27 -0
  136. package/dist/lib/validator.js +100 -0
  137. package/dist/lib/validator.js.map +1 -0
  138. package/dist/lib/variable-promotion.d.ts +69 -0
  139. package/dist/lib/variable-promotion.js +95 -0
  140. package/dist/lib/variable-promotion.js.map +1 -0
  141. package/dist/lib/walker.d.ts +147 -0
  142. package/dist/lib/walker.js +723 -0
  143. package/dist/lib/walker.js.map +1 -0
  144. package/dist/mcp/server.d.ts +7 -0
  145. package/dist/mcp/server.js +34 -0
  146. package/dist/mcp/server.js.map +1 -0
  147. package/dist/mcp/stdio.d.ts +7 -0
  148. package/dist/mcp/stdio.js +25 -0
  149. package/dist/mcp/stdio.js.map +1 -0
  150. package/dist/mcp/tools/echo.d.ts +7 -0
  151. package/dist/mcp/tools/echo.js +17 -0
  152. package/dist/mcp/tools/echo.js.map +1 -0
  153. package/dist/mcp/tools/sf-gql-aggregate.d.ts +11 -0
  154. package/dist/mcp/tools/sf-gql-aggregate.js +75 -0
  155. package/dist/mcp/tools/sf-gql-aggregate.js.map +1 -0
  156. package/dist/mcp/tools/sf-gql-create.d.ts +11 -0
  157. package/dist/mcp/tools/sf-gql-create.js +35 -0
  158. package/dist/mcp/tools/sf-gql-create.js.map +1 -0
  159. package/dist/mcp/tools/sf-gql-delete.d.ts +11 -0
  160. package/dist/mcp/tools/sf-gql-delete.js +31 -0
  161. package/dist/mcp/tools/sf-gql-delete.js.map +1 -0
  162. package/dist/mcp/tools/sf-gql-detail.d.ts +11 -0
  163. package/dist/mcp/tools/sf-gql-detail.js +58 -0
  164. package/dist/mcp/tools/sf-gql-detail.js.map +1 -0
  165. package/dist/mcp/tools/sf-gql-discover.d.ts +9 -0
  166. package/dist/mcp/tools/sf-gql-discover.js +51 -0
  167. package/dist/mcp/tools/sf-gql-discover.js.map +1 -0
  168. package/dist/mcp/tools/sf-gql-list.d.ts +11 -0
  169. package/dist/mcp/tools/sf-gql-list.js +53 -0
  170. package/dist/mcp/tools/sf-gql-list.js.map +1 -0
  171. package/dist/mcp/tools/sf-gql-raw.d.ts +11 -0
  172. package/dist/mcp/tools/sf-gql-raw.js +39 -0
  173. package/dist/mcp/tools/sf-gql-raw.js.map +1 -0
  174. package/dist/mcp/tools/sf-gql-update.d.ts +11 -0
  175. package/dist/mcp/tools/sf-gql-update.js +35 -0
  176. package/dist/mcp/tools/sf-gql-update.js.map +1 -0
  177. package/dist/mcp/tools/shared/zod-schemas.d.ts +49 -0
  178. package/dist/mcp/tools/shared/zod-schemas.js +46 -0
  179. package/dist/mcp/tools/shared/zod-schemas.js.map +1 -0
  180. package/package.json +36 -0
  181. package/ralph-loop.sh +120 -0
  182. package/scripts/smoke-orderby.sh +190 -0
  183. package/src/__tests__/helpers/prime-deps.ts +46 -0
  184. package/src/__tests__/helpers/schema.ts +73 -0
  185. package/src/__tests__/helpers/stdout.ts +33 -0
  186. package/src/__tests__/setup.ts +19 -0
  187. package/src/cli.ts +764 -0
  188. package/src/commands/__tests__/query.spec.ts +137 -0
  189. package/src/commands/args.ts +306 -0
  190. package/src/commands/build.ts +288 -0
  191. package/src/commands/connect.ts +60 -0
  192. package/src/commands/describe.ts +246 -0
  193. package/src/commands/meta.ts +171 -0
  194. package/src/commands/navigate.ts +134 -0
  195. package/src/commands/query-helpers.ts +1202 -0
  196. package/src/commands/query.ts +1085 -0
  197. package/src/commands/review.ts +670 -0
  198. package/src/commands/session-mgmt.ts +256 -0
  199. package/src/commands/type.ts +437 -0
  200. package/src/commands/validate-input.ts +38 -0
  201. package/src/intent/__tests__/build-aggregate.spec.ts +931 -0
  202. package/src/intent/__tests__/build-create-validation.spec.ts +135 -0
  203. package/src/intent/__tests__/build-delete.spec.ts +121 -0
  204. package/src/intent/__tests__/build-detail.spec.ts +333 -0
  205. package/src/intent/__tests__/build-discover.spec.ts +432 -0
  206. package/src/intent/__tests__/build-list.spec.ts +284 -0
  207. package/src/intent/__tests__/build-mutation.spec.ts +108 -0
  208. package/src/intent/__tests__/build-output.spec.ts +55 -0
  209. package/src/intent/__tests__/build-raw.spec.ts +153 -0
  210. package/src/intent/__tests__/build-update-validation.spec.ts +134 -0
  211. package/src/intent/build-aggregate.ts +182 -0
  212. package/src/intent/build-create.ts +19 -0
  213. package/src/intent/build-delete.ts +62 -0
  214. package/src/intent/build-detail.ts +95 -0
  215. package/src/intent/build-discover.ts +199 -0
  216. package/src/intent/build-list.ts +91 -0
  217. package/src/intent/build-mutation.ts +75 -0
  218. package/src/intent/build-output.ts +72 -0
  219. package/src/intent/build-raw.ts +61 -0
  220. package/src/intent/build-update.ts +19 -0
  221. package/src/intent/get-schema-with-priming.ts +43 -0
  222. package/src/intent/select-child-relationship.ts +48 -0
  223. package/src/intent/types.ts +181 -0
  224. package/src/lib/__tests__/apply-command.parity.spec.ts +97 -0
  225. package/src/lib/__tests__/apply-command.spec.ts +171 -0
  226. package/src/lib/__tests__/auth.spec.ts +292 -0
  227. package/src/lib/__tests__/formatter.spec.ts +86 -0
  228. package/src/lib/__tests__/graphql-name.spec.ts +64 -0
  229. package/src/lib/__tests__/introspect.spec.ts +399 -0
  230. package/src/lib/__tests__/object-info.spec.ts +124 -0
  231. package/src/lib/__tests__/path-selection.spec.ts +219 -0
  232. package/src/lib/__tests__/prime-schema.spec.ts +269 -0
  233. package/src/lib/__tests__/query-builder.spec.ts +95 -0
  234. package/src/lib/__tests__/query-commands.spec.ts +74 -0
  235. package/src/lib/__tests__/session.spec.ts +292 -0
  236. package/src/lib/__tests__/tokenize.spec.ts +33 -0
  237. package/src/lib/__tests__/uiapi.spec.ts +192 -0
  238. package/src/lib/__tests__/variable-promotion.spec.ts +211 -0
  239. package/src/lib/__tests__/walker.spec.ts +250 -0
  240. package/src/lib/apply-command.ts +286 -0
  241. package/src/lib/auth.ts +157 -0
  242. package/src/lib/codegen.ts +899 -0
  243. package/src/lib/command-registry.ts +434 -0
  244. package/src/lib/formatter.ts +587 -0
  245. package/src/lib/fs-utils.ts +46 -0
  246. package/src/lib/graphql-name.ts +35 -0
  247. package/src/lib/interactive.ts +634 -0
  248. package/src/lib/introspect.ts +320 -0
  249. package/src/lib/object-info.ts +409 -0
  250. package/src/lib/path-selection.ts +162 -0
  251. package/src/lib/prime-schema.ts +195 -0
  252. package/src/lib/query-builder.ts +193 -0
  253. package/src/lib/query-commands.ts +290 -0
  254. package/src/lib/session.ts +1304 -0
  255. package/src/lib/tokenize.ts +43 -0
  256. package/src/lib/uiapi.ts +176 -0
  257. package/src/lib/validator.ts +143 -0
  258. package/src/lib/variable-promotion.ts +145 -0
  259. package/src/lib/walker.ts +975 -0
  260. package/src/mcp/__tests__/server.spec.ts +155 -0
  261. package/src/mcp/server.ts +38 -0
  262. package/src/mcp/stdio.ts +29 -0
  263. package/src/mcp/tools/__tests__/sf-gql-aggregate.spec.ts +173 -0
  264. package/src/mcp/tools/__tests__/sf-gql-create.spec.ts +235 -0
  265. package/src/mcp/tools/__tests__/sf-gql-delete.spec.ts +194 -0
  266. package/src/mcp/tools/__tests__/sf-gql-detail.spec.ts +246 -0
  267. package/src/mcp/tools/__tests__/sf-gql-discover.spec.ts +320 -0
  268. package/src/mcp/tools/__tests__/sf-gql-list.spec.ts +128 -0
  269. package/src/mcp/tools/__tests__/sf-gql-raw.spec.ts +141 -0
  270. package/src/mcp/tools/__tests__/sf-gql-update.spec.ts +207 -0
  271. package/src/mcp/tools/echo.ts +24 -0
  272. package/src/mcp/tools/sf-gql-aggregate.ts +102 -0
  273. package/src/mcp/tools/sf-gql-create.ts +55 -0
  274. package/src/mcp/tools/sf-gql-delete.ts +49 -0
  275. package/src/mcp/tools/sf-gql-detail.ts +85 -0
  276. package/src/mcp/tools/sf-gql-discover.ts +67 -0
  277. package/src/mcp/tools/sf-gql-list.ts +73 -0
  278. package/src/mcp/tools/sf-gql-raw.ts +56 -0
  279. package/src/mcp/tools/sf-gql-update.ts +55 -0
  280. package/src/mcp/tools/shared/zod-schemas.ts +55 -0
  281. package/tsconfig.json +18 -0
  282. 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
+ });