@salesforce/graphiti 10.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_GUIDE.md +424 -0
- package/CHANGELOG.md +448 -0
- package/LICENSE.txt +82 -0
- package/README.md +204 -0
- package/TASK.md +249 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +683 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/args.d.ts +13 -0
- package/dist/commands/args.js +207 -0
- package/dist/commands/args.js.map +1 -0
- package/dist/commands/build.d.ts +11 -0
- package/dist/commands/build.js +209 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/connect.d.ts +8 -0
- package/dist/commands/connect.js +55 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/describe.d.ts +6 -0
- package/dist/commands/describe.js +229 -0
- package/dist/commands/describe.js.map +1 -0
- package/dist/commands/meta.d.ts +9 -0
- package/dist/commands/meta.js +140 -0
- package/dist/commands/meta.js.map +1 -0
- package/dist/commands/navigate.d.ts +14 -0
- package/dist/commands/navigate.js +105 -0
- package/dist/commands/navigate.js.map +1 -0
- package/dist/commands/query-helpers.d.ts +80 -0
- package/dist/commands/query-helpers.js +865 -0
- package/dist/commands/query-helpers.js.map +1 -0
- package/dist/commands/query.d.ts +26 -0
- package/dist/commands/query.js +901 -0
- package/dist/commands/query.js.map +1 -0
- package/dist/commands/review.d.ts +18 -0
- package/dist/commands/review.js +533 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/session-mgmt.d.ts +25 -0
- package/dist/commands/session-mgmt.js +206 -0
- package/dist/commands/session-mgmt.js.map +1 -0
- package/dist/commands/type.d.ts +6 -0
- package/dist/commands/type.js +342 -0
- package/dist/commands/type.js.map +1 -0
- package/dist/commands/validate-input.d.ts +6 -0
- package/dist/commands/validate-input.js +32 -0
- package/dist/commands/validate-input.js.map +1 -0
- package/dist/intent/build-aggregate.d.ts +33 -0
- package/dist/intent/build-aggregate.js +134 -0
- package/dist/intent/build-aggregate.js.map +1 -0
- package/dist/intent/build-create.d.ts +14 -0
- package/dist/intent/build-create.js +16 -0
- package/dist/intent/build-create.js.map +1 -0
- package/dist/intent/build-delete.d.ts +30 -0
- package/dist/intent/build-delete.js +53 -0
- package/dist/intent/build-delete.js.map +1 -0
- package/dist/intent/build-detail.d.ts +32 -0
- package/dist/intent/build-detail.js +80 -0
- package/dist/intent/build-detail.js.map +1 -0
- package/dist/intent/build-discover.d.ts +30 -0
- package/dist/intent/build-discover.js +149 -0
- package/dist/intent/build-discover.js.map +1 -0
- package/dist/intent/build-list.d.ts +28 -0
- package/dist/intent/build-list.js +75 -0
- package/dist/intent/build-list.js.map +1 -0
- package/dist/intent/build-mutation.d.ts +23 -0
- package/dist/intent/build-mutation.js +54 -0
- package/dist/intent/build-mutation.js.map +1 -0
- package/dist/intent/build-output.d.ts +27 -0
- package/dist/intent/build-output.js +60 -0
- package/dist/intent/build-output.js.map +1 -0
- package/dist/intent/build-raw.d.ts +23 -0
- package/dist/intent/build-raw.js +54 -0
- package/dist/intent/build-raw.js.map +1 -0
- package/dist/intent/build-update.d.ts +14 -0
- package/dist/intent/build-update.js +16 -0
- package/dist/intent/build-update.js.map +1 -0
- package/dist/intent/get-schema-with-priming.d.ts +26 -0
- package/dist/intent/get-schema-with-priming.js +32 -0
- package/dist/intent/get-schema-with-priming.js.map +1 -0
- package/dist/intent/select-child-relationship.d.ts +19 -0
- package/dist/intent/select-child-relationship.js +38 -0
- package/dist/intent/select-child-relationship.js.map +1 -0
- package/dist/intent/types.d.ts +159 -0
- package/dist/intent/types.js +21 -0
- package/dist/intent/types.js.map +1 -0
- package/dist/lib/apply-command.d.ts +15 -0
- package/dist/lib/apply-command.js +238 -0
- package/dist/lib/apply-command.js.map +1 -0
- package/dist/lib/auth.d.ts +38 -0
- package/dist/lib/auth.js +113 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/codegen.d.ts +32 -0
- package/dist/lib/codegen.js +700 -0
- package/dist/lib/codegen.js.map +1 -0
- package/dist/lib/command-registry.d.ts +59 -0
- package/dist/lib/command-registry.js +366 -0
- package/dist/lib/command-registry.js.map +1 -0
- package/dist/lib/formatter.d.ts +76 -0
- package/dist/lib/formatter.js +419 -0
- package/dist/lib/formatter.js.map +1 -0
- package/dist/lib/fs-utils.d.ts +23 -0
- package/dist/lib/fs-utils.js +46 -0
- package/dist/lib/fs-utils.js.map +1 -0
- package/dist/lib/graphql-name.d.ts +27 -0
- package/dist/lib/graphql-name.js +32 -0
- package/dist/lib/graphql-name.js.map +1 -0
- package/dist/lib/interactive.d.ts +6 -0
- package/dist/lib/interactive.js +562 -0
- package/dist/lib/interactive.js.map +1 -0
- package/dist/lib/introspect.d.ts +40 -0
- package/dist/lib/introspect.js +280 -0
- package/dist/lib/introspect.js.map +1 -0
- package/dist/lib/object-info.d.ts +79 -0
- package/dist/lib/object-info.js +313 -0
- package/dist/lib/object-info.js.map +1 -0
- package/dist/lib/path-selection.d.ts +50 -0
- package/dist/lib/path-selection.js +146 -0
- package/dist/lib/path-selection.js.map +1 -0
- package/dist/lib/prime-schema.d.ts +59 -0
- package/dist/lib/prime-schema.js +158 -0
- package/dist/lib/prime-schema.js.map +1 -0
- package/dist/lib/query-builder.d.ts +10 -0
- package/dist/lib/query-builder.js +168 -0
- package/dist/lib/query-builder.js.map +1 -0
- package/dist/lib/query-commands.d.ts +19 -0
- package/dist/lib/query-commands.js +262 -0
- package/dist/lib/query-commands.js.map +1 -0
- package/dist/lib/session.d.ts +205 -0
- package/dist/lib/session.js +1075 -0
- package/dist/lib/session.js.map +1 -0
- package/dist/lib/tokenize.d.ts +12 -0
- package/dist/lib/tokenize.js +48 -0
- package/dist/lib/tokenize.js.map +1 -0
- package/dist/lib/uiapi.d.ts +109 -0
- package/dist/lib/uiapi.js +159 -0
- package/dist/lib/uiapi.js.map +1 -0
- package/dist/lib/validator.d.ts +27 -0
- package/dist/lib/validator.js +100 -0
- package/dist/lib/validator.js.map +1 -0
- package/dist/lib/variable-promotion.d.ts +69 -0
- package/dist/lib/variable-promotion.js +95 -0
- package/dist/lib/variable-promotion.js.map +1 -0
- package/dist/lib/walker.d.ts +147 -0
- package/dist/lib/walker.js +723 -0
- package/dist/lib/walker.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +34 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.d.ts +7 -0
- package/dist/mcp/stdio.js +25 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/mcp/tools/echo.d.ts +7 -0
- package/dist/mcp/tools/echo.js +17 -0
- package/dist/mcp/tools/echo.js.map +1 -0
- package/dist/mcp/tools/sf-gql-aggregate.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-aggregate.js +75 -0
- package/dist/mcp/tools/sf-gql-aggregate.js.map +1 -0
- package/dist/mcp/tools/sf-gql-create.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-create.js +35 -0
- package/dist/mcp/tools/sf-gql-create.js.map +1 -0
- package/dist/mcp/tools/sf-gql-delete.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-delete.js +31 -0
- package/dist/mcp/tools/sf-gql-delete.js.map +1 -0
- package/dist/mcp/tools/sf-gql-detail.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-detail.js +58 -0
- package/dist/mcp/tools/sf-gql-detail.js.map +1 -0
- package/dist/mcp/tools/sf-gql-discover.d.ts +9 -0
- package/dist/mcp/tools/sf-gql-discover.js +51 -0
- package/dist/mcp/tools/sf-gql-discover.js.map +1 -0
- package/dist/mcp/tools/sf-gql-list.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-list.js +53 -0
- package/dist/mcp/tools/sf-gql-list.js.map +1 -0
- package/dist/mcp/tools/sf-gql-raw.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-raw.js +39 -0
- package/dist/mcp/tools/sf-gql-raw.js.map +1 -0
- package/dist/mcp/tools/sf-gql-update.d.ts +11 -0
- package/dist/mcp/tools/sf-gql-update.js +35 -0
- package/dist/mcp/tools/sf-gql-update.js.map +1 -0
- package/dist/mcp/tools/shared/zod-schemas.d.ts +49 -0
- package/dist/mcp/tools/shared/zod-schemas.js +46 -0
- package/dist/mcp/tools/shared/zod-schemas.js.map +1 -0
- package/package.json +36 -0
- package/ralph-loop.sh +120 -0
- package/scripts/smoke-orderby.sh +190 -0
- package/src/__tests__/helpers/prime-deps.ts +46 -0
- package/src/__tests__/helpers/schema.ts +73 -0
- package/src/__tests__/helpers/stdout.ts +33 -0
- package/src/__tests__/setup.ts +19 -0
- package/src/cli.ts +764 -0
- package/src/commands/__tests__/query.spec.ts +137 -0
- package/src/commands/args.ts +306 -0
- package/src/commands/build.ts +288 -0
- package/src/commands/connect.ts +60 -0
- package/src/commands/describe.ts +246 -0
- package/src/commands/meta.ts +171 -0
- package/src/commands/navigate.ts +134 -0
- package/src/commands/query-helpers.ts +1202 -0
- package/src/commands/query.ts +1085 -0
- package/src/commands/review.ts +670 -0
- package/src/commands/session-mgmt.ts +256 -0
- package/src/commands/type.ts +437 -0
- package/src/commands/validate-input.ts +38 -0
- package/src/intent/__tests__/build-aggregate.spec.ts +931 -0
- package/src/intent/__tests__/build-create-validation.spec.ts +135 -0
- package/src/intent/__tests__/build-delete.spec.ts +121 -0
- package/src/intent/__tests__/build-detail.spec.ts +333 -0
- package/src/intent/__tests__/build-discover.spec.ts +432 -0
- package/src/intent/__tests__/build-list.spec.ts +284 -0
- package/src/intent/__tests__/build-mutation.spec.ts +108 -0
- package/src/intent/__tests__/build-output.spec.ts +55 -0
- package/src/intent/__tests__/build-raw.spec.ts +153 -0
- package/src/intent/__tests__/build-update-validation.spec.ts +134 -0
- package/src/intent/build-aggregate.ts +182 -0
- package/src/intent/build-create.ts +19 -0
- package/src/intent/build-delete.ts +62 -0
- package/src/intent/build-detail.ts +95 -0
- package/src/intent/build-discover.ts +199 -0
- package/src/intent/build-list.ts +91 -0
- package/src/intent/build-mutation.ts +75 -0
- package/src/intent/build-output.ts +72 -0
- package/src/intent/build-raw.ts +61 -0
- package/src/intent/build-update.ts +19 -0
- package/src/intent/get-schema-with-priming.ts +43 -0
- package/src/intent/select-child-relationship.ts +48 -0
- package/src/intent/types.ts +181 -0
- package/src/lib/__tests__/apply-command.parity.spec.ts +97 -0
- package/src/lib/__tests__/apply-command.spec.ts +171 -0
- package/src/lib/__tests__/auth.spec.ts +292 -0
- package/src/lib/__tests__/formatter.spec.ts +86 -0
- package/src/lib/__tests__/graphql-name.spec.ts +64 -0
- package/src/lib/__tests__/introspect.spec.ts +399 -0
- package/src/lib/__tests__/object-info.spec.ts +124 -0
- package/src/lib/__tests__/path-selection.spec.ts +219 -0
- package/src/lib/__tests__/prime-schema.spec.ts +269 -0
- package/src/lib/__tests__/query-builder.spec.ts +95 -0
- package/src/lib/__tests__/query-commands.spec.ts +74 -0
- package/src/lib/__tests__/session.spec.ts +292 -0
- package/src/lib/__tests__/tokenize.spec.ts +33 -0
- package/src/lib/__tests__/uiapi.spec.ts +192 -0
- package/src/lib/__tests__/variable-promotion.spec.ts +211 -0
- package/src/lib/__tests__/walker.spec.ts +250 -0
- package/src/lib/apply-command.ts +286 -0
- package/src/lib/auth.ts +157 -0
- package/src/lib/codegen.ts +899 -0
- package/src/lib/command-registry.ts +434 -0
- package/src/lib/formatter.ts +587 -0
- package/src/lib/fs-utils.ts +46 -0
- package/src/lib/graphql-name.ts +35 -0
- package/src/lib/interactive.ts +634 -0
- package/src/lib/introspect.ts +320 -0
- package/src/lib/object-info.ts +409 -0
- package/src/lib/path-selection.ts +162 -0
- package/src/lib/prime-schema.ts +195 -0
- package/src/lib/query-builder.ts +193 -0
- package/src/lib/query-commands.ts +290 -0
- package/src/lib/session.ts +1304 -0
- package/src/lib/tokenize.ts +43 -0
- package/src/lib/uiapi.ts +176 -0
- package/src/lib/validator.ts +143 -0
- package/src/lib/variable-promotion.ts +145 -0
- package/src/lib/walker.ts +975 -0
- package/src/mcp/__tests__/server.spec.ts +155 -0
- package/src/mcp/server.ts +38 -0
- package/src/mcp/stdio.ts +29 -0
- package/src/mcp/tools/__tests__/sf-gql-aggregate.spec.ts +173 -0
- package/src/mcp/tools/__tests__/sf-gql-create.spec.ts +235 -0
- package/src/mcp/tools/__tests__/sf-gql-delete.spec.ts +194 -0
- package/src/mcp/tools/__tests__/sf-gql-detail.spec.ts +246 -0
- package/src/mcp/tools/__tests__/sf-gql-discover.spec.ts +320 -0
- package/src/mcp/tools/__tests__/sf-gql-list.spec.ts +128 -0
- package/src/mcp/tools/__tests__/sf-gql-raw.spec.ts +141 -0
- package/src/mcp/tools/__tests__/sf-gql-update.spec.ts +207 -0
- package/src/mcp/tools/echo.ts +24 -0
- package/src/mcp/tools/sf-gql-aggregate.ts +102 -0
- package/src/mcp/tools/sf-gql-create.ts +55 -0
- package/src/mcp/tools/sf-gql-delete.ts +49 -0
- package/src/mcp/tools/sf-gql-detail.ts +85 -0
- package/src/mcp/tools/sf-gql-discover.ts +67 -0
- package/src/mcp/tools/sf-gql-list.ts +73 -0
- package/src/mcp/tools/sf-gql-raw.ts +56 -0
- package/src/mcp/tools/sf-gql-update.ts +55 -0
- package/src/mcp/tools/shared/zod-schemas.ts +55 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,135 @@
|
|
|
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 { buildCreate } from "../build-create.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
|
+
AccountCreate(input: AccountCreateInput!): AccountCreatePayload
|
|
19
|
+
Custom_Object__cCreate(input: Custom_Object__cCreateInput!): Custom_Object__cCreatePayload
|
|
20
|
+
}
|
|
21
|
+
input AccountCreateInput { Account: AccountCreateRepresentation! }
|
|
22
|
+
input AccountCreateRepresentation { Name: String, Industry: String }
|
|
23
|
+
type AccountCreatePayload { Record: Account }
|
|
24
|
+
input Custom_Object__cCreateInput { Custom_Object__c: Custom_Object__cCreateRepresentation! }
|
|
25
|
+
input Custom_Object__cCreateRepresentation { Name: String }
|
|
26
|
+
type Custom_Object__cCreatePayload { 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-create-validation";
|
|
33
|
+
const ORG_URL = "https://test-create-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-create — GraphQL name validation", () => {
|
|
41
|
+
describe("object name validation", () => {
|
|
42
|
+
it("throws for object with special characters: 'not valid!'", async () => {
|
|
43
|
+
await expect(
|
|
44
|
+
buildCreate({ 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(buildCreate({ 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(buildCreate({ 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 buildCreate(
|
|
62
|
+
{ org: ORG, object: "Custom_Object__c", returnFields: ["Id"] },
|
|
63
|
+
noopPrimeDeps(),
|
|
64
|
+
);
|
|
65
|
+
expect(result.query).toContain("Custom_Object__cCreate");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("accepts standard object name: 'Account'", async () => {
|
|
69
|
+
const result = await buildCreate(
|
|
70
|
+
{ org: ORG, object: "Account", returnFields: ["Id"] },
|
|
71
|
+
noopPrimeDeps(),
|
|
72
|
+
);
|
|
73
|
+
expect(result.query).toContain("mutation CreateAccount");
|
|
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
|
+
buildCreate({ 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
|
+
buildCreate({ 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
|
+
buildCreate({ 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
|
+
buildCreate({ 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 buildCreate(
|
|
105
|
+
{ org: ORG, object: "Account", inputVariable: "$myInput" },
|
|
106
|
+
noopPrimeDeps(),
|
|
107
|
+
);
|
|
108
|
+
// The leading $ is stripped by existing code before validation
|
|
109
|
+
expect(result.query).toContain("$myInput");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("accepts valid inputVariable: 'accountInput'", async () => {
|
|
113
|
+
const result = await buildCreate(
|
|
114
|
+
{ org: ORG, object: "Account", inputVariable: "accountInput" },
|
|
115
|
+
noopPrimeDeps(),
|
|
116
|
+
);
|
|
117
|
+
expect(result.query).toContain("$accountInput");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("accepts default inputVariable 'input' when not specified", async () => {
|
|
121
|
+
const result = await buildCreate({ org: ORG, object: "Account" }, noopPrimeDeps());
|
|
122
|
+
expect(result.query).toContain("$input");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("operationName name validation", () => {
|
|
127
|
+
// Exhaustive name matrix lives in lib/__tests__/graphql-name.spec.ts; this
|
|
128
|
+
// just asserts buildMutation wires operationName through the guard.
|
|
129
|
+
it("throws for an operationName that is not a valid GraphQL Name", async () => {
|
|
130
|
+
await expect(
|
|
131
|
+
buildCreate({ org: ORG, object: "Account", operationName: "has spaces" }, noopPrimeDeps()),
|
|
132
|
+
).rejects.toThrow(/buildMutation: operationName 'has spaces' is not a valid GraphQL Name/);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
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 { buildDelete } from "../build-delete.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
|
+
AccountDelete(input: RecordDeleteInput!): RecordDeletePayload
|
|
25
|
+
Custom_Object__cDelete(input: RecordDeleteInput!): RecordDeletePayload
|
|
26
|
+
}
|
|
27
|
+
input RecordDeleteInput { Id: ID! }
|
|
28
|
+
type RecordDeletePayload { Id: ID }
|
|
29
|
+
type Account { Id: ID!, Name: StringValue }
|
|
30
|
+
type Custom_Object__c { Id: ID!, Name: StringValue }
|
|
31
|
+
type StringValue { value: String }
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const ORG = "test-delete";
|
|
35
|
+
const ORG_URL = "https://test-delete.my.salesforce.com";
|
|
36
|
+
const SCHEMA = buildSchema(SCHEMA_SDL);
|
|
37
|
+
primeSchemaCache(ORG, SCHEMA);
|
|
38
|
+
primeSchemaCache(ORG_URL, SCHEMA);
|
|
39
|
+
|
|
40
|
+
function noopPrimeDeps() {
|
|
41
|
+
return makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("intent/build-delete", () => {
|
|
45
|
+
it("renders a delete mutation with RecordDeleteInput! and selects Id only", async () => {
|
|
46
|
+
const result = await buildDelete({ org: ORG, object: "Account" }, noopPrimeDeps());
|
|
47
|
+
|
|
48
|
+
expect(result.query).toContain("mutation DeleteAccount");
|
|
49
|
+
expect(result.query).toContain("$input: RecordDeleteInput!");
|
|
50
|
+
expect(result.query).toContain("AccountDelete(input: $input)");
|
|
51
|
+
// Id is plain ID on the payload — selected directly, never `Id { value }`.
|
|
52
|
+
expect(result.query).toMatch(/AccountDelete\(input: \$input\)\s*{\s*Id\s*}/);
|
|
53
|
+
// No `Record { ... }` sub-selection (delete payloads have no Record path).
|
|
54
|
+
expect(result.query).not.toMatch(/\bRecord\s*{/);
|
|
55
|
+
expect(result.query).not.toContain("Id { value }");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("declares a single required input variable of type RecordDeleteInput!", async () => {
|
|
59
|
+
const result = await buildDelete({ org: ORG, object: "Account" }, noopPrimeDeps());
|
|
60
|
+
|
|
61
|
+
expect(result.variables).toEqual([
|
|
62
|
+
{ name: "input", type: "RecordDeleteInput!", required: true },
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("uses RecordDeleteInput! for custom objects too (schema-wide, not <Object>-specific)", async () => {
|
|
67
|
+
const result = await buildDelete({ org: ORG, object: "Custom_Object__c" }, noopPrimeDeps());
|
|
68
|
+
|
|
69
|
+
expect(result.query).toContain("Custom_Object__cDelete(input: $input)");
|
|
70
|
+
expect(result.query).toContain("$input: RecordDeleteInput!");
|
|
71
|
+
// Crucially NOT Custom_Object__cDeleteInput.
|
|
72
|
+
expect(result.query).not.toContain("Custom_Object__cDeleteInput");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("honors a custom operationName", async () => {
|
|
76
|
+
const result = await buildDelete(
|
|
77
|
+
{ org: ORG, object: "Account", operationName: "RemoveAcct" },
|
|
78
|
+
noopPrimeDeps(),
|
|
79
|
+
);
|
|
80
|
+
expect(result.query).toContain("mutation RemoveAcct");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("honors a custom inputVariable and strips a leading $", async () => {
|
|
84
|
+
const result = await buildDelete(
|
|
85
|
+
{ org: ORG, object: "Account", inputVariable: "$acctInput" },
|
|
86
|
+
noopPrimeDeps(),
|
|
87
|
+
);
|
|
88
|
+
expect(result.query).toContain("$acctInput: RecordDeleteInput!");
|
|
89
|
+
expect(result.query).toContain("AccountDelete(input: $acctInput)");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("produces no validation warnings for a well-formed delete", async () => {
|
|
93
|
+
const result = await buildDelete({ org: ORG, object: "Account" }, noopPrimeDeps());
|
|
94
|
+
expect(result.warnings.filter((w) => w.startsWith("Validation:"))).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("throws for an object that is not a valid GraphQL Name", async () => {
|
|
98
|
+
await expect(buildDelete({ org: ORG, object: "my-object" }, noopPrimeDeps())).rejects.toThrow(
|
|
99
|
+
/buildDelete: object 'my-object' is not a valid GraphQL Name/,
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("throws for an inputVariable that is not a valid GraphQL Name", async () => {
|
|
104
|
+
await expect(
|
|
105
|
+
buildDelete({ org: ORG, object: "Account", inputVariable: "has spaces" }, noopPrimeDeps()),
|
|
106
|
+
).rejects.toThrow(/buildDelete: inputVariable 'has spaces' is not a valid GraphQL Name/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("throws for an operationName that is not a valid GraphQL Name", async () => {
|
|
110
|
+
await expect(
|
|
111
|
+
buildDelete({ org: ORG, object: "Account", operationName: "has spaces" }, noopPrimeDeps()),
|
|
112
|
+
).rejects.toThrow(/buildDelete: operationName 'has spaces' is not a valid GraphQL Name/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("threads instanceUrl as 3rd arg to createSession", async () => {
|
|
116
|
+
const spy = vi.mocked(sessionModule.createSession);
|
|
117
|
+
spy.mockClear();
|
|
118
|
+
await buildDelete({ org: ORG, object: "Account" }, noopPrimeDeps());
|
|
119
|
+
expect(spy).toHaveBeenCalledWith(ORG, "mutation", ORG_URL);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,333 @@
|
|
|
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 path from "node:path";
|
|
8
|
+
import { buildSchema, introspectionFromSchema } from "graphql";
|
|
9
|
+
import { describe, expect, it, vi } from "vitest";
|
|
10
|
+
import { atomicWriteJson } from "../../lib/fs-utils.js";
|
|
11
|
+
import {
|
|
12
|
+
schemaCacheKeyForInstanceUrl,
|
|
13
|
+
schemaDir,
|
|
14
|
+
type SchemaMetadata,
|
|
15
|
+
} from "../../lib/introspect.js";
|
|
16
|
+
import { type PrimeDeps } from "../../lib/prime-schema.js";
|
|
17
|
+
import * as sessionModule from "../../lib/session.js";
|
|
18
|
+
import { primeSchemaCache } from "../../lib/walker.js";
|
|
19
|
+
import { buildDetail } from "../build-detail.js";
|
|
20
|
+
|
|
21
|
+
vi.mock("../../lib/session.js", async (importOriginal) => {
|
|
22
|
+
const actual = await importOriginal<typeof sessionModule>();
|
|
23
|
+
return { ...actual, createSession: vi.fn(actual.createSession) };
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// SDL mirrors build-list.spec.ts but extends every `_Filter` with an
|
|
27
|
+
// `Id: IDOperators` field so the FR-5.5 `where: { Id: { eq: $id } }` binding
|
|
28
|
+
// validates cleanly. Without it, the rendered query would still be correct
|
|
29
|
+
// but graphql-js validate() would warn — that's fine in production (real
|
|
30
|
+
// UIAPI filters all expose Id), and we don't want spurious warnings noise
|
|
31
|
+
// in test assertions.
|
|
32
|
+
const SCHEMA_SDL = `
|
|
33
|
+
type Query { uiapi: UIAPI! }
|
|
34
|
+
type UIAPI { query: RecordQuery! }
|
|
35
|
+
|
|
36
|
+
type RecordQuery {
|
|
37
|
+
Account(first: Int, after: String, where: Account_Filter, orderBy: Account_OrderBy, scope: Scope): AccountConnection!
|
|
38
|
+
Case(first: Int, after: String, where: Case_Filter, orderBy: Case_OrderBy, scope: Scope): CaseConnection!
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
enum Scope { MINE EVERYTHING }
|
|
42
|
+
enum Order { ASC DESC }
|
|
43
|
+
|
|
44
|
+
input Account_Filter { Id: IDOperators, Industry: PicklistOperators, Name: StringOperators }
|
|
45
|
+
input Account_OrderBy { Name: OrderByClause, Industry: OrderByClause }
|
|
46
|
+
input Case_Filter { Id: IDOperators, Status: PicklistOperators, Priority: PicklistOperators }
|
|
47
|
+
input Case_OrderBy { CreatedDate: OrderByClause }
|
|
48
|
+
input IDOperators { eq: ID, ne: ID, in: [ID!] }
|
|
49
|
+
input PicklistOperators { eq: String, ne: String, in: [String!] }
|
|
50
|
+
input StringOperators { eq: String, like: String }
|
|
51
|
+
input OrderByClause { order: Order!, nulls: NullsOrder }
|
|
52
|
+
enum NullsOrder { FIRST LAST }
|
|
53
|
+
|
|
54
|
+
type AccountConnection {
|
|
55
|
+
edges: [AccountEdge!]!
|
|
56
|
+
pageInfo: PageInfo!
|
|
57
|
+
totalCount: Int
|
|
58
|
+
}
|
|
59
|
+
type AccountEdge { node: Account! }
|
|
60
|
+
type CaseConnection {
|
|
61
|
+
edges: [CaseEdge!]!
|
|
62
|
+
pageInfo: PageInfo!
|
|
63
|
+
}
|
|
64
|
+
type CaseEdge { node: Case! }
|
|
65
|
+
type PageInfo { hasNextPage: Boolean!, endCursor: String }
|
|
66
|
+
|
|
67
|
+
type Account {
|
|
68
|
+
Id: ID!
|
|
69
|
+
Name: StringValue
|
|
70
|
+
Industry: StringValue
|
|
71
|
+
Owner: OwnerUnion
|
|
72
|
+
Contacts(first: Int, where: Contact_Filter, orderBy: Contact_OrderBy): ContactConnection
|
|
73
|
+
}
|
|
74
|
+
type Case {
|
|
75
|
+
Id: ID!
|
|
76
|
+
Subject: StringValue
|
|
77
|
+
Status: StringValue
|
|
78
|
+
Owner: OwnerUnion
|
|
79
|
+
Account: Account
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type ContactConnection { edges: [ContactEdge!]! }
|
|
83
|
+
type ContactEdge { node: Contact! }
|
|
84
|
+
input Contact_Filter { Id: IDOperators, Title: StringOperators }
|
|
85
|
+
input Contact_OrderBy { LastName: OrderByClause }
|
|
86
|
+
|
|
87
|
+
type Contact {
|
|
88
|
+
Id: ID!
|
|
89
|
+
LastName: StringValue
|
|
90
|
+
Title: StringValue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
union OwnerUnion = User | Group
|
|
94
|
+
type User { Id: ID!, Name: StringValue, Email: StringValue }
|
|
95
|
+
type Group { Id: ID!, Name: StringValue }
|
|
96
|
+
|
|
97
|
+
type StringValue { value: String }
|
|
98
|
+
`;
|
|
99
|
+
|
|
100
|
+
const ORG = "test-detail";
|
|
101
|
+
const ORG_URL = "https://test-detail.my.salesforce.com";
|
|
102
|
+
const SCHEMA = buildSchema(SCHEMA_SDL);
|
|
103
|
+
primeSchemaCache(ORG, SCHEMA);
|
|
104
|
+
primeSchemaCache(ORG_URL, SCHEMA);
|
|
105
|
+
|
|
106
|
+
function noopPrimeDeps(): PrimeDeps {
|
|
107
|
+
return {
|
|
108
|
+
getOrgAuth: async () => ({
|
|
109
|
+
alias: ORG,
|
|
110
|
+
username: "u",
|
|
111
|
+
instanceUrl: ORG_URL,
|
|
112
|
+
accessToken: "t",
|
|
113
|
+
orgId: "00D",
|
|
114
|
+
}),
|
|
115
|
+
downloadSchema: async (auth) => {
|
|
116
|
+
const cacheKey = schemaCacheKeyForInstanceUrl(auth.instanceUrl);
|
|
117
|
+
const filePath = path.join(schemaDir(), `${cacheKey}.json`);
|
|
118
|
+
atomicWriteJson(filePath, { data: introspectionFromSchema(SCHEMA) });
|
|
119
|
+
const meta: SchemaMetadata = {
|
|
120
|
+
cacheKey,
|
|
121
|
+
instanceUrl: auth.instanceUrl,
|
|
122
|
+
typeCount: 0,
|
|
123
|
+
downloadedAt: new Date().toISOString(),
|
|
124
|
+
filePath,
|
|
125
|
+
};
|
|
126
|
+
return meta;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
describe("intent/build-detail", () => {
|
|
132
|
+
it("declares $id: ID! and binds where { Id: { eq: $id } } with first: 1 (FR-5.5)", async () => {
|
|
133
|
+
const out = await buildDetail(
|
|
134
|
+
{ org: ORG, object: "Account", fields: ["Id", "Name"] },
|
|
135
|
+
noopPrimeDeps(),
|
|
136
|
+
);
|
|
137
|
+
const id = out.variables.find((v) => v.name === "id");
|
|
138
|
+
expect(id).toBeDefined();
|
|
139
|
+
expect(id!.type).toBe("ID!");
|
|
140
|
+
expect(id!.required).toBe(true);
|
|
141
|
+
expect(out.query).toMatch(/\$id\s*:\s*ID!/);
|
|
142
|
+
expect(out.query).toMatch(/where\s*:\s*\{\s*Id\s*:\s*\{\s*eq\s*:\s*\$id\s*\}/);
|
|
143
|
+
expect(out.query).toMatch(/first\s*:\s*1\b/);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("respects custom idVariable name", async () => {
|
|
147
|
+
const out = await buildDetail(
|
|
148
|
+
{ org: ORG, object: "Account", fields: ["Id"], idVariable: "accountId" },
|
|
149
|
+
noopPrimeDeps(),
|
|
150
|
+
);
|
|
151
|
+
const accountId = out.variables.find((v) => v.name === "accountId");
|
|
152
|
+
expect(accountId).toBeDefined();
|
|
153
|
+
expect(accountId!.type).toBe("ID!");
|
|
154
|
+
expect(out.query).toMatch(/\$accountId\s*:\s*ID!/);
|
|
155
|
+
expect(out.query).toMatch(/where\s*:\s*\{\s*Id\s*:\s*\{\s*eq\s*:\s*\$accountId\s*\}/);
|
|
156
|
+
// And no leftover `$id` from the default.
|
|
157
|
+
expect(out.variables.find((v) => v.name === "id")).toBeUndefined();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("scalar fields select { value } except Id (FR-4.1)", async () => {
|
|
161
|
+
const out = await buildDetail(
|
|
162
|
+
{ org: ORG, object: "Account", fields: ["Id", "Name"] },
|
|
163
|
+
noopPrimeDeps(),
|
|
164
|
+
);
|
|
165
|
+
expect(out.query).toMatch(/\bId\b/);
|
|
166
|
+
expect(out.query).toMatch(/Name\s*\{\s*value\s*\}/);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("default operationName is <Object>Detail", async () => {
|
|
170
|
+
const out = await buildDetail({ org: ORG, object: "Case", fields: ["Id"] }, noopPrimeDeps());
|
|
171
|
+
expect(out.query).toMatch(/\bquery\s+CaseDetail\b/);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("respects custom operationName", async () => {
|
|
175
|
+
const out = await buildDetail(
|
|
176
|
+
{ org: ORG, object: "Case", fields: ["Id"], operationName: "GetMyCase" },
|
|
177
|
+
noopPrimeDeps(),
|
|
178
|
+
);
|
|
179
|
+
expect(out.query).toMatch(/\bquery\s+GetMyCase\b/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("rejects an operationName that is not a valid GraphQL Name", async () => {
|
|
183
|
+
await expect(
|
|
184
|
+
buildDetail(
|
|
185
|
+
{ org: ORG, object: "Account", fields: ["Id"], operationName: "has spaces" },
|
|
186
|
+
noopPrimeDeps(),
|
|
187
|
+
),
|
|
188
|
+
).rejects.toThrow(/buildDetail: operationName 'has spaces' is not a valid GraphQL Name/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("rejects an object that is not a valid GraphQL Name", async () => {
|
|
192
|
+
await expect(
|
|
193
|
+
buildDetail({ org: ORG, object: "Order Item", fields: ["Id"] }, noopPrimeDeps()),
|
|
194
|
+
).rejects.toThrow(/buildDetail: object 'Order Item' is not a valid GraphQL Name/);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("parentFields walks dotted path with value wrapping (FR-4.2)", async () => {
|
|
198
|
+
const out = await buildDetail(
|
|
199
|
+
{ org: ORG, object: "Case", fields: ["Id"], parentFields: ["Account.Name"] },
|
|
200
|
+
noopPrimeDeps(),
|
|
201
|
+
);
|
|
202
|
+
expect(out.query).toMatch(/Account\s*\{[^}]*Name\s*\{\s*value\s*\}/s);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("parentFields expands polymorphic union into inline fragments (FR-4.3)", async () => {
|
|
206
|
+
const out = await buildDetail(
|
|
207
|
+
{ org: ORG, object: "Case", fields: ["Id"], parentFields: ["Owner.Name"] },
|
|
208
|
+
noopPrimeDeps(),
|
|
209
|
+
);
|
|
210
|
+
expect(out.query).toMatch(/\.\.\.\s+on\s+User\s*\{[^}]*Name\s*\{\s*value\s*\}/s);
|
|
211
|
+
expect(out.query).toMatch(/\.\.\.\s+on\s+Group\s*\{[^}]*Name\s*\{\s*value\s*\}/s);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("parentFields skips union members lacking the field (FR-4.3)", async () => {
|
|
215
|
+
const out = await buildDetail(
|
|
216
|
+
{ org: ORG, object: "Case", fields: ["Id"], parentFields: ["Owner.Email"] },
|
|
217
|
+
noopPrimeDeps(),
|
|
218
|
+
);
|
|
219
|
+
expect(out.query).toMatch(/\.\.\.\s+on\s+User\s*\{[^}]*Email/s);
|
|
220
|
+
expect(out.query).not.toMatch(/\.\.\.\s+on\s+Group\s*\{[^}]*Email/s);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("childRelationships rendered as edges/node connection with first/orderBy (FR-4.4)", async () => {
|
|
224
|
+
const out = await buildDetail(
|
|
225
|
+
{
|
|
226
|
+
org: ORG,
|
|
227
|
+
object: "Account",
|
|
228
|
+
fields: ["Id"],
|
|
229
|
+
childRelationships: [
|
|
230
|
+
{
|
|
231
|
+
relationshipName: "Contacts",
|
|
232
|
+
fields: ["Id", "LastName"],
|
|
233
|
+
first: 5,
|
|
234
|
+
orderBy: { LastName: { order: "ASC" } },
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
noopPrimeDeps(),
|
|
239
|
+
);
|
|
240
|
+
expect(out.query).toMatch(/Contacts\s*\([^)]*first:\s*5/);
|
|
241
|
+
expect(out.query).toMatch(/Contacts\s*\([^)]*orderBy:\s*\{\s*LastName/s);
|
|
242
|
+
expect(out.query).toMatch(
|
|
243
|
+
/Contacts\s*\([^)]*\)\s*\{\s*edges\s*\{\s*node\s*\{[^}]*LastName\s*\{\s*value\s*\}/s,
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("childRelationships filter $varName promotes typed variable (FR-5.1, 5.2)", async () => {
|
|
248
|
+
const out = await buildDetail(
|
|
249
|
+
{
|
|
250
|
+
org: ORG,
|
|
251
|
+
object: "Account",
|
|
252
|
+
fields: ["Id"],
|
|
253
|
+
childRelationships: [
|
|
254
|
+
{
|
|
255
|
+
relationshipName: "Contacts",
|
|
256
|
+
fields: ["Id"],
|
|
257
|
+
filter: { Title: { like: "$titlePattern" } },
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
noopPrimeDeps(),
|
|
262
|
+
);
|
|
263
|
+
const titlePattern = out.variables.find((v) => v.name === "titlePattern");
|
|
264
|
+
expect(titlePattern).toBeDefined();
|
|
265
|
+
expect(titlePattern!.required).toBe(false);
|
|
266
|
+
// Both the ID! and the promoted nullable variable coexist.
|
|
267
|
+
expect(out.variables.find((v) => v.name === "id")?.required).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("does NOT declare $after or select pageInfo (negative vs sf_gql_list)", async () => {
|
|
271
|
+
const out = await buildDetail({ org: ORG, object: "Account", fields: ["Id"] }, noopPrimeDeps());
|
|
272
|
+
expect(out.variables.find((v) => v.name === "after")).toBeUndefined();
|
|
273
|
+
expect(out.query).not.toMatch(/\$after\b/);
|
|
274
|
+
expect(out.query).not.toMatch(/pageInfo\b/);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("rendered query passes graphql-js validation (no Validation: warnings)", async () => {
|
|
278
|
+
const out = await buildDetail(
|
|
279
|
+
{ org: ORG, object: "Account", fields: ["Id", "Name"] },
|
|
280
|
+
noopPrimeDeps(),
|
|
281
|
+
);
|
|
282
|
+
const validationWarnings = out.warnings.filter((w) => w.startsWith("Validation:"));
|
|
283
|
+
expect(validationWarnings).toEqual([]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("rejects empty idVariable rather than rendering $: ID!", async () => {
|
|
287
|
+
await expect(
|
|
288
|
+
buildDetail({ org: ORG, object: "Account", fields: ["Id"], idVariable: "" }, noopPrimeDeps()),
|
|
289
|
+
).rejects.toThrow(/idVariable must be a non-empty string/);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("throws when idVariable collides with a $varName already promoted by a child filter", async () => {
|
|
293
|
+
// User's child filter promotes `$id: String` (nullable, inferred). The
|
|
294
|
+
// default idVariable also wants `id` but as `ID!`. Without the guard,
|
|
295
|
+
// `addVariable` would silently overwrite the type and corrupt the
|
|
296
|
+
// child-filter binding.
|
|
297
|
+
await expect(
|
|
298
|
+
buildDetail(
|
|
299
|
+
{
|
|
300
|
+
org: ORG,
|
|
301
|
+
object: "Account",
|
|
302
|
+
fields: ["Id"],
|
|
303
|
+
childRelationships: [
|
|
304
|
+
{
|
|
305
|
+
relationshipName: "Contacts",
|
|
306
|
+
fields: ["Id"],
|
|
307
|
+
filter: { Title: { like: "$id" } },
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
noopPrimeDeps(),
|
|
312
|
+
),
|
|
313
|
+
).rejects.toThrow(/idVariable "id" collides with a \$id reference/);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("does NOT throw when same name appears as ID! (e.g., user explicitly references $id in a child filter as the same record)", async () => {
|
|
317
|
+
// The collision guard fires only when the existing type differs from `ID!`.
|
|
318
|
+
// A child filter that references the *same* `$id` is unusual but legal.
|
|
319
|
+
// This test pins that we don't over-trigger.
|
|
320
|
+
const out = await buildDetail(
|
|
321
|
+
{ org: ORG, object: "Account", fields: ["Id"], idVariable: "accountId" },
|
|
322
|
+
noopPrimeDeps(),
|
|
323
|
+
);
|
|
324
|
+
expect(out.variables.find((v) => v.name === "accountId")?.type).toBe("ID!");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("threads instanceUrl as 3rd arg to createSession", async () => {
|
|
328
|
+
const spy = vi.mocked(sessionModule.createSession);
|
|
329
|
+
spy.mockClear();
|
|
330
|
+
await buildDetail({ org: ORG, object: "Account", fields: ["Id", "Name"] }, noopPrimeDeps());
|
|
331
|
+
expect(spy).toHaveBeenCalledWith(ORG, "query", ORG_URL);
|
|
332
|
+
});
|
|
333
|
+
});
|