@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,432 @@
|
|
|
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 { rmSync } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { buildSchema, introspectionFromSchema } from "graphql";
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { atomicWriteJson } from "../../lib/fs-utils.js";
|
|
12
|
+
import {
|
|
13
|
+
schemaCacheKeyForInstanceUrl,
|
|
14
|
+
schemaDir,
|
|
15
|
+
type SchemaMetadata,
|
|
16
|
+
} from "../../lib/introspect.js";
|
|
17
|
+
import { type FieldMetadata, type ObjectInfoResult } from "../../lib/object-info.js";
|
|
18
|
+
import { type PrimeDeps } from "../../lib/prime-schema.js";
|
|
19
|
+
import { primeSchemaCache } from "../../lib/walker.js";
|
|
20
|
+
import { buildDiscover, type DiscoverDeps } from "../build-discover.js";
|
|
21
|
+
|
|
22
|
+
const ORG = "test-discover";
|
|
23
|
+
const ORG_URL = "https://test-discover.my.salesforce.com";
|
|
24
|
+
|
|
25
|
+
const SCHEMA = buildSchema(`
|
|
26
|
+
type Query { uiapi: UIAPI! }
|
|
27
|
+
type UIAPI { query: RecordQuery! }
|
|
28
|
+
|
|
29
|
+
type RecordQuery {
|
|
30
|
+
"Customer accounts"
|
|
31
|
+
Account(first: Int, after: String): AccountConnection!
|
|
32
|
+
Case(first: Int, after: String): CaseConnection!
|
|
33
|
+
Contact(first: Int, after: String): ContactConnection!
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type AccountConnection { edges: [AccountEdge!]! }
|
|
37
|
+
type AccountEdge { node: Account! }
|
|
38
|
+
type Account { Id: ID!, Name: StringValue }
|
|
39
|
+
|
|
40
|
+
type CaseConnection { edges: [CaseEdge!]! }
|
|
41
|
+
type CaseEdge { node: Case! }
|
|
42
|
+
type Case { Id: ID!, Subject: StringValue }
|
|
43
|
+
|
|
44
|
+
type ContactConnection { edges: [ContactEdge!]! }
|
|
45
|
+
type ContactEdge { node: Contact! }
|
|
46
|
+
type Contact { Id: ID!, LastName: StringValue }
|
|
47
|
+
|
|
48
|
+
type StringValue { value: String }
|
|
49
|
+
`);
|
|
50
|
+
|
|
51
|
+
primeSchemaCache(ORG, SCHEMA);
|
|
52
|
+
primeSchemaCache(ORG_URL, SCHEMA);
|
|
53
|
+
|
|
54
|
+
function makePrimeDeps(): PrimeDeps {
|
|
55
|
+
return {
|
|
56
|
+
getOrgAuth: async () => ({
|
|
57
|
+
alias: ORG,
|
|
58
|
+
username: "u",
|
|
59
|
+
instanceUrl: ORG_URL,
|
|
60
|
+
accessToken: "t",
|
|
61
|
+
orgId: "00D",
|
|
62
|
+
}),
|
|
63
|
+
downloadSchema: async (auth) => {
|
|
64
|
+
const cacheKey = schemaCacheKeyForInstanceUrl(auth.instanceUrl);
|
|
65
|
+
const filePath = path.join(schemaDir(), `${cacheKey}.json`);
|
|
66
|
+
atomicWriteJson(filePath, { data: introspectionFromSchema(SCHEMA) });
|
|
67
|
+
const meta: SchemaMetadata = {
|
|
68
|
+
cacheKey,
|
|
69
|
+
instanceUrl: auth.instanceUrl,
|
|
70
|
+
typeCount: 0,
|
|
71
|
+
downloadedAt: new Date().toISOString(),
|
|
72
|
+
filePath,
|
|
73
|
+
};
|
|
74
|
+
return meta;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function makeField(overrides: Partial<FieldMetadata>): FieldMetadata {
|
|
80
|
+
return {
|
|
81
|
+
apiName: "Field",
|
|
82
|
+
label: "Field",
|
|
83
|
+
dataType: "STRING",
|
|
84
|
+
required: false,
|
|
85
|
+
createable: false,
|
|
86
|
+
updateable: false,
|
|
87
|
+
calculated: false,
|
|
88
|
+
custom: false,
|
|
89
|
+
filterable: true,
|
|
90
|
+
sortable: true,
|
|
91
|
+
nameField: false,
|
|
92
|
+
reference: false,
|
|
93
|
+
relationshipName: null,
|
|
94
|
+
compound: false,
|
|
95
|
+
compoundFieldName: null,
|
|
96
|
+
defaultedOnCreate: false,
|
|
97
|
+
extraTypeInfo: null,
|
|
98
|
+
inlineHelpText: null,
|
|
99
|
+
precision: 0,
|
|
100
|
+
scale: 0,
|
|
101
|
+
referenceToInfos: [],
|
|
102
|
+
controllerName: null,
|
|
103
|
+
controllingFields: [],
|
|
104
|
+
...overrides,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const ACCOUNT_INFO: ObjectInfoResult = {
|
|
109
|
+
apiName: "Account",
|
|
110
|
+
label: "Account",
|
|
111
|
+
labelPlural: "Accounts",
|
|
112
|
+
createable: true,
|
|
113
|
+
deletable: true,
|
|
114
|
+
updateable: true,
|
|
115
|
+
queryable: true,
|
|
116
|
+
searchable: true,
|
|
117
|
+
custom: false,
|
|
118
|
+
keyPrefix: "001",
|
|
119
|
+
nameFields: ["Name"],
|
|
120
|
+
defaultRecordTypeId: null,
|
|
121
|
+
fields: [
|
|
122
|
+
makeField({
|
|
123
|
+
apiName: "Id",
|
|
124
|
+
label: "Account ID",
|
|
125
|
+
dataType: "ID",
|
|
126
|
+
filterable: true,
|
|
127
|
+
sortable: true,
|
|
128
|
+
createable: false,
|
|
129
|
+
updateable: false,
|
|
130
|
+
}),
|
|
131
|
+
makeField({
|
|
132
|
+
apiName: "Name",
|
|
133
|
+
label: "Account Name",
|
|
134
|
+
dataType: "STRING",
|
|
135
|
+
nameField: true,
|
|
136
|
+
required: true,
|
|
137
|
+
createable: true,
|
|
138
|
+
updateable: true,
|
|
139
|
+
}),
|
|
140
|
+
makeField({
|
|
141
|
+
apiName: "Industry",
|
|
142
|
+
label: "Industry",
|
|
143
|
+
dataType: "PICKLIST",
|
|
144
|
+
createable: true,
|
|
145
|
+
updateable: true,
|
|
146
|
+
}),
|
|
147
|
+
makeField({
|
|
148
|
+
apiName: "OwnerId",
|
|
149
|
+
label: "Owner ID",
|
|
150
|
+
dataType: "REFERENCE",
|
|
151
|
+
reference: true,
|
|
152
|
+
relationshipName: "Owner",
|
|
153
|
+
referenceToInfos: [
|
|
154
|
+
{ apiName: "User", nameFields: ["Name"] },
|
|
155
|
+
{ apiName: "Group", nameFields: ["Name"] },
|
|
156
|
+
],
|
|
157
|
+
createable: true,
|
|
158
|
+
updateable: true,
|
|
159
|
+
}),
|
|
160
|
+
makeField({
|
|
161
|
+
apiName: "BillingAddress",
|
|
162
|
+
label: "Billing Address",
|
|
163
|
+
dataType: "ADDRESS",
|
|
164
|
+
compound: true,
|
|
165
|
+
filterable: false,
|
|
166
|
+
sortable: false,
|
|
167
|
+
}),
|
|
168
|
+
makeField({
|
|
169
|
+
apiName: "AutoNumber__c",
|
|
170
|
+
label: "Auto Number",
|
|
171
|
+
dataType: "STRING",
|
|
172
|
+
required: true,
|
|
173
|
+
createable: true,
|
|
174
|
+
defaultedOnCreate: true,
|
|
175
|
+
}),
|
|
176
|
+
],
|
|
177
|
+
childRelationships: [
|
|
178
|
+
{ childObjectApiName: "Contact", fieldName: "AccountId", relationshipName: "Contacts" },
|
|
179
|
+
{ childObjectApiName: "Case", fieldName: "AccountId", relationshipName: null },
|
|
180
|
+
],
|
|
181
|
+
recordTypeInfos: [],
|
|
182
|
+
picklists: [
|
|
183
|
+
{
|
|
184
|
+
apiName: "Industry",
|
|
185
|
+
label: "Industry",
|
|
186
|
+
required: false,
|
|
187
|
+
values: [
|
|
188
|
+
{ value: "Technology", label: "Technology" },
|
|
189
|
+
{ value: "Finance", label: "Finance" },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
fetchedAt: new Date().toISOString(),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
function makeDeps(): DiscoverDeps {
|
|
197
|
+
return {
|
|
198
|
+
primeDeps: makePrimeDeps(),
|
|
199
|
+
getOrgAuth: async () => ({
|
|
200
|
+
alias: ORG,
|
|
201
|
+
username: "u",
|
|
202
|
+
instanceUrl: ORG_URL,
|
|
203
|
+
accessToken: "t",
|
|
204
|
+
orgId: "00D",
|
|
205
|
+
}),
|
|
206
|
+
getObjectInfo: async (_auth, _alias, name) => {
|
|
207
|
+
if (name === "Account") return ACCOUNT_INFO;
|
|
208
|
+
throw new Error(`No fixture for ${name}`);
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
describe("intent/build-discover", () => {
|
|
214
|
+
describe("list_objects mode (FR-11.1)", () => {
|
|
215
|
+
it("returns queryable SObjects sorted alphabetically", async () => {
|
|
216
|
+
const out = await buildDiscover({ org: ORG, mode: "list_objects" }, makeDeps());
|
|
217
|
+
expect(out.mode).toBe("list_objects");
|
|
218
|
+
if (out.mode !== "list_objects") return;
|
|
219
|
+
expect(out.objects.map((o) => o.name)).toEqual(["Account", "Case", "Contact"]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("includes the schema description as the label when present", async () => {
|
|
223
|
+
const out = await buildDiscover({ org: ORG, mode: "list_objects" }, makeDeps());
|
|
224
|
+
if (out.mode !== "list_objects") return;
|
|
225
|
+
const account = out.objects.find((o) => o.name === "Account");
|
|
226
|
+
expect(account?.label).toBe("Customer accounts");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("filters by search substring (case-insensitive)", async () => {
|
|
230
|
+
const out = await buildDiscover(
|
|
231
|
+
{ org: ORG, mode: "list_objects", search: "cas" },
|
|
232
|
+
makeDeps(),
|
|
233
|
+
);
|
|
234
|
+
if (out.mode !== "list_objects") return;
|
|
235
|
+
expect(out.objects.map((o) => o.name)).toEqual(["Case"]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("excludes non-Connection fields like search/aggregate helpers", async () => {
|
|
239
|
+
const ALIAS = "test-discover-helpers";
|
|
240
|
+
const URL = "https://test-discover-helpers.my.salesforce.com";
|
|
241
|
+
const helperSchema = buildSchema(`
|
|
242
|
+
type Query { uiapi: UIAPI! }
|
|
243
|
+
type UIAPI { query: RecordQuery! }
|
|
244
|
+
type RecordQuery {
|
|
245
|
+
Account(first: Int): AccountConnection!
|
|
246
|
+
search(term: String!): SearchResult!
|
|
247
|
+
aggregate: AggregateResult!
|
|
248
|
+
}
|
|
249
|
+
type AccountConnection { edges: [AccountEdge!]! }
|
|
250
|
+
type AccountEdge { node: Account! }
|
|
251
|
+
type Account { Id: ID! }
|
|
252
|
+
type SearchResult { hits: Int }
|
|
253
|
+
type AggregateResult { total: Int }
|
|
254
|
+
`);
|
|
255
|
+
primeSchemaCache(ALIAS, helperSchema);
|
|
256
|
+
primeSchemaCache(URL, helperSchema);
|
|
257
|
+
|
|
258
|
+
const helperDeps: DiscoverDeps = {
|
|
259
|
+
primeDeps: {
|
|
260
|
+
getOrgAuth: async () => ({
|
|
261
|
+
alias: ALIAS,
|
|
262
|
+
username: "u",
|
|
263
|
+
instanceUrl: URL,
|
|
264
|
+
accessToken: "t",
|
|
265
|
+
orgId: "00D",
|
|
266
|
+
}),
|
|
267
|
+
downloadSchema: async (auth) => {
|
|
268
|
+
const cacheKey = schemaCacheKeyForInstanceUrl(auth.instanceUrl);
|
|
269
|
+
const filePath = path.join(schemaDir(), `${cacheKey}.json`);
|
|
270
|
+
atomicWriteJson(filePath, { data: introspectionFromSchema(helperSchema) });
|
|
271
|
+
return {
|
|
272
|
+
cacheKey,
|
|
273
|
+
instanceUrl: auth.instanceUrl,
|
|
274
|
+
typeCount: 0,
|
|
275
|
+
downloadedAt: new Date().toISOString(),
|
|
276
|
+
filePath,
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const out = await buildDiscover({ org: ALIAS, mode: "list_objects" }, helperDeps);
|
|
283
|
+
if (out.mode !== "list_objects") return;
|
|
284
|
+
expect(out.objects.map((o) => o.name)).toEqual(["Account"]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("surfaces the FR-13.3 priming note in warnings on cold prime", async () => {
|
|
288
|
+
const ALIAS = "test-discover-cold";
|
|
289
|
+
const URL = "https://test-discover-cold.my.salesforce.com";
|
|
290
|
+
const coldSchema = buildSchema(`
|
|
291
|
+
type Query { uiapi: UIAPI! }
|
|
292
|
+
type UIAPI { query: RecordQuery! }
|
|
293
|
+
type RecordQuery { Account(first: Int): AccountConnection! }
|
|
294
|
+
type AccountConnection { edges: [AccountEdge!]! }
|
|
295
|
+
type AccountEdge { node: Account! }
|
|
296
|
+
type Account { Id: ID! }
|
|
297
|
+
`);
|
|
298
|
+
primeSchemaCache(URL, coldSchema);
|
|
299
|
+
|
|
300
|
+
// downloadSchema below writes to the real ~/.graphiti/schemas/ dir.
|
|
301
|
+
// The "cold prime" assertion requires that cache file to be ABSENT so
|
|
302
|
+
// buildDiscover primes it and emits the FR-13.3 warning. A prior run
|
|
303
|
+
// of this test leaves the file behind, so on the next run the cache is
|
|
304
|
+
// already present, no priming happens, and the warning assertion fails.
|
|
305
|
+
// Clear it before (restore the cold precondition) and after (no leak).
|
|
306
|
+
const coldCachePath = path.join(schemaDir(), `${schemaCacheKeyForInstanceUrl(URL)}.json`);
|
|
307
|
+
rmSync(coldCachePath, { force: true });
|
|
308
|
+
|
|
309
|
+
const coldDeps: DiscoverDeps = {
|
|
310
|
+
primeDeps: {
|
|
311
|
+
getOrgAuth: async () => ({
|
|
312
|
+
alias: ALIAS,
|
|
313
|
+
username: "u",
|
|
314
|
+
instanceUrl: URL,
|
|
315
|
+
accessToken: "t",
|
|
316
|
+
orgId: "00D",
|
|
317
|
+
}),
|
|
318
|
+
downloadSchema: async (auth) => {
|
|
319
|
+
const cacheKey = schemaCacheKeyForInstanceUrl(auth.instanceUrl);
|
|
320
|
+
const filePath = path.join(schemaDir(), `${cacheKey}.json`);
|
|
321
|
+
atomicWriteJson(filePath, { data: introspectionFromSchema(coldSchema) });
|
|
322
|
+
return {
|
|
323
|
+
cacheKey,
|
|
324
|
+
instanceUrl: auth.instanceUrl,
|
|
325
|
+
typeCount: 0,
|
|
326
|
+
downloadedAt: new Date().toISOString(),
|
|
327
|
+
filePath,
|
|
328
|
+
};
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const out = await buildDiscover({ org: ALIAS, mode: "list_objects" }, coldDeps);
|
|
335
|
+
if (out.mode !== "list_objects") return;
|
|
336
|
+
expect(out.warnings).toBeDefined();
|
|
337
|
+
expect(out.warnings?.[0]).toMatch(/Primed schema cache for "test-discover-cold"/);
|
|
338
|
+
} finally {
|
|
339
|
+
rmSync(coldCachePath, { force: true });
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe("describe_object mode (FR-11.2)", () => {
|
|
345
|
+
it("returns object metadata with picklists, child relationships, parent refs", async () => {
|
|
346
|
+
const out = await buildDiscover(
|
|
347
|
+
{ org: ORG, mode: "describe_object", object: "Account" },
|
|
348
|
+
makeDeps(),
|
|
349
|
+
);
|
|
350
|
+
expect(out.mode).toBe("describe_object");
|
|
351
|
+
if (out.mode !== "describe_object") return;
|
|
352
|
+
|
|
353
|
+
expect(out.object.name).toBe("Account");
|
|
354
|
+
const fieldNames = out.object.fields.map((f) => f.name);
|
|
355
|
+
expect(fieldNames).toContain("Id");
|
|
356
|
+
expect(fieldNames).toContain("Name");
|
|
357
|
+
|
|
358
|
+
const industry = out.object.fields.find((f) => f.name === "Industry");
|
|
359
|
+
expect(industry?.picklistValues).toEqual(["Technology", "Finance"]);
|
|
360
|
+
|
|
361
|
+
expect(out.object.childRelationships).toEqual([
|
|
362
|
+
{ relationshipName: "Contacts", childObject: "Contact" },
|
|
363
|
+
]);
|
|
364
|
+
|
|
365
|
+
expect(out.object.parentReferences).toEqual([
|
|
366
|
+
{ field: "OwnerId", targetObjects: ["User", "Group"] },
|
|
367
|
+
]);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("filterableFields excludes compound fields", async () => {
|
|
371
|
+
const out = await buildDiscover(
|
|
372
|
+
{ org: ORG, mode: "describe_object", object: "Account" },
|
|
373
|
+
makeDeps(),
|
|
374
|
+
);
|
|
375
|
+
if (out.mode !== "describe_object") return;
|
|
376
|
+
expect(out.object.filterableFields).not.toContain("BillingAddress");
|
|
377
|
+
expect(out.object.filterableFields).toContain("Name");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("orderByExample uses the first sortable non-compound field", async () => {
|
|
381
|
+
const out = await buildDiscover(
|
|
382
|
+
{ org: ORG, mode: "describe_object", object: "Account" },
|
|
383
|
+
makeDeps(),
|
|
384
|
+
);
|
|
385
|
+
if (out.mode !== "describe_object") return;
|
|
386
|
+
expect(Object.keys(out.object.orderByExample)).toHaveLength(1);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("requiredOnCreate excludes fields defaulted on create", async () => {
|
|
390
|
+
const out = await buildDiscover(
|
|
391
|
+
{ org: ORG, mode: "describe_object", object: "Account" },
|
|
392
|
+
makeDeps(),
|
|
393
|
+
);
|
|
394
|
+
if (out.mode !== "describe_object") return;
|
|
395
|
+
expect(out.object.requiredOnCreate).toEqual(["Name"]);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("throws when object is missing", async () => {
|
|
399
|
+
await expect(
|
|
400
|
+
buildDiscover({ org: ORG, mode: "describe_object" }, makeDeps()),
|
|
401
|
+
).rejects.toThrow(/requires "object"/);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe("describe_field mode (FR-11.3)", () => {
|
|
406
|
+
it("returns the named field's metadata", async () => {
|
|
407
|
+
const out = await buildDiscover(
|
|
408
|
+
{ org: ORG, mode: "describe_field", object: "Account", field: "Industry" },
|
|
409
|
+
makeDeps(),
|
|
410
|
+
);
|
|
411
|
+
expect(out.mode).toBe("describe_field");
|
|
412
|
+
if (out.mode !== "describe_field") return;
|
|
413
|
+
expect(out.field.name).toBe("Industry");
|
|
414
|
+
expect(out.field.picklistValues).toEqual(["Technology", "Finance"]);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("throws when field is missing from spec", async () => {
|
|
418
|
+
await expect(
|
|
419
|
+
buildDiscover({ org: ORG, mode: "describe_field", object: "Account" }, makeDeps()),
|
|
420
|
+
).rejects.toThrow(/requires "field"/);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("throws when field is not on the object", async () => {
|
|
424
|
+
await expect(
|
|
425
|
+
buildDiscover(
|
|
426
|
+
{ org: ORG, mode: "describe_field", object: "Account", field: "DoesNotExist" },
|
|
427
|
+
makeDeps(),
|
|
428
|
+
),
|
|
429
|
+
).rejects.toThrow(/not found on "Account"/);
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
});
|
|
@@ -0,0 +1,284 @@
|
|
|
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 { buildList } from "../build-list.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! }
|
|
22
|
+
|
|
23
|
+
type RecordQuery {
|
|
24
|
+
Account(first: Int, after: String, where: Account_Filter, orderBy: Account_OrderBy, scope: Scope): AccountConnection!
|
|
25
|
+
Case(first: Int, after: String, where: Case_Filter, orderBy: Case_OrderBy, scope: Scope): CaseConnection!
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
enum Scope { MINE EVERYTHING }
|
|
29
|
+
enum Order { ASC DESC }
|
|
30
|
+
|
|
31
|
+
input Account_Filter { Industry: PicklistOperators, Name: StringOperators }
|
|
32
|
+
input Account_OrderBy { Name: OrderByClause, Industry: OrderByClause }
|
|
33
|
+
input Case_Filter { Status: PicklistOperators, Priority: PicklistOperators }
|
|
34
|
+
input Case_OrderBy { CreatedDate: OrderByClause }
|
|
35
|
+
input PicklistOperators { eq: String, ne: String, in: [String!] }
|
|
36
|
+
input StringOperators { eq: String, like: String }
|
|
37
|
+
input OrderByClause { order: Order!, nulls: NullsOrder }
|
|
38
|
+
enum NullsOrder { FIRST LAST }
|
|
39
|
+
|
|
40
|
+
type AccountConnection {
|
|
41
|
+
edges: [AccountEdge!]!
|
|
42
|
+
pageInfo: PageInfo!
|
|
43
|
+
totalCount: Int
|
|
44
|
+
}
|
|
45
|
+
type AccountEdge { node: Account! }
|
|
46
|
+
type CaseConnection {
|
|
47
|
+
edges: [CaseEdge!]!
|
|
48
|
+
pageInfo: PageInfo!
|
|
49
|
+
}
|
|
50
|
+
type CaseEdge { node: Case! }
|
|
51
|
+
type PageInfo { hasNextPage: Boolean!, endCursor: String }
|
|
52
|
+
|
|
53
|
+
type Account {
|
|
54
|
+
Id: ID!
|
|
55
|
+
Name: StringValue
|
|
56
|
+
Industry: StringValue
|
|
57
|
+
Owner: OwnerUnion
|
|
58
|
+
Contacts(first: Int, where: Contact_Filter, orderBy: Contact_OrderBy): ContactConnection
|
|
59
|
+
}
|
|
60
|
+
type Case {
|
|
61
|
+
Id: ID!
|
|
62
|
+
Subject: StringValue
|
|
63
|
+
Status: StringValue
|
|
64
|
+
Owner: OwnerUnion
|
|
65
|
+
Account: Account
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type ContactConnection { edges: [ContactEdge!]! }
|
|
69
|
+
type ContactEdge { node: Contact! }
|
|
70
|
+
input Contact_Filter { Title: StringOperators }
|
|
71
|
+
input Contact_OrderBy { LastName: OrderByClause }
|
|
72
|
+
|
|
73
|
+
type Contact {
|
|
74
|
+
Id: ID!
|
|
75
|
+
LastName: StringValue
|
|
76
|
+
Title: StringValue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
union OwnerUnion = User | Group
|
|
80
|
+
type User { Id: ID!, Name: StringValue, Email: StringValue }
|
|
81
|
+
type Group { Id: ID!, Name: StringValue }
|
|
82
|
+
|
|
83
|
+
type StringValue { value: String }
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
const ORG = "test-list";
|
|
87
|
+
const ORG_URL = "https://test-list.my.salesforce.com";
|
|
88
|
+
const SCHEMA = buildSchema(SCHEMA_SDL);
|
|
89
|
+
// Prime the in-memory schema cache by both alias and URL so getSchema()
|
|
90
|
+
// resolves either key without touching disk or the keychain.
|
|
91
|
+
primeSchemaCache(ORG, SCHEMA);
|
|
92
|
+
primeSchemaCache(ORG_URL, SCHEMA);
|
|
93
|
+
|
|
94
|
+
const noopPrimeDeps = () => makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
|
|
95
|
+
|
|
96
|
+
describe("intent/build-list", () => {
|
|
97
|
+
it("scalar fields select { value } except Id (FR-4.1)", async () => {
|
|
98
|
+
const out = await buildList(
|
|
99
|
+
{ org: ORG, object: "Account", fields: ["Id", "Name"] },
|
|
100
|
+
noopPrimeDeps(),
|
|
101
|
+
);
|
|
102
|
+
expect(out.query).toMatch(/\bId\b/);
|
|
103
|
+
expect(out.query).toMatch(/Name\s*\{\s*value\s*\}/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("default operationName is <Object>List", async () => {
|
|
107
|
+
const out = await buildList({ org: ORG, object: "Case", fields: ["Id"] }, noopPrimeDeps());
|
|
108
|
+
expect(out.query).toMatch(/\bquery\s+CaseList\b/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("respects custom operationName", async () => {
|
|
112
|
+
const out = await buildList(
|
|
113
|
+
{ org: ORG, object: "Case", fields: ["Id"], operationName: "MyCases" },
|
|
114
|
+
noopPrimeDeps(),
|
|
115
|
+
);
|
|
116
|
+
expect(out.query).toMatch(/\bquery\s+MyCases\b/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("rejects an operationName that is not a valid GraphQL Name", async () => {
|
|
120
|
+
await expect(
|
|
121
|
+
buildList(
|
|
122
|
+
{ org: ORG, object: "Account", fields: ["Id"], operationName: "has spaces" },
|
|
123
|
+
noopPrimeDeps(),
|
|
124
|
+
),
|
|
125
|
+
).rejects.toThrow(/buildList: operationName 'has spaces' is not a valid GraphQL Name/);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("rejects an object that is not a valid GraphQL Name", async () => {
|
|
129
|
+
await expect(
|
|
130
|
+
buildList({ org: ORG, object: "Order Item", fields: ["Id"] }, noopPrimeDeps()),
|
|
131
|
+
).rejects.toThrow(/buildList: object 'Order Item' is not a valid GraphQL Name/);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("declares $after and selects pageInfo (FR-7)", async () => {
|
|
135
|
+
const out = await buildList({ org: ORG, object: "Case", fields: ["Id"] }, noopPrimeDeps());
|
|
136
|
+
expect(out.variables.find((v) => v.name === "after")).toBeDefined();
|
|
137
|
+
expect(out.query).toMatch(/\$after\s*:\s*String\b/);
|
|
138
|
+
expect(out.query).toMatch(/pageInfo\s*\{[^}]*hasNextPage[^}]*endCursor/s);
|
|
139
|
+
expect(out.query).toMatch(/first\s*:\s*10\b/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("parentFields walks dotted path with value wrapping (FR-4.2)", async () => {
|
|
143
|
+
const out = await buildList(
|
|
144
|
+
{ org: ORG, object: "Case", fields: ["Id"], parentFields: ["Account.Name"] },
|
|
145
|
+
noopPrimeDeps(),
|
|
146
|
+
);
|
|
147
|
+
expect(out.query).toMatch(/Account\s*\{[^}]*Name\s*\{\s*value\s*\}/s);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("parentFields expands polymorphic union into inline fragments (FR-4.3)", async () => {
|
|
151
|
+
const out = await buildList(
|
|
152
|
+
{ org: ORG, object: "Case", fields: ["Id"], parentFields: ["Owner.Name"] },
|
|
153
|
+
noopPrimeDeps(),
|
|
154
|
+
);
|
|
155
|
+
expect(out.query).toMatch(/\.\.\.\s+on\s+User\s*\{[^}]*Name\s*\{\s*value\s*\}/s);
|
|
156
|
+
expect(out.query).toMatch(/\.\.\.\s+on\s+Group\s*\{[^}]*Name\s*\{\s*value\s*\}/s);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("parentFields skips union members lacking the field (FR-4.3)", async () => {
|
|
160
|
+
const out = await buildList(
|
|
161
|
+
{ org: ORG, object: "Case", fields: ["Id"], parentFields: ["Owner.Email"] },
|
|
162
|
+
noopPrimeDeps(),
|
|
163
|
+
);
|
|
164
|
+
expect(out.query).toMatch(/\.\.\.\s+on\s+User\s*\{[^}]*Email/s);
|
|
165
|
+
expect(out.query).not.toMatch(/\.\.\.\s+on\s+Group\s*\{[^}]*Email/s);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("childRelationships rendered as edges/node connection (FR-4.4)", async () => {
|
|
169
|
+
const out = await buildList(
|
|
170
|
+
{
|
|
171
|
+
org: ORG,
|
|
172
|
+
object: "Account",
|
|
173
|
+
fields: ["Id"],
|
|
174
|
+
childRelationships: [
|
|
175
|
+
{ relationshipName: "Contacts", fields: ["Id", "LastName"], first: 5 },
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
noopPrimeDeps(),
|
|
179
|
+
);
|
|
180
|
+
expect(out.query).toMatch(/Contacts\s*\([^)]*first:\s*5/);
|
|
181
|
+
expect(out.query).toMatch(
|
|
182
|
+
/Contacts\s*\([^)]*\)\s*\{\s*edges\s*\{\s*node\s*\{[^}]*LastName\s*\{\s*value\s*\}/s,
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("filter with $varName promotes typed variable (FR-5.1, 5.2)", async () => {
|
|
187
|
+
const out = await buildList(
|
|
188
|
+
{
|
|
189
|
+
org: ORG,
|
|
190
|
+
object: "Case",
|
|
191
|
+
fields: ["Id"],
|
|
192
|
+
filter: { Status: { eq: "$status" } },
|
|
193
|
+
},
|
|
194
|
+
noopPrimeDeps(),
|
|
195
|
+
);
|
|
196
|
+
const status = out.variables.find((v) => v.name === "status");
|
|
197
|
+
expect(status).toBeDefined();
|
|
198
|
+
expect(status!.required).toBe(false);
|
|
199
|
+
expect(out.query).toMatch(/\$status\b/);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("literal filter values are inlined, no variable promoted", async () => {
|
|
203
|
+
const out = await buildList(
|
|
204
|
+
{
|
|
205
|
+
org: ORG,
|
|
206
|
+
object: "Case",
|
|
207
|
+
fields: ["Id"],
|
|
208
|
+
filter: { Status: { eq: "New" } },
|
|
209
|
+
},
|
|
210
|
+
noopPrimeDeps(),
|
|
211
|
+
);
|
|
212
|
+
expect(out.variables.find((v) => v.name === "status")).toBeUndefined();
|
|
213
|
+
expect(out.query).toMatch(/Status\s*:\s*\{\s*eq\s*:\s*"New"/);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("orderBy emitted as singleton object (FR-6.1)", async () => {
|
|
217
|
+
const out = await buildList(
|
|
218
|
+
{
|
|
219
|
+
org: ORG,
|
|
220
|
+
object: "Case",
|
|
221
|
+
fields: ["Id"],
|
|
222
|
+
orderBy: { CreatedDate: { order: "ASC" } },
|
|
223
|
+
},
|
|
224
|
+
noopPrimeDeps(),
|
|
225
|
+
);
|
|
226
|
+
expect(out.query).toMatch(/orderBy\s*:\s*\{\s*CreatedDate/);
|
|
227
|
+
expect(out.query).not.toMatch(/orderBy\s*:\s*\[/);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("orderBy array is collapsed to first element (FR-6.2)", async () => {
|
|
231
|
+
const out = await buildList(
|
|
232
|
+
{
|
|
233
|
+
org: ORG,
|
|
234
|
+
object: "Account",
|
|
235
|
+
fields: ["Id"],
|
|
236
|
+
orderBy: [{ Name: { order: "ASC" } }, { Industry: { order: "DESC" } }],
|
|
237
|
+
},
|
|
238
|
+
noopPrimeDeps(),
|
|
239
|
+
);
|
|
240
|
+
expect(out.query).toMatch(/orderBy\s*:\s*\{\s*Name/);
|
|
241
|
+
expect(out.query).not.toMatch(/Industry/);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("scope literal is set on connection", async () => {
|
|
245
|
+
const out = await buildList(
|
|
246
|
+
{ org: ORG, object: "Case", fields: ["Id"], scope: "MINE" },
|
|
247
|
+
noopPrimeDeps(),
|
|
248
|
+
);
|
|
249
|
+
expect(out.query).toMatch(/scope\s*:\s*MINE/);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("$varName scope promotes a variable", async () => {
|
|
253
|
+
const out = await buildList(
|
|
254
|
+
{ org: ORG, object: "Case", fields: ["Id"], scope: "$myScope" },
|
|
255
|
+
noopPrimeDeps(),
|
|
256
|
+
);
|
|
257
|
+
expect(out.variables.find((v) => v.name === "myScope")).toBeDefined();
|
|
258
|
+
expect(out.query).toMatch(/scope\s*:\s*\$myScope/);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("unresolvable filter input path falls back to String (FR-5.4)", async () => {
|
|
262
|
+
// `Account_Filter` has `Industry` and `Name`, but no `BogusField` —
|
|
263
|
+
// inferTypeFromArgsPath will throw, and promoteVariables falls back to String.
|
|
264
|
+
const out = await buildList(
|
|
265
|
+
{
|
|
266
|
+
org: ORG,
|
|
267
|
+
object: "Account",
|
|
268
|
+
fields: ["Id"],
|
|
269
|
+
filter: { BogusField: { eq: "$bogus" } },
|
|
270
|
+
},
|
|
271
|
+
noopPrimeDeps(),
|
|
272
|
+
);
|
|
273
|
+
const bogus = out.variables.find((v) => v.name === "bogus");
|
|
274
|
+
expect(bogus).toBeDefined();
|
|
275
|
+
expect(bogus!.type).toBe("String");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("threads instanceUrl as 3rd arg to createSession", async () => {
|
|
279
|
+
const spy = vi.mocked(sessionModule.createSession);
|
|
280
|
+
spy.mockClear();
|
|
281
|
+
await buildList({ org: ORG, object: "Case", fields: ["Id"] }, noopPrimeDeps());
|
|
282
|
+
expect(spy).toHaveBeenCalledWith(ORG, "query", ORG_URL);
|
|
283
|
+
});
|
|
284
|
+
});
|