@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,108 @@
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, vi } from "vitest";
9
+ import { makeNoopPrimeDeps } from "../../__tests__/helpers/prime-deps.js";
10
+ import * as sessionModule from "../../lib/session.js";
11
+ import { primeSchemaCache } from "../../lib/walker.js";
12
+ import { buildMutation } from "../build-mutation.js";
13
+
14
+ vi.mock("../../lib/session.js", async (importOriginal) => {
15
+ const actual = await importOriginal<typeof sessionModule>();
16
+ return { ...actual, createSession: vi.fn(actual.createSession) };
17
+ });
18
+
19
+ const SCHEMA_SDL = `
20
+ type Query { _placeholder: Boolean }
21
+ type Mutation { uiapi(input: UIAPIMutationsInput): UIAPIMutations! }
22
+ input UIAPIMutationsInput { allOrNone: Boolean }
23
+ type UIAPIMutations {
24
+ AccountCreate(input: AccountCreateInput!): AccountCreatePayload
25
+ AccountUpdate(input: AccountUpdateInput!): AccountUpdatePayload
26
+ }
27
+ input AccountCreateInput { Account: AccountCreateRepresentation! }
28
+ input AccountCreateRepresentation { Name: String }
29
+ type AccountCreatePayload { Record: Account }
30
+ input AccountUpdateInput { Account: AccountUpdateRepresentation! }
31
+ input AccountUpdateRepresentation { Name: String }
32
+ type AccountUpdatePayload { Record: Account }
33
+ type Account { Id: ID!, Name: StringValue }
34
+ type StringValue { value: String }
35
+ `;
36
+
37
+ const ORG = "test-build-mutation";
38
+ const ORG_URL = "https://test-build-mutation.my.salesforce.com";
39
+ const SCHEMA = buildSchema(SCHEMA_SDL);
40
+ primeSchemaCache(ORG, SCHEMA);
41
+ primeSchemaCache(ORG_URL, SCHEMA);
42
+
43
+ const noopPrimeDeps = () => makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
44
+
45
+ describe("intent/build-mutation", () => {
46
+ it("op=Create renders mutation Create<Object> with <Object>CreateInput!", async () => {
47
+ const result = await buildMutation(
48
+ { org: ORG, object: "Account", returnFields: ["Id", "Name"] },
49
+ "Create",
50
+ noopPrimeDeps(),
51
+ );
52
+ expect(result.query).toMatch(/mutation CreateAccount/);
53
+ expect(result.query).toMatch(/\$input:\s*AccountCreateInput!/);
54
+ expect(result.query).toMatch(/AccountCreate\(input:\s*\$input\)/);
55
+ expect(result.query).toMatch(/Name\s*\{\s*value/s);
56
+ expect(result.variables).toEqual([
57
+ { name: "input", type: "AccountCreateInput!", required: true },
58
+ ]);
59
+ });
60
+
61
+ it("op=Update renders mutation Update<Object> with <Object>UpdateInput!", async () => {
62
+ const result = await buildMutation(
63
+ { org: ORG, object: "Account", returnFields: ["Id", "Name"] },
64
+ "Update",
65
+ noopPrimeDeps(),
66
+ );
67
+ expect(result.query).toMatch(/mutation UpdateAccount/);
68
+ expect(result.query).toMatch(/\$input:\s*AccountUpdateInput!/);
69
+ expect(result.query).toMatch(/AccountUpdate\(input:\s*\$input\)/);
70
+ expect(result.query).toMatch(/Name\s*\{\s*value/s);
71
+ expect(result.variables).toEqual([
72
+ { name: "input", type: "AccountUpdateInput!", required: true },
73
+ ]);
74
+ });
75
+
76
+ it('op=Update defaults returnFields to ["Id"]', async () => {
77
+ const result = await buildMutation({ org: ORG, object: "Account" }, "Update", noopPrimeDeps());
78
+ expect(result.query).toMatch(/\bId\b/);
79
+ expect(result.query).not.toMatch(/Name\s*\{/);
80
+ });
81
+
82
+ it("op=Update with custom inputVariable strips leading $ and declares it", async () => {
83
+ const result = await buildMutation(
84
+ { org: ORG, object: "Account", inputVariable: "$myInput" },
85
+ "Update",
86
+ noopPrimeDeps(),
87
+ );
88
+ expect(result.query).toMatch(/\$myInput:\s*AccountUpdateInput!/);
89
+ expect(result.query).toMatch(/input:\s*\$myInput/);
90
+ expect(result.query).not.toMatch(/\$\$myInput/);
91
+ });
92
+
93
+ it("op=Update with custom operationName uses it", async () => {
94
+ const result = await buildMutation(
95
+ { org: ORG, object: "Account", operationName: "MutateAccount" },
96
+ "Update",
97
+ noopPrimeDeps(),
98
+ );
99
+ expect(result.query).toMatch(/mutation MutateAccount/);
100
+ });
101
+
102
+ it("threads instanceUrl as 3rd arg to createSession", async () => {
103
+ const spy = vi.mocked(sessionModule.createSession);
104
+ spy.mockClear();
105
+ await buildMutation({ org: ORG, object: "Account" }, "Update", noopPrimeDeps());
106
+ expect(spy).toHaveBeenCalledWith(ORG, "mutation", ORG_URL);
107
+ });
108
+ });
@@ -0,0 +1,55 @@
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, selectLeaf } from "../../lib/session.js";
10
+ import { buildOutput } from "../build-output.js";
11
+
12
+ const schema = buildSchema(`
13
+ type Query { ping: String }
14
+ `);
15
+
16
+ describe("intent/build-output", () => {
17
+ it("renders query, no warnings on valid query", () => {
18
+ const session = createSession("test", "query");
19
+ selectLeaf(session, ["ping"]);
20
+ const out = buildOutput(session, schema);
21
+ expect(out.query).toMatch(/ping/);
22
+ expect(out.warnings).toEqual([]);
23
+ expect(typeof out.types).toBe("string");
24
+ });
25
+
26
+ it("threads priming note into warnings", () => {
27
+ const session = createSession("test", "query");
28
+ selectLeaf(session, ["ping"]);
29
+ const out = buildOutput(session, schema, 'Note: Primed schema cache for "a" (took 12ms)');
30
+ expect(out.warnings).toHaveLength(1);
31
+ expect(out.warnings[0]!).toMatch(/Primed schema cache/);
32
+ });
33
+
34
+ it("emits Validation: prefix for query-level errors", () => {
35
+ const session = createSession("test", "query");
36
+ selectLeaf(session, ["doesNotExist"]);
37
+ const out = buildOutput(session, schema);
38
+ expect(out.warnings.some((w) => w.startsWith("Validation:"))).toBe(true);
39
+ });
40
+
41
+ it("surfaces codegen failure as Codegen: warning + placeholder types", () => {
42
+ const session = createSession("test", "query");
43
+ selectLeaf(session, ["ping"]);
44
+ // Trigger: assign non-string values to session.id and session.name so that
45
+ // generateTypes' internal toPascalCase call throws. If codegen ever guards
46
+ // these reads with String(...), this trigger will stop firing and the test
47
+ // will fail loudly — replace the trigger with another path that makes
48
+ // generateTypes throw (e.g., an unsupported codegen language option).
49
+ (session as unknown as { id: unknown }).id = { broken: true };
50
+ (session as unknown as { name: unknown }).name = { broken: true };
51
+ const out = buildOutput(session, schema);
52
+ expect(out.types).toMatch(/\/\/ Type generation failed:/);
53
+ expect(out.warnings.some((w) => w.startsWith("Codegen:"))).toBe(true);
54
+ });
55
+ });
@@ -0,0 +1,153 @@
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, vi } from "vitest";
9
+ import { makeNoopPrimeDeps } from "../../__tests__/helpers/prime-deps.js";
10
+ import * as sessionModule from "../../lib/session.js";
11
+ import { primeSchemaCache } from "../../lib/walker.js";
12
+ import { buildRaw } from "../build-raw.js";
13
+
14
+ vi.mock("../../lib/session.js", async (importOriginal) => {
15
+ const actual = await importOriginal<typeof sessionModule>();
16
+ return { ...actual, createSession: vi.fn(actual.createSession) };
17
+ });
18
+
19
+ const SCHEMA_SDL = `
20
+ type Query { uiapi: UIAPI! }
21
+ type UIAPI { query: RecordQuery!, aggregate: RecordQueryAggregate! }
22
+ type RecordQuery {
23
+ Case(first: Int, after: String, where: Case_Filter): CaseConnection!
24
+ }
25
+ type RecordQueryAggregate {
26
+ Case(first: Int, where: Case_Filter, groupBy: Case_GroupBy): CaseAggregateConnection
27
+ }
28
+ input Case_Filter { Status: PicklistOperators }
29
+ input Case_GroupBy { Status: GroupByClause }
30
+ input GroupByClause { group: Boolean }
31
+ input PicklistOperators { eq: String }
32
+ type CaseConnection { edges: [CaseEdge!]!, pageInfo: PageInfo! }
33
+ type CaseEdge { node: Case! }
34
+ type CaseAggregateConnection { edges: [CaseAggregateEdge!]!, pageInfo: PageInfo! }
35
+ type CaseAggregateEdge { node: CaseResult! }
36
+ type CaseResult { aggregate: CaseAggregate }
37
+ type CaseAggregate { Id: IDAggregate }
38
+ type IDAggregate { count: Int }
39
+ type PageInfo { hasNextPage: Boolean!, endCursor: String }
40
+ type Case { Id: ID!, Subject: StringValue, Status: StringValue }
41
+ type StringValue { value: String }
42
+
43
+ type Mutation { uiapi: UIAPIMut! }
44
+ type UIAPIMut { placeholder: Boolean }
45
+ `;
46
+
47
+ const ORG = "test-raw";
48
+ const ORG_URL = "https://test-raw.my.salesforce.com";
49
+ const SCHEMA = buildSchema(SCHEMA_SDL);
50
+ primeSchemaCache(ORG, SCHEMA);
51
+ primeSchemaCache(ORG_URL, SCHEMA);
52
+ const deps = () => makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
53
+
54
+ describe("intent/build-raw", () => {
55
+ it("renders a query from select + set commands", async () => {
56
+ const out = await buildRaw(
57
+ {
58
+ org: ORG,
59
+ commands: [
60
+ "select uiapi/query/Case/edges/node/Subject/value",
61
+ "set uiapi/query/Case first=5",
62
+ ],
63
+ },
64
+ deps(),
65
+ );
66
+ expect(out.query).toMatch(/Subject\s*\{\s*value\s*\}/);
67
+ expect(out.query).toMatch(/first:\s*5/);
68
+ expect(out.query).toMatch(/\bquery\b/);
69
+ });
70
+
71
+ it("uses the mutation root when operation is mutation", async () => {
72
+ const out = await buildRaw(
73
+ { org: ORG, commands: ["select uiapi/placeholder"], operation: "mutation" },
74
+ deps(),
75
+ );
76
+ expect(out.query).toMatch(/\bmutation\b/);
77
+ });
78
+
79
+ it("uses the aggregate root when operation is aggregate", async () => {
80
+ const out = await buildRaw(
81
+ {
82
+ org: ORG,
83
+ commands: ["select uiapi/aggregate/Case/edges/node/aggregate/Id/count"],
84
+ operation: "aggregate",
85
+ },
86
+ deps(),
87
+ );
88
+ // Aggregate operations render under the query keyword against uiapi.aggregate.
89
+ expect(out.query).toMatch(/\bquery\s+RawAggregate\b/);
90
+ expect(out.query).toMatch(/aggregate\s*\{[\s\S]*Id\s*\{[\s\S]*count/);
91
+ });
92
+
93
+ it("honors typeName as the operation name", async () => {
94
+ const out = await buildRaw(
95
+ { org: ORG, commands: ["select uiapi/query/Case/edges/node/Id"], typeName: "MyRaw" },
96
+ deps(),
97
+ );
98
+ expect(out.query).toMatch(/\bquery\s+MyRaw\b/);
99
+ });
100
+
101
+ it("defaults operation name to RawQuery", async () => {
102
+ const out = await buildRaw(
103
+ { org: ORG, commands: ["select uiapi/query/Case/edges/node/Id"] },
104
+ deps(),
105
+ );
106
+ expect(out.query).toMatch(/\bquery\s+RawQuery\b/);
107
+ });
108
+
109
+ it("fails fast: a bad command throws with its index and text", async () => {
110
+ await expect(
111
+ buildRaw(
112
+ {
113
+ org: ORG,
114
+ commands: ["select uiapi/query/Case/edges/node/Id", "frobnicate the widget"],
115
+ },
116
+ deps(),
117
+ ),
118
+ ).rejects.toThrow(/command 1 \(frobnicate the widget\).*unknown command 'frobnicate'/s);
119
+ });
120
+
121
+ it("rejects an empty commands array", async () => {
122
+ await expect(buildRaw({ org: ORG, commands: [] }, deps())).rejects.toThrow(
123
+ /at least one command/,
124
+ );
125
+ });
126
+
127
+ it("returns a ToolOutput with the standard shape", async () => {
128
+ const out = await buildRaw(
129
+ { org: ORG, commands: ["select uiapi/query/Case/edges/node/Id"] },
130
+ deps(),
131
+ );
132
+ expect(out).toHaveProperty("query");
133
+ expect(out).toHaveProperty("variables");
134
+ expect(out).toHaveProperty("types");
135
+ expect(Array.isArray(out.warnings)).toBe(true);
136
+ });
137
+
138
+ it("rejects a typeName that is not a valid GraphQL Name", async () => {
139
+ await expect(
140
+ buildRaw(
141
+ { org: ORG, commands: ["select uiapi/query/Case/edges/node/Id"], typeName: "has spaces" },
142
+ deps(),
143
+ ),
144
+ ).rejects.toThrow(/buildRaw: typeName 'has spaces' is not a valid GraphQL Name/);
145
+ });
146
+
147
+ it("threads instanceUrl as 3rd arg to createSession", async () => {
148
+ const spy = vi.mocked(sessionModule.createSession);
149
+ spy.mockClear();
150
+ await buildRaw({ org: ORG, commands: ["select uiapi/query/Case/edges/node/Id"] }, deps());
151
+ expect(spy).toHaveBeenCalledWith(ORG, "query", ORG_URL);
152
+ });
153
+ });
@@ -0,0 +1,134 @@
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 { makeNoopPrimeDeps } from "../../__tests__/helpers/prime-deps.js";
10
+ import { primeSchemaCache } from "../../lib/walker.js";
11
+ import { buildUpdate } from "../build-update.js";
12
+
13
+ const SCHEMA_SDL = `
14
+ type Query { _placeholder: Boolean }
15
+ type Mutation { uiapi(input: UIAPIMutationsInput): UIAPIMutations! }
16
+ input UIAPIMutationsInput { allOrNone: Boolean }
17
+ type UIAPIMutations {
18
+ AccountUpdate(input: AccountUpdateInput!): AccountUpdatePayload
19
+ Custom_Object__cUpdate(input: Custom_Object__cUpdateInput!): Custom_Object__cUpdatePayload
20
+ }
21
+ input AccountUpdateInput { Account: AccountUpdateRepresentation! }
22
+ input AccountUpdateRepresentation { Name: String, Industry: String }
23
+ type AccountUpdatePayload { Record: Account }
24
+ input Custom_Object__cUpdateInput { Custom_Object__c: Custom_Object__cUpdateRepresentation! }
25
+ input Custom_Object__cUpdateRepresentation { Name: String }
26
+ type Custom_Object__cUpdatePayload { Record: Custom_Object__c }
27
+ type Account { Id: ID!, Name: StringValue, Industry: StringValue }
28
+ type Custom_Object__c { Id: ID!, Name: StringValue }
29
+ type StringValue { value: String }
30
+ `;
31
+
32
+ const ORG = "test-update-validation";
33
+ const ORG_URL = "https://test-update-validation.my.salesforce.com";
34
+ const SCHEMA = buildSchema(SCHEMA_SDL);
35
+ primeSchemaCache(ORG, SCHEMA);
36
+ primeSchemaCache(ORG_URL, SCHEMA);
37
+
38
+ const noopPrimeDeps = () => makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
39
+
40
+ describe("intent/build-update — GraphQL name validation", () => {
41
+ describe("object name validation", () => {
42
+ it("throws for object with special characters: 'not valid!'", async () => {
43
+ await expect(
44
+ buildUpdate({ org: ORG, object: "not valid!" }, noopPrimeDeps()),
45
+ ).rejects.toThrow(/buildMutation: object 'not valid!' is not a valid GraphQL Name/);
46
+ });
47
+
48
+ it("throws for object starting with a digit: '123Object'", async () => {
49
+ await expect(buildUpdate({ org: ORG, object: "123Object" }, noopPrimeDeps())).rejects.toThrow(
50
+ /buildMutation: object '123Object' is not a valid GraphQL Name/,
51
+ );
52
+ });
53
+
54
+ it("throws for object with hyphens: 'my-object'", async () => {
55
+ await expect(buildUpdate({ org: ORG, object: "my-object" }, noopPrimeDeps())).rejects.toThrow(
56
+ /buildMutation: object 'my-object' is not a valid GraphQL Name/,
57
+ );
58
+ });
59
+
60
+ it("accepts valid object name with underscores: 'Custom_Object__c'", async () => {
61
+ const result = await buildUpdate(
62
+ { org: ORG, object: "Custom_Object__c", returnFields: ["Id"] },
63
+ noopPrimeDeps(),
64
+ );
65
+ expect(result.query).toContain("Custom_Object__cUpdate");
66
+ });
67
+
68
+ it("accepts standard object name: 'Account'", async () => {
69
+ const result = await buildUpdate(
70
+ { org: ORG, object: "Account", returnFields: ["Id"] },
71
+ noopPrimeDeps(),
72
+ );
73
+ expect(result.query).toContain("mutation UpdateAccount");
74
+ });
75
+
76
+ it("accepts object starting with underscore: '_Foo'", async () => {
77
+ // The regex allows underscore as first char per GraphQL spec.
78
+ await expect(
79
+ buildUpdate({ org: ORG, object: "_Foo" }, noopPrimeDeps()),
80
+ ).resolves.toBeDefined();
81
+ });
82
+ });
83
+
84
+ describe("inputVariable name validation", () => {
85
+ it("throws for inputVariable with spaces: 'has spaces'", async () => {
86
+ await expect(
87
+ buildUpdate({ org: ORG, object: "Account", inputVariable: "has spaces" }, noopPrimeDeps()),
88
+ ).rejects.toThrow(/buildMutation: inputVariable 'has spaces' is not a valid GraphQL Name/);
89
+ });
90
+
91
+ it("throws for inputVariable with dollar sign in body: 'my$var'", async () => {
92
+ await expect(
93
+ buildUpdate({ org: ORG, object: "Account", inputVariable: "my$var" }, noopPrimeDeps()),
94
+ ).rejects.toThrow(/buildMutation: inputVariable 'my\$var' is not a valid GraphQL Name/);
95
+ });
96
+
97
+ it("throws for inputVariable starting with digit: '1input'", async () => {
98
+ await expect(
99
+ buildUpdate({ org: ORG, object: "Account", inputVariable: "1input" }, noopPrimeDeps()),
100
+ ).rejects.toThrow(/buildMutation: inputVariable '1input' is not a valid GraphQL Name/);
101
+ });
102
+
103
+ it("accepts inputVariable with leading $ (prefix stripped): '$myInput'", async () => {
104
+ const result = await buildUpdate(
105
+ { org: ORG, object: "Account", inputVariable: "$myInput" },
106
+ noopPrimeDeps(),
107
+ );
108
+ expect(result.query).toContain("$myInput");
109
+ });
110
+
111
+ it("accepts valid inputVariable: 'accountInput'", async () => {
112
+ const result = await buildUpdate(
113
+ { org: ORG, object: "Account", inputVariable: "accountInput" },
114
+ noopPrimeDeps(),
115
+ );
116
+ expect(result.query).toContain("$accountInput");
117
+ });
118
+
119
+ it("accepts default inputVariable 'input' when not specified", async () => {
120
+ const result = await buildUpdate({ org: ORG, object: "Account" }, noopPrimeDeps());
121
+ expect(result.query).toContain("$input");
122
+ });
123
+ });
124
+
125
+ describe("operationName name validation", () => {
126
+ // Exhaustive name matrix lives in lib/__tests__/graphql-name.spec.ts; this
127
+ // just asserts buildMutation wires operationName through the guard.
128
+ it("throws for an operationName that is not a valid GraphQL Name", async () => {
129
+ await expect(
130
+ buildUpdate({ org: ORG, object: "Account", operationName: "has spaces" }, noopPrimeDeps()),
131
+ ).rejects.toThrow(/buildMutation: operationName 'has spaces' is not a valid GraphQL Name/);
132
+ });
133
+ });
134
+ });
@@ -0,0 +1,182 @@
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 { buildOutput } from "./build-output.js";
8
+ import { getSchemaWithPriming } from "./get-schema-with-priming.js";
9
+ import {
10
+ type AggregateSpec,
11
+ type AggregationSpec,
12
+ type GroupByElement,
13
+ type ToolOutput,
14
+ } from "./types.js";
15
+ import { assertGraphqlName } from "../lib/graphql-name.js";
16
+ import { type PrimeDeps } from "../lib/prime-schema.js";
17
+ import {
18
+ addVariable,
19
+ createSession,
20
+ createSiblingFieldInstance,
21
+ deepSetArg,
22
+ type QuerySession,
23
+ selectLeaf,
24
+ } from "../lib/session.js";
25
+ import { aggregatePath } from "../lib/uiapi.js";
26
+ import { normalizeOrderBy, promoteVariables } from "../lib/variable-promotion.js";
27
+
28
+ const FIELD_REQUIRED_FNS = new Set<AggregationSpec["function"]>(["sum", "avg", "min", "max"]);
29
+
30
+ /**
31
+ * Build a UIAPI aggregate query against a Salesforce org. Implements
32
+ * `sf_gql_aggregate` intent for the graphiti MCP server (FR-8).
33
+ *
34
+ * Implicit behaviors not visible in the signature:
35
+ * - `aggregations[]` projects each entry under the SObject's aggregate node
36
+ * using `<function><PascalCaseField>` as the default GraphQL alias; pass
37
+ * `alias` to override. Default field is `Id` for `count`/`countDistinct`;
38
+ * `sum`/`avg`/`min`/`max` require a field (FR-8.3).
39
+ * - Duplicate result keys (default or explicit) throw — the caller must
40
+ * disambiguate with aliases (FR-8.4).
41
+ * - `groupBy` accepts flat field names only in v1; each renders as
42
+ * `{Field: {group: true}}` and selects `Field { value }` on the
43
+ * aggregate node so the grouping leaf comes back in the response.
44
+ * Dotted paths throw.
45
+ * - `filter` applies to every aggregation in the call. `$varName` leaves
46
+ * in the filter promote to typed query variables. Per-aggregation
47
+ * filters are not supported (FR-8.6).
48
+ * - Operation name defaults to `<Object>Aggregate`.
49
+ *
50
+ * Throws on invalid `object` or `operationName` (must be valid GraphQL Names),
51
+ * auth-missing or introspection failure (via `getSchemaWithPriming`), on
52
+ * FR-8.3/8.4 spec violations, and on dotted groupBy paths. Never throws on
53
+ * validation or codegen failure (those surface as `warnings[]`).
54
+ */
55
+ export async function buildAggregate(spec: AggregateSpec, deps?: PrimeDeps): Promise<ToolOutput> {
56
+ assertGraphqlName(spec.object, "buildAggregate", "object");
57
+
58
+ const { schema, primingNote, instanceUrl } = await getSchemaWithPriming(spec.org, deps);
59
+
60
+ const session = createSession(spec.org, "aggregate", instanceUrl);
61
+ session.operationName = spec.operationName ?? `${spec.object}Aggregate`;
62
+ assertGraphqlName(session.operationName, "buildAggregate", "operationName");
63
+
64
+ const connectionPath = aggregatePath(spec.object);
65
+ const aggregateNodePath = [...connectionPath, "edges", "node", "aggregate"];
66
+
67
+ const groupBy = spec.groupBy ?? [];
68
+ const extraWarnings: string[] = [];
69
+ applyGroupBy(session, connectionPath, aggregateNodePath, groupBy);
70
+
71
+ if (spec.filter) {
72
+ promoteVariables(session, schema, connectionPath, "where", spec.filter, extraWarnings);
73
+ deepSetArg(session, connectionPath, "where", [], JSON.stringify(spec.filter));
74
+ }
75
+
76
+ const orderBy = normalizeOrderBy(spec.orderBy);
77
+ if (Array.isArray(spec.orderBy) && spec.orderBy.length > 1) {
78
+ extraWarnings.push(
79
+ "orderBy: array collapsed to first element. UIAPI accepts a single orderBy object — use multiple keys in one object for multi-field ordering (e.g. { Industry: { order: DESC }, Name: { order: ASC } }).",
80
+ );
81
+ }
82
+ if (orderBy) {
83
+ promoteVariables(session, schema, connectionPath, "orderBy", orderBy, extraWarnings);
84
+ deepSetArg(session, connectionPath, "orderBy", [], JSON.stringify(orderBy));
85
+ }
86
+
87
+ if (spec.first !== undefined) {
88
+ deepSetArg(session, connectionPath, "first", [], String(spec.first));
89
+ }
90
+
91
+ addVariable(session, "after", "String");
92
+ deepSetArg(session, connectionPath, "after", [], "$after");
93
+
94
+ selectLeaf(session, [...connectionPath, "pageInfo", "hasNextPage"]);
95
+ selectLeaf(session, [...connectionPath, "pageInfo", "endCursor"]);
96
+
97
+ // FR-8.2 — `aggregate(Account)` with no aggregations and no groupBy
98
+ // defaults to `count` over `Id` (the SOQL `select count(*)` analogue).
99
+ const requested = spec.aggregations ?? [];
100
+ const aggregations: AggregationSpec[] =
101
+ requested.length === 0 && groupBy.length === 0 ? [{ function: "count" }] : requested;
102
+
103
+ // Pre-seed with groupBy field names: each renders as a top-level response
104
+ // key on the aggregate node alongside aggregation aliases. An aggregation
105
+ // alias colliding with a groupBy key produces invalid GraphQL (different
106
+ // shapes, same response key — spec §5.3.2).
107
+ const seenKeys = new Set<string>(groupBy.map((el) => (typeof el === "string" ? el : el.field)));
108
+ for (const agg of aggregations) {
109
+ if (agg.field === undefined && FIELD_REQUIRED_FNS.has(agg.function)) {
110
+ throw new Error(`buildAggregate: aggregation '${agg.function}' requires a field (FR-8.3)`);
111
+ }
112
+ if (agg.alias !== undefined) {
113
+ assertGraphqlName(agg.alias, "buildAggregate", "alias");
114
+ }
115
+ const field = agg.field ?? "Id";
116
+ const key = agg.alias ?? defaultKey(agg.function, field);
117
+ if (seenKeys.has(key)) {
118
+ throw new Error(
119
+ `buildAggregate: duplicate aggregation key '${key}' — set distinct aliases (FR-8.4)`,
120
+ );
121
+ }
122
+ seenKeys.add(key);
123
+ applyAggregation(session, aggregateNodePath, agg, field, key);
124
+ }
125
+
126
+ return buildOutput(session, schema, primingNote, extraWarnings);
127
+ }
128
+
129
+ function applyGroupBy(
130
+ session: QuerySession,
131
+ connectionPath: string[],
132
+ aggregateNodePath: string[],
133
+ groupBy: GroupByElement[],
134
+ ): void {
135
+ if (groupBy.length === 0) return;
136
+ const seenGroupByFields = new Set<string>();
137
+ for (const element of groupBy) {
138
+ const field = typeof element === "string" ? element : element.field;
139
+ if (seenGroupByFields.has(field)) {
140
+ throw new Error(`buildAggregate: duplicate groupBy field '${field}'`);
141
+ }
142
+ seenGroupByFields.add(field);
143
+ if (field.includes(".")) {
144
+ throw new Error(
145
+ `buildAggregate: dotted groupBy field '${field}' is not supported in v1 (flat-only)`,
146
+ );
147
+ }
148
+ assertGraphqlName(field, "buildAggregate", "groupBy field");
149
+ if (typeof element === "string") {
150
+ deepSetArg(session, connectionPath, "groupBy", [field, "group"], "true");
151
+ } else {
152
+ deepSetArg(session, connectionPath, "groupBy", [field, "function"], element.function);
153
+ }
154
+ selectLeaf(session, [...aggregateNodePath, field, "value"]);
155
+ }
156
+ }
157
+
158
+ function applyAggregation(
159
+ session: QuerySession,
160
+ aggregateNodePath: string[],
161
+ agg: AggregationSpec,
162
+ field: string,
163
+ key: string,
164
+ ): void {
165
+ const fieldPath = [...aggregateNodePath, field];
166
+ createSiblingFieldInstance(session, fieldPath, key);
167
+ selectLeaf(session, [...fieldPath, agg.function, "value"]);
168
+ }
169
+
170
+ function defaultKey(fn: string, field: string): string {
171
+ return `${fn}${pascalCase(field)}`;
172
+ }
173
+
174
+ function pascalCase(field: string): string {
175
+ const stripped = field.endsWith("__c") ? field.slice(0, -3) : field;
176
+ return stripped
177
+ .replace(/[^a-zA-Z0-9]/g, " ")
178
+ .split(/\s+/)
179
+ .filter(Boolean)
180
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
181
+ .join("");
182
+ }
@@ -0,0 +1,19 @@
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 { buildMutation } from "./build-mutation.js";
8
+ import { type CreateSpec, type ToolOutput } from "./types.js";
9
+ import { type PrimeDeps } from "../lib/prime-schema.js";
10
+
11
+ /**
12
+ * Build a UIAPI create mutation against a Salesforce org. Implements
13
+ * `sf_gql_create` intent for the graphiti MCP server (FR-5.6). Thin
14
+ * wrapper around `buildMutation` with `op = "Create"`; see that helper
15
+ * for behavior details.
16
+ */
17
+ export async function buildCreate(spec: CreateSpec, deps?: PrimeDeps): Promise<ToolOutput> {
18
+ return buildMutation(spec, "Create", deps);
19
+ }