@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,975 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ /* eslint-disable @typescript-eslint/no-explicit-any -- graphiti traverses untyped schema/introspection JSON; see follow-up to replace with `unknown` + narrowing */
7
+
8
+ import fs from "fs";
9
+ import {
10
+ buildClientSchema,
11
+ buildASTSchema,
12
+ parse,
13
+ printSchema,
14
+ type GraphQLSchema,
15
+ type GraphQLField,
16
+ type GraphQLNamedType,
17
+ type GraphQLArgument,
18
+ type GraphQLInputField,
19
+ isObjectType,
20
+ isInputObjectType,
21
+ isEnumType,
22
+ isUnionType,
23
+ isInterfaceType,
24
+ isScalarType,
25
+ isListType,
26
+ isNonNullType,
27
+ getNamedType,
28
+ type GraphQLOutputType,
29
+ type GraphQLType,
30
+ } from "graphql";
31
+ import { loadIntrospectionResult, getSchemaFilePath, normalizeInstanceUrl } from "./introspect.js";
32
+ import { type OperationType } from "./session.js";
33
+
34
+ // ── Fuzzy matching ───────────────────────────────────────────────────────────
35
+
36
+ function levenshtein(a: string, b: string): number {
37
+ const la = a.length;
38
+ const lb = b.length;
39
+ if (la === 0) return lb;
40
+ if (lb === 0) return la;
41
+ const prev = Array.from({ length: lb + 1 }, (_, i) => i);
42
+ for (let i = 1; i <= la; i++) {
43
+ let prevDiag = prev[0];
44
+ prev[0] = i;
45
+ for (let j = 1; j <= lb; j++) {
46
+ const tmp = prev[j];
47
+ prev[j] = a[i - 1] === b[j - 1] ? prevDiag : 1 + Math.min(prevDiag, prev[j - 1], prev[j]);
48
+ prevDiag = tmp;
49
+ }
50
+ }
51
+ return prev[lb];
52
+ }
53
+
54
+ function findClosestFields(segment: string, available: string[], maxSuggestions = 3): string[] {
55
+ const lower = segment.toLowerCase();
56
+ const threshold = Math.max(3, Math.floor(segment.length * 0.5));
57
+ const scored = available
58
+ .map((name) => ({ name, dist: levenshtein(lower, name.toLowerCase()) }))
59
+ .filter((e) => e.dist <= threshold)
60
+ .sort((a, b) => a.dist - b.dist);
61
+ return scored.slice(0, maxSuggestions).map((e) => e.name);
62
+ }
63
+
64
+ // ── Schema loading ───────────────────────────────────────────────────────────
65
+
66
+ const schemaCache = new Map<string, GraphQLSchema>();
67
+
68
+ /**
69
+ * Salesforce sometimes emits INPUT_OBJECT types with no fields
70
+ * (e.g. `*_SearchOrderBy` types and a few mutation Representation types).
71
+ * That violates the GraphQL spec, so `validateSchema()` rejects the result
72
+ * and downstream `validate(schema, document)` calls throw before they ever
73
+ * see the user's query. Patch the introspection in-memory by adding a
74
+ * synthetic `_placeholder: String` field — keeps the type referenceable
75
+ * without fabricating real fields.
76
+ */
77
+ function patchEmptyInputObjects(data: any): void {
78
+ const types: any[] = data?.__schema?.types ?? [];
79
+ for (const t of types) {
80
+ if (t?.kind === "INPUT_OBJECT" && Array.isArray(t.inputFields) && t.inputFields.length === 0) {
81
+ t.inputFields = [
82
+ {
83
+ name: "_placeholder",
84
+ description:
85
+ "Synthetic placeholder added by graphiti to satisfy GraphQL schema validation; unused.",
86
+ type: { kind: "SCALAR", name: "String", ofType: null },
87
+ defaultValue: null,
88
+ },
89
+ ];
90
+ }
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Builds a GraphQLSchema from an introspection file, using a persisted SDL
96
+ * side-file as a cache to avoid re-running buildClientSchema on every cold start.
97
+ *
98
+ * The SDL file (<cacheKey>.graphql) sits next to the introspection JSON and is
99
+ * regenerated whenever the introspection file is newer.
100
+ */
101
+ function buildSchemaWithSdlCache(introspectionFilePath: string): GraphQLSchema {
102
+ const sdlPath = introspectionFilePath.replace(/\.json$/, ".graphql");
103
+
104
+ try {
105
+ const introspMtime = fs.statSync(introspectionFilePath).mtimeMs;
106
+ if (fs.existsSync(sdlPath)) {
107
+ const sdlMtime = fs.statSync(sdlPath).mtimeMs;
108
+ if (sdlMtime >= introspMtime) {
109
+ const sdl = fs.readFileSync(sdlPath, "utf-8");
110
+ // `assumeValid: true` because `printSchema` drops the body of
111
+ // validated-but-empty input types; the SDL came from a schema we
112
+ // already loaded successfully via the JSON path below.
113
+ return buildASTSchema(parse(sdl), { assumeValid: true });
114
+ }
115
+ }
116
+ } catch {
117
+ // Fall through to full build.
118
+ }
119
+
120
+ const raw = JSON.parse(fs.readFileSync(introspectionFilePath, "utf-8"));
121
+ const data = raw?.data ?? raw;
122
+ patchEmptyInputObjects(data);
123
+ const schema = buildClientSchema(data);
124
+
125
+ // Persist SDL for the next invocation.
126
+ try {
127
+ fs.writeFileSync(sdlPath, printSchema(schema), "utf-8");
128
+ } catch {
129
+ // Non-critical — writable filesystem is not guaranteed.
130
+ }
131
+
132
+ return schema;
133
+ }
134
+
135
+ export class MutationContextError extends Error {
136
+ constructor(message: string) {
137
+ super(message);
138
+ this.name = "MutationContextError";
139
+ }
140
+ }
141
+
142
+ export function getSchema(instanceUrl: string): GraphQLSchema {
143
+ const cacheKey = /^https?:\/\//i.test(instanceUrl)
144
+ ? normalizeInstanceUrl(instanceUrl)
145
+ : instanceUrl; // test-primed direct key
146
+
147
+ const cached = schemaCache.get(cacheKey);
148
+ if (cached) return cached;
149
+
150
+ let schema: GraphQLSchema;
151
+ try {
152
+ const filePath = getSchemaFilePath(instanceUrl);
153
+ schema = buildSchemaWithSdlCache(filePath);
154
+ } catch {
155
+ const introspection = loadIntrospectionResult(instanceUrl);
156
+ const data = introspection?.data ?? introspection;
157
+ schema = buildClientSchema(data);
158
+ }
159
+
160
+ schemaCache.set(cacheKey, schema);
161
+ return schema;
162
+ }
163
+
164
+ export function clearSchemaCache(instanceUrl?: string): void {
165
+ if (instanceUrl) {
166
+ schemaCache.delete(instanceUrl);
167
+ schemaCache.delete(normalizeInstanceUrl(instanceUrl));
168
+ } else {
169
+ schemaCache.clear();
170
+ }
171
+ }
172
+
173
+ export function primeSchemaCache(alias: string, schema: GraphQLSchema): void {
174
+ schemaCache.set(alias, schema);
175
+ }
176
+
177
+ // ── Type info structures ─────────────────────────────────────────────────────
178
+
179
+ export interface FieldInfo {
180
+ name: string;
181
+ typeName: string;
182
+ typeKind: TypeInfo["kind"];
183
+ isNonNull: boolean;
184
+ isList: boolean;
185
+ description: string | null;
186
+ args: ArgInfo[];
187
+ }
188
+
189
+ export interface ArgInfo {
190
+ name: string;
191
+ typeName: string;
192
+ typeKind: string;
193
+ isNonNull: boolean;
194
+ description: string | null;
195
+ defaultValue: string | undefined;
196
+ enumValues?: string[];
197
+ }
198
+
199
+ export interface TypeInfo {
200
+ name: string;
201
+ kind: "OBJECT" | "INPUT_OBJECT" | "ENUM" | "UNION" | "INTERFACE" | "SCALAR";
202
+ description: string | null;
203
+ fields: FieldInfo[];
204
+ inputFields: InputFieldInfo[];
205
+ enumValues: EnumValueInfo[];
206
+ possibleTypes: string[];
207
+ interfaces: string[];
208
+ }
209
+
210
+ export interface InputFieldInfo {
211
+ name: string;
212
+ typeName: string;
213
+ typeKind: string;
214
+ isNonNull: boolean;
215
+ description: string | null;
216
+ defaultValue: string | undefined;
217
+ enumValues?: string[];
218
+ }
219
+
220
+ export interface EnumValueInfo {
221
+ name: string;
222
+ description: string | null;
223
+ }
224
+
225
+ // ── Data Cloud field detection ───────────────────────────────────────────────
226
+
227
+ const DATA_CLOUD_RE = /__dlm$/;
228
+
229
+ /**
230
+ * Returns true for Salesforce Data Cloud (Data Lake Model) objects.
231
+ * These have names ending in `__dlm` (e.g. `ssot__Account__dlm`).
232
+ */
233
+ export function isDataCloudField(field: FieldInfo): boolean {
234
+ return DATA_CLOUD_RE.test(field.name);
235
+ }
236
+
237
+ export function filterDataCloudFields(fields: FieldInfo[], includeDataCloud: boolean): FieldInfo[] {
238
+ if (includeDataCloud) return fields;
239
+ return fields.filter((f) => !isDataCloudField(f));
240
+ }
241
+
242
+ // ── Type formatting ──────────────────────────────────────────────────────────
243
+
244
+ function formatType(type: GraphQLType): string {
245
+ if (isNonNullType(type)) {
246
+ return `${formatType(type.ofType)}!`;
247
+ }
248
+ if (isListType(type)) {
249
+ return `[${formatType(type.ofType)}]`;
250
+ }
251
+ return (type as GraphQLNamedType).name;
252
+ }
253
+
254
+ export function getTypeKind(type: GraphQLNamedType): TypeInfo["kind"] {
255
+ if (isObjectType(type)) return "OBJECT";
256
+ if (isInputObjectType(type)) return "INPUT_OBJECT";
257
+ if (isEnumType(type)) return "ENUM";
258
+ if (isUnionType(type)) return "UNION";
259
+ if (isInterfaceType(type)) return "INTERFACE";
260
+ return "SCALAR";
261
+ }
262
+
263
+ function validateFragmentTarget(
264
+ schema: GraphQLSchema,
265
+ currentType: GraphQLNamedType,
266
+ targetTypeName: string,
267
+ ): GraphQLNamedType {
268
+ const namedType = schema.getType(targetTypeName);
269
+ if (!namedType) {
270
+ throw new Error(`Type "${targetTypeName}" not found in schema`);
271
+ }
272
+
273
+ if (isUnionType(currentType)) {
274
+ const allowed = currentType.getTypes().map((type) => type.name);
275
+ if (!allowed.includes(targetTypeName)) {
276
+ throw new Error(
277
+ `Type "${targetTypeName}" is not a possible type of union ${currentType.name}. Allowed: ${allowed.join(", ")}`,
278
+ );
279
+ }
280
+ return namedType;
281
+ }
282
+
283
+ if (isInterfaceType(currentType)) {
284
+ const allowed = schema.getPossibleTypes(currentType).map((type) => type.name);
285
+ if (!allowed.includes(targetTypeName)) {
286
+ throw new Error(
287
+ `Type "${targetTypeName}" does not implement interface ${currentType.name}. Allowed: ${allowed.join(", ")}`,
288
+ );
289
+ }
290
+ return namedType;
291
+ }
292
+
293
+ if (isObjectType(currentType)) {
294
+ if (currentType.name !== targetTypeName) {
295
+ // Look for relationship fields on this object whose type is a union/interface containing the target type
296
+ const hints: string[] = [];
297
+ const fields = currentType.getFields();
298
+ for (const [fieldName, field] of Object.entries(fields)) {
299
+ const fieldNamedType = getNamedType(field.type);
300
+ if (!fieldNamedType) continue;
301
+ if (isUnionType(fieldNamedType)) {
302
+ const members = fieldNamedType.getTypes().map((t) => t.name);
303
+ if (members.includes(targetTypeName)) {
304
+ hints.push(`${fieldName}/on:${targetTypeName}`);
305
+ }
306
+ } else if (isInterfaceType(fieldNamedType)) {
307
+ const impls = schema.getPossibleTypes(fieldNamedType).map((t) => t.name);
308
+ if (impls.includes(targetTypeName)) {
309
+ hints.push(`${fieldName}/on:${targetTypeName}`);
310
+ }
311
+ }
312
+ }
313
+ let msg = `Type "${targetTypeName}" is not a valid inline-fragment target for object ${currentType.name}.`;
314
+ if (hints.length > 0) {
315
+ msg += `\nDid you mean to use it through a relationship? Try: ${hints.join(" or ")}`;
316
+ }
317
+ throw new Error(msg);
318
+ }
319
+ return namedType;
320
+ }
321
+
322
+ throw new Error(
323
+ `Cannot apply an inline fragment at ${currentType.name} (${getTypeKind(currentType)}).`,
324
+ );
325
+ }
326
+
327
+ // ── Field extraction ─────────────────────────────────────────────────────────
328
+
329
+ function extractArgInfo(arg: GraphQLArgument): ArgInfo {
330
+ const named = getNamedType(arg.type);
331
+ const typeKind = named ? getTypeKind(named) : "SCALAR";
332
+ return {
333
+ name: arg.name,
334
+ typeName: formatType(arg.type),
335
+ typeKind,
336
+ isNonNull: isNonNullType(arg.type),
337
+ description: arg.description ?? null,
338
+ defaultValue: arg.defaultValue !== undefined ? JSON.stringify(arg.defaultValue) : undefined,
339
+ enumValues:
340
+ typeKind === "ENUM" && named && isEnumType(named)
341
+ ? named.getValues().map((v) => v.name)
342
+ : undefined,
343
+ };
344
+ }
345
+
346
+ function extractFieldInfo(field: GraphQLField<any, any>): FieldInfo {
347
+ const named = getNamedType(field.type);
348
+ return {
349
+ name: field.name,
350
+ typeName: formatType(field.type),
351
+ typeKind: named ? getTypeKind(named) : "SCALAR",
352
+ isNonNull: isNonNullType(field.type),
353
+ isList: isListType(isNonNullType(field.type) ? field.type.ofType : field.type),
354
+ description: field.description ?? null,
355
+ args: field.args.map(extractArgInfo),
356
+ };
357
+ }
358
+
359
+ function extractInputFieldInfo(field: GraphQLInputField): InputFieldInfo {
360
+ const named = getNamedType(field.type);
361
+ const typeKind = named ? getTypeKind(named) : "SCALAR";
362
+ return {
363
+ name: field.name,
364
+ typeName: formatType(field.type),
365
+ typeKind,
366
+ isNonNull: isNonNullType(field.type),
367
+ description: field.description ?? null,
368
+ defaultValue: field.defaultValue !== undefined ? JSON.stringify(field.defaultValue) : undefined,
369
+ enumValues:
370
+ typeKind === "ENUM" && named && isEnumType(named)
371
+ ? named.getValues().map((v) => v.name)
372
+ : undefined,
373
+ };
374
+ }
375
+
376
+ // ── Walker result ────────────────────────────────────────────────────────────
377
+
378
+ export interface WalkerResult {
379
+ type: GraphQLNamedType;
380
+ typeName: string;
381
+ kind: TypeInfo["kind"];
382
+ fields: FieldInfo[];
383
+ args: ArgInfo[];
384
+ possibleTypes: string[];
385
+ isLeaf: boolean;
386
+ /** True when inside a mutation result record type where only scalar/value fields are selectable. */
387
+ inMutationRecord: boolean;
388
+ /** Relationship fields hidden from selection in mutation results (shown as [query-only] in ls -l). */
389
+ mutationHiddenFields: FieldInfo[];
390
+ }
391
+
392
+ // ── Deduplication helper ─────────────────────────────────────────────────────
393
+
394
+ function deduplicateByName<T extends { name: string }>(items: T[]): T[] {
395
+ const seen = new Set<string>();
396
+ return items.filter((item) => {
397
+ if (seen.has(item.name)) return false;
398
+ seen.add(item.name);
399
+ return true;
400
+ });
401
+ }
402
+
403
+ // ── Mutation return type field filtering ─────────────────────────────────────
404
+
405
+ /**
406
+ * Returns the set of type names that implement the FieldValue interface.
407
+ * These are the value-wrapper types (StringValue, PicklistValue, etc.)
408
+ * that ARE available in mutation return payloads.
409
+ *
410
+ * Falls back to name-based detection (*Value pattern) if the interface
411
+ * is absent from the schema.
412
+ */
413
+ function getFieldValueTypeNames(schema: GraphQLSchema): Set<string> {
414
+ const fieldValueInterface = schema.getType("FieldValue");
415
+ if (fieldValueInterface && isInterfaceType(fieldValueInterface)) {
416
+ return new Set(schema.getPossibleTypes(fieldValueInterface).map((t) => t.name));
417
+ }
418
+ // Fallback: match common Salesforce value wrapper naming convention
419
+ const names = new Set<string>();
420
+ const typeMap = schema.getTypeMap();
421
+ for (const typeName of Object.keys(typeMap)) {
422
+ if (/Value$/.test(typeName) && !typeName.startsWith("__")) {
423
+ names.add(typeName);
424
+ }
425
+ }
426
+ return names;
427
+ }
428
+
429
+ /**
430
+ * Returns true when the given named type represents a relationship/connection
431
+ * that is NOT available in mutation return payloads. Scalars, enums, and types
432
+ * that implement the FieldValue interface are allowed; everything else is blocked.
433
+ */
434
+ function isRelationshipType(
435
+ schema: GraphQLSchema,
436
+ type: GraphQLNamedType,
437
+ fieldValueTypes: Set<string>,
438
+ ): boolean {
439
+ if (isScalarType(type) || isEnumType(type)) return false;
440
+ if (fieldValueTypes.has(type.name)) return false;
441
+ // Object types not in the FieldValue set are SObjects, connections, or aggregates
442
+ return true;
443
+ }
444
+
445
+ // ── Root type resolution ─────────────────────────────────────────────────────
446
+
447
+ export function getRootType(schema: GraphQLSchema, operation: OperationType): GraphQLNamedType {
448
+ const rootType = operation === "mutation" ? schema.getMutationType() : schema.getQueryType();
449
+
450
+ if (!rootType) {
451
+ throw new Error(`Schema has no ${operation} type`);
452
+ }
453
+ return rootType;
454
+ }
455
+
456
+ export function getRootFields(schema: GraphQLSchema, operation: OperationType): FieldInfo[] {
457
+ const rootType = getRootType(schema, operation);
458
+ if (!isObjectType(rootType)) return [];
459
+ return Object.values(rootType.getFields()).map(extractFieldInfo);
460
+ }
461
+
462
+ // ── Path resolution ──────────────────────────────────────────────────────────
463
+
464
+ /**
465
+ * Walks the schema graph along a path and returns what's available at the end.
466
+ * Handles regular fields, inline fragment segments [TypeName], and resolves
467
+ * through NonNull/List wrappers.
468
+ *
469
+ * Returns the args of the *last* field in the path (not the terminal type's fields' args).
470
+ */
471
+ export function resolvePath(
472
+ schema: GraphQLSchema,
473
+ operation: OperationType,
474
+ pathSegments: string[],
475
+ ): WalkerResult {
476
+ let currentType: GraphQLNamedType = getRootType(schema, operation);
477
+ let lastFieldArgs: ArgInfo[] = [];
478
+ // Tracks whether we've entered the record-level type inside a mutation payload.
479
+ // Relationship fields (SObjects, unions, connections) are not available in
480
+ // mutation results once we're inside the Record type.
481
+ let inMutationRecord = false;
482
+
483
+ for (const segment of pathSegments) {
484
+ if ((segment.startsWith("[") && segment.endsWith("]")) || segment.startsWith("on:")) {
485
+ const typeName = segment.startsWith("on:") ? segment.slice(3) : segment.slice(1, -1);
486
+ const namedType = validateFragmentTarget(schema, currentType, typeName);
487
+ currentType = namedType;
488
+ lastFieldArgs = [];
489
+ continue;
490
+ }
491
+
492
+ if (segment.startsWith("#")) {
493
+ // Named fragment reference — resolve to its target type
494
+ // The caller should handle fragment cursor paths by looking up the fragment's onType
495
+ const typeName = segment.slice(1);
496
+ const namedType = schema.getType(typeName);
497
+ if (!namedType) {
498
+ throw new Error(`Fragment target type "${typeName}" not found in schema`);
499
+ }
500
+ currentType = namedType;
501
+ lastFieldArgs = [];
502
+ continue;
503
+ }
504
+
505
+ // Regular field — look it up on the current object/interface type
506
+ if (!isObjectType(currentType) && !isInterfaceType(currentType)) {
507
+ if (isUnionType(currentType)) {
508
+ const members = currentType.getTypes().map((t) => t.name);
509
+ throw new Error(
510
+ `Cannot select field "${segment}" on ${currentType.name} (UNION). ` +
511
+ `Use inline fragment syntax: on:${members[0]}/${segment}\n` +
512
+ `Possible types: ${members.join(", ")}\n` +
513
+ `Example: select .../on:${members[0]}/${segment}`,
514
+ );
515
+ }
516
+ throw new Error(
517
+ `Cannot select field "${segment}" on ${currentType.name} (${getTypeKind(currentType)}). Only OBJECT and INTERFACE types have fields.`,
518
+ );
519
+ }
520
+
521
+ const fields = currentType.getFields();
522
+ const field = fields[segment];
523
+ if (!field) {
524
+ const available = Object.keys(fields);
525
+ const suggestions = findClosestFields(segment, available);
526
+ let hint =
527
+ suggestions.length > 0
528
+ ? `Did you mean: ${suggestions.join(", ")}?`
529
+ : `Available: ${available.slice(0, 10).join(", ")}${available.length > 10 ? "..." : ""}`;
530
+ if (operation === "query" && /(?:Create|Update|Delete)$/.test(segment)) {
531
+ hint += `\nHint: "${segment}" looks like a mutation. Create a mutation session with: new <org> --name <name> --mutation`;
532
+ }
533
+ throw new Error(`Field "${segment}" not found on type ${currentType.name}. ${hint}`);
534
+ }
535
+
536
+ lastFieldArgs = field.args.map(extractArgInfo);
537
+ const namedReturnType = getNamedType(field.type);
538
+ if (!namedReturnType) {
539
+ throw new Error(`Could not resolve return type of field ${currentType.name}.${segment}`);
540
+ }
541
+
542
+ // Detect mutation record context: when navigating FROM a Payload type INTO the record type
543
+ if (operation === "mutation") {
544
+ if (currentType.name.endsWith("Payload") && !namedReturnType.name.endsWith("Payload")) {
545
+ inMutationRecord = true;
546
+ } else if (inMutationRecord) {
547
+ // Inside mutation record: block navigation into relationship/connection types
548
+ if (isRelationshipType(schema, namedReturnType, getFieldValueTypeNames(schema))) {
549
+ throw new MutationContextError(
550
+ `"${segment}" is not available in mutation results. ` +
551
+ `Only scalar fields and value wrappers (e.g. Name/, Status/) can be selected.\n` +
552
+ `Tip: query the record by Id after the mutation to fetch related data.`,
553
+ );
554
+ }
555
+ }
556
+ }
557
+
558
+ currentType = namedReturnType;
559
+ }
560
+
561
+ // Build the result based on the terminal type
562
+ const kind = getTypeKind(currentType);
563
+ const fields: FieldInfo[] = [];
564
+ const mutationHiddenFields: FieldInfo[] = [];
565
+ const possibleTypes: string[] = [];
566
+
567
+ if (isObjectType(currentType) || isInterfaceType(currentType)) {
568
+ const allFields = Object.values(currentType.getFields()).map(extractFieldInfo);
569
+ if (inMutationRecord) {
570
+ const fieldValueTypes = getFieldValueTypeNames(schema);
571
+ for (const f of allFields) {
572
+ const namedTypeName = f.typeName.replace(/[![\]]/g, "");
573
+ const ft = schema.getType(namedTypeName);
574
+ if (!ft || !isRelationshipType(schema, ft, fieldValueTypes)) {
575
+ fields.push(f);
576
+ } else {
577
+ mutationHiddenFields.push(f);
578
+ }
579
+ }
580
+ } else {
581
+ fields.push(...allFields);
582
+ }
583
+ }
584
+
585
+ if (!inMutationRecord) {
586
+ if (isUnionType(currentType)) {
587
+ possibleTypes.push(...currentType.getTypes().map((t) => t.name));
588
+ }
589
+
590
+ if (isInterfaceType(currentType)) {
591
+ const impls = schema.getPossibleTypes(currentType);
592
+ possibleTypes.push(...impls.map((t) => t.name));
593
+ }
594
+ }
595
+
596
+ const isLeaf = isScalarType(currentType) || isEnumType(currentType);
597
+
598
+ const deduplicatedArgs = deduplicateByName(lastFieldArgs);
599
+
600
+ return {
601
+ type: currentType,
602
+ typeName: currentType.name,
603
+ kind,
604
+ fields,
605
+ args: deduplicatedArgs,
606
+ possibleTypes,
607
+ isLeaf,
608
+ inMutationRecord,
609
+ mutationHiddenFields,
610
+ };
611
+ }
612
+
613
+ // ── Resolve a field by name on a given parent path ───────────────────────────
614
+
615
+ /**
616
+ * Given a parent path + field name, resolves the field's schema info.
617
+ * Used to determine whether a field returns an object type (needs sub-selections)
618
+ * or is a leaf.
619
+ */
620
+ export function resolveFieldOnPath(
621
+ schema: GraphQLSchema,
622
+ operation: OperationType,
623
+ parentPath: string[],
624
+ fieldName: string,
625
+ ): WalkerResult {
626
+ return resolvePath(schema, operation, [...parentPath, fieldName]);
627
+ }
628
+
629
+ export function getFragmentTargets(schema: GraphQLSchema, type: GraphQLNamedType): string[] {
630
+ if (isUnionType(type)) {
631
+ return type.getTypes().map((item) => item.name);
632
+ }
633
+ if (isInterfaceType(type)) {
634
+ return schema.getPossibleTypes(type).map((item) => item.name);
635
+ }
636
+ if (isObjectType(type)) {
637
+ return [type.name];
638
+ }
639
+ return [];
640
+ }
641
+
642
+ // ── Type inspection ──────────────────────────────────────────────────────────
643
+
644
+ export function inspectType(schema: GraphQLSchema, typeName: string): TypeInfo {
645
+ const type = schema.getType(typeName);
646
+ if (!type) {
647
+ throw new Error(`Type "${typeName}" not found in schema`);
648
+ }
649
+
650
+ const info: TypeInfo = {
651
+ name: type.name,
652
+ kind: getTypeKind(type),
653
+ description: ("description" in type ? type.description : null) ?? null,
654
+ fields: [],
655
+ inputFields: [],
656
+ enumValues: [],
657
+ possibleTypes: [],
658
+ interfaces: [],
659
+ };
660
+
661
+ if (isObjectType(type) || isInterfaceType(type)) {
662
+ info.fields = Object.values(type.getFields()).map(extractFieldInfo);
663
+ if (isObjectType(type)) {
664
+ info.interfaces = type.getInterfaces().map((i) => i.name);
665
+ }
666
+ }
667
+
668
+ if (isInputObjectType(type)) {
669
+ info.inputFields = Object.values(type.getFields()).map(extractInputFieldInfo);
670
+ }
671
+
672
+ if (isEnumType(type)) {
673
+ info.enumValues = type.getValues().map((v) => ({
674
+ name: v.name,
675
+ description: v.description ?? null,
676
+ }));
677
+ }
678
+
679
+ if (isUnionType(type)) {
680
+ info.possibleTypes = type.getTypes().map((t) => t.name);
681
+ }
682
+
683
+ if (isInterfaceType(type)) {
684
+ const impls = schema.getPossibleTypes(type);
685
+ info.possibleTypes = impls.map((t) => t.name);
686
+ }
687
+
688
+ return info;
689
+ }
690
+
691
+ // ── Input type walking ───────────────────────────────────────────────────────
692
+
693
+ export interface InputWalkerResult {
694
+ type: GraphQLNamedType;
695
+ typeName: string;
696
+ kind: TypeInfo["kind"];
697
+ inputFields: InputFieldInfo[];
698
+ enumValues: EnumValueInfo[];
699
+ isLeaf: boolean;
700
+ isList: boolean;
701
+ isNonNull: boolean;
702
+ }
703
+
704
+ /**
705
+ * Walks an INPUT_OBJECT type structure along a path.
706
+ * Numeric segments index into list types (unwrapping [T] → T).
707
+ * Returns info about the terminal type: its input fields if navigable,
708
+ * or isLeaf:true for SCALAR/ENUM.
709
+ */
710
+ export function resolveInputPath(
711
+ schema: GraphQLSchema,
712
+ inputTypeName: string,
713
+ pathSegments: string[],
714
+ ): InputWalkerResult {
715
+ const startType = schema.getType(inputTypeName);
716
+ if (!startType) throw new Error(`Type "${inputTypeName}" not found in schema`);
717
+
718
+ let currentType: GraphQLNamedType = getNamedType(startType) ?? (startType as GraphQLNamedType);
719
+ let currentIsList = isListType(isNonNullType(startType) ? (startType as any).ofType : startType);
720
+ let currentIsNonNull = isNonNullType(startType);
721
+
722
+ for (const segment of pathSegments) {
723
+ // Numeric segments index into a list → unwrap to the item type
724
+ if (/^\d+$/.test(segment)) {
725
+ if (!currentIsList && !isInputObjectType(currentType)) {
726
+ throw new Error(`Cannot index into non-list type ${currentType.name} with "${segment}".`);
727
+ }
728
+ currentIsList = false;
729
+ currentIsNonNull = false;
730
+ continue;
731
+ }
732
+
733
+ if (!isInputObjectType(currentType)) {
734
+ throw new Error(
735
+ `Cannot navigate into field "${segment}" on ${currentType.name} (${getTypeKind(currentType)}). ` +
736
+ `Only INPUT_OBJECT types have navigable fields.`,
737
+ );
738
+ }
739
+
740
+ const fields = currentType.getFields();
741
+ const field = fields[segment];
742
+ if (!field) {
743
+ const available = Object.keys(fields);
744
+ const suggestions = findClosestFields(segment, available);
745
+ const hint =
746
+ suggestions.length > 0
747
+ ? `Did you mean: ${suggestions.join(", ")}?`
748
+ : `Available: ${available.slice(0, 10).join(", ")}${available.length > 10 ? "..." : ""}`;
749
+ throw new Error(`Field "${segment}" not found on input type ${currentType.name}. ${hint}`);
750
+ }
751
+
752
+ const rawType = field.type;
753
+ const namedType = getNamedType(rawType);
754
+ if (!namedType) {
755
+ throw new Error(`Could not resolve type of input field ${currentType.name}.${segment}`);
756
+ }
757
+ currentIsNonNull = isNonNullType(rawType);
758
+ currentIsList = isListType(isNonNullType(rawType) ? rawType.ofType : rawType);
759
+ currentType = namedType;
760
+ }
761
+
762
+ const kind = getTypeKind(currentType);
763
+ const isLeaf = isScalarType(currentType) || isEnumType(currentType);
764
+ const inputFields: InputFieldInfo[] = [];
765
+ const enumValues: EnumValueInfo[] = [];
766
+
767
+ if (isInputObjectType(currentType)) {
768
+ inputFields.push(
769
+ ...deduplicateByName(Object.values(currentType.getFields()).map(extractInputFieldInfo)),
770
+ );
771
+ }
772
+
773
+ if (isEnumType(currentType)) {
774
+ enumValues.push(
775
+ ...deduplicateByName(
776
+ currentType.getValues().map((v) => ({
777
+ name: v.name,
778
+ description: v.description ?? null,
779
+ })),
780
+ ),
781
+ );
782
+ }
783
+
784
+ return {
785
+ type: currentType,
786
+ typeName: currentType.name,
787
+ kind,
788
+ inputFields,
789
+ enumValues,
790
+ isLeaf: isLeaf && !currentIsList,
791
+ isList: currentIsList,
792
+ isNonNull: currentIsNonNull,
793
+ };
794
+ }
795
+
796
+ /**
797
+ * Resolves a single argument by name from a WalkerResult and returns
798
+ * its type info for navigation into @args/.
799
+ */
800
+ export function resolveArgByName(
801
+ schema: GraphQLSchema,
802
+ walkerResult: WalkerResult,
803
+ argName: string,
804
+ ): { typeName: string; isNonNull: boolean; isList: boolean; typeKind: string } {
805
+ const arg = walkerResult.args.find((a) => a.name === argName);
806
+ if (!arg) {
807
+ const available = walkerResult.args.map((a) => a.name);
808
+ const suggestions = findClosestFields(argName, available);
809
+ const hint =
810
+ suggestions.length > 0
811
+ ? `Did you mean: ${suggestions.join(", ")}?`
812
+ : `Available args: ${available.join(", ")}`;
813
+ throw new Error(`Argument "${argName}" not found on field at this path. ${hint}`);
814
+ }
815
+
816
+ const rawTypeName = arg.typeName.replace(/[![\]]/g, "");
817
+ const isList = arg.typeName.includes("[");
818
+ return {
819
+ typeName: rawTypeName,
820
+ isNonNull: arg.isNonNull,
821
+ isList,
822
+ typeKind: arg.typeKind,
823
+ };
824
+ }
825
+
826
+ // ── Raw field type lookup (preserves NonNull/List wrappers) ──────────────────
827
+
828
+ /**
829
+ * Resolves the parent type for a schema path and returns the raw
830
+ * GraphQLField for the given field name. Used by codegen to read the
831
+ * field's output type (with NonNull/List wrappers) and its description
832
+ * directly from the schema.
833
+ *
834
+ * Returns null if the parent path can't be resolved or the field doesn't exist.
835
+ */
836
+ function resolveParentField(
837
+ schema: GraphQLSchema,
838
+ operation: OperationType,
839
+ parentPath: string[],
840
+ fieldName: string,
841
+ ): GraphQLField<any, any> | null {
842
+ try {
843
+ const parentResult = resolvePath(schema, operation, parentPath);
844
+ const parentType = parentResult.type;
845
+ if (!isObjectType(parentType) && !isInterfaceType(parentType)) return null;
846
+ return parentType.getFields()[fieldName] ?? null;
847
+ } catch {
848
+ return null;
849
+ }
850
+ }
851
+
852
+ export function getRawFieldType(
853
+ schema: GraphQLSchema,
854
+ operation: OperationType,
855
+ parentPath: string[],
856
+ fieldName: string,
857
+ ): GraphQLOutputType | null {
858
+ return resolveParentField(schema, operation, parentPath, fieldName)?.type ?? null;
859
+ }
860
+
861
+ export function getFieldDescription(
862
+ schema: GraphQLSchema,
863
+ operation: OperationType,
864
+ parentPath: string[],
865
+ fieldName: string,
866
+ ): string | null {
867
+ return resolveParentField(schema, operation, parentPath, fieldName)?.description ?? null;
868
+ }
869
+
870
+ // ── Search ───────────────────────────────────────────────────────────────────
871
+
872
+ export interface SearchResult {
873
+ typeName: string;
874
+ kind: string;
875
+ fieldName?: string;
876
+ fieldType?: string;
877
+ description?: string | null;
878
+ }
879
+
880
+ export interface SearchMatcher {
881
+ test(input: string): boolean;
882
+ }
883
+
884
+ /**
885
+ * Splits a CamelCase/PascalCase identifier into word segments.
886
+ * e.g. "AccountId" → ["Account", "Id"], "CSNDesktopTask" → ["CSN", "Desktop", "Task"]
887
+ */
888
+ function splitCamelCase(name: string): string[] {
889
+ return name
890
+ .replace(/([a-z0-9])([A-Z])/g, "$1\0$2")
891
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1\0$2")
892
+ .split(/[\0_]/)
893
+ .filter(Boolean);
894
+ }
895
+
896
+ /**
897
+ * Builds a SearchMatcher from a plain-text search string.
898
+ *
899
+ * The pattern is split by whitespace into terms. Each term is
900
+ * prefix-matched (case-insensitive) against CamelCase word segments
901
+ * of the field name. "Id" matches "AccountId" but not "Hide".
902
+ */
903
+ export function parseSearchTerms(pattern: string): SearchMatcher {
904
+ const terms = pattern.split(/\s+/).filter(Boolean);
905
+ if (terms.length === 0) return { test: () => true };
906
+
907
+ const termRegexes = terms.map((t) => new RegExp(`^${t}`, "i"));
908
+
909
+ return {
910
+ test(input: string): boolean {
911
+ if (termRegexes.some((re) => re.test(input))) return true;
912
+ const segments = splitCamelCase(input);
913
+ return termRegexes.some((re) => segments.some((seg) => re.test(seg)));
914
+ },
915
+ };
916
+ }
917
+
918
+ /**
919
+ * Builds a SearchMatcher from a regex pattern string.
920
+ *
921
+ * Supports `/pattern/flags` syntax for explicit flags.
922
+ * Without delimiters, defaults to case-insensitive matching.
923
+ */
924
+ export function parseSearchRegex(pattern: string): SearchMatcher {
925
+ const delimited = pattern.match(/^\/(.+)\/([gimsuy]*)$/);
926
+ if (delimited) {
927
+ return new RegExp(delimited[1], delimited[2]);
928
+ }
929
+ return new RegExp(pattern, "i");
930
+ }
931
+
932
+ export function searchSchema(
933
+ schema: GraphQLSchema,
934
+ pattern: string,
935
+ maxResults = 50,
936
+ ): SearchResult[] {
937
+ const regex = parseSearchRegex(pattern);
938
+ const results: SearchResult[] = [];
939
+ const typeMap = schema.getTypeMap();
940
+
941
+ for (const [typeName, type] of Object.entries(typeMap)) {
942
+ if (typeName.startsWith("__")) continue;
943
+ if (results.length >= maxResults) break;
944
+
945
+ if (regex.test(typeName)) {
946
+ results.push({
947
+ typeName,
948
+ kind: getTypeKind(type as GraphQLNamedType),
949
+ description: ("description" in type ? type.description : null) ?? null,
950
+ });
951
+ }
952
+
953
+ // Search fields
954
+ if (
955
+ (isObjectType(type) || isInterfaceType(type) || isInputObjectType(type)) &&
956
+ results.length < maxResults
957
+ ) {
958
+ const fields = type.getFields();
959
+ for (const [fieldName, field] of Object.entries(fields)) {
960
+ if (results.length >= maxResults) break;
961
+ if (regex.test(fieldName)) {
962
+ results.push({
963
+ typeName,
964
+ kind: getTypeKind(type),
965
+ fieldName,
966
+ fieldType: formatType((field as any).type),
967
+ description: field.description ?? null,
968
+ });
969
+ }
970
+ }
971
+ }
972
+ }
973
+
974
+ return results;
975
+ }