@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,899 @@
|
|
|
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 type { GraphQLSchema, GraphQLType, GraphQLOutputType } from "graphql";
|
|
8
|
+
import {
|
|
9
|
+
isEnumType,
|
|
10
|
+
isScalarType,
|
|
11
|
+
isInputObjectType,
|
|
12
|
+
isListType,
|
|
13
|
+
isNonNullType,
|
|
14
|
+
getNamedType,
|
|
15
|
+
} from "graphql";
|
|
16
|
+
import { getCachedObjectInfo, getRequiredCreateFields } from "./object-info.js";
|
|
17
|
+
import type {
|
|
18
|
+
OperationType,
|
|
19
|
+
QuerySession,
|
|
20
|
+
FieldProjectionNode,
|
|
21
|
+
FragmentProjectionNode,
|
|
22
|
+
VariableDefinition,
|
|
23
|
+
} from "./session.js";
|
|
24
|
+
import { getChildren } from "./session.js";
|
|
25
|
+
import { resolvePath, getRawFieldType, getFieldDescription, type WalkerResult } from "./walker.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Collects all SObject names referenced in a session's projection nodes.
|
|
29
|
+
* Includes primary SObjects (from query/mutation paths) and parent/child
|
|
30
|
+
* relationship SObjects detected via schema type resolution.
|
|
31
|
+
* Used to pre-warm the ObjectInfo cache before generating types.
|
|
32
|
+
*/
|
|
33
|
+
export function collectSessionSObjects(session: QuerySession, schema: GraphQLSchema): Set<string> {
|
|
34
|
+
const sObjects = new Set<string>();
|
|
35
|
+
for (const node of session.nodes) {
|
|
36
|
+
if (node.kind !== "field") continue;
|
|
37
|
+
const fieldNode = node as FieldProjectionNode;
|
|
38
|
+
const name = detectSObjectFromPath(fieldNode.schemaPath);
|
|
39
|
+
if (name) sObjects.add(name);
|
|
40
|
+
// Also resolve the node's schema path to detect relationship SObjects
|
|
41
|
+
try {
|
|
42
|
+
const wr = resolvePath(schema, session.operation, fieldNode.schemaPath);
|
|
43
|
+
// If the resolved type looks like an SObject name (PascalCase, no Connection/Edge suffix)
|
|
44
|
+
if (
|
|
45
|
+
wr.typeName &&
|
|
46
|
+
!wr.typeName.endsWith("Connection") &&
|
|
47
|
+
!wr.typeName.endsWith("Edge") &&
|
|
48
|
+
!wr.typeName.endsWith("Value") &&
|
|
49
|
+
!wr.isLeaf &&
|
|
50
|
+
/^[A-Z]/.test(wr.typeName)
|
|
51
|
+
) {
|
|
52
|
+
sObjects.add(wr.typeName);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
/* ignore resolution failures */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return sObjects;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const OUTPUT_SCALAR_MAP: Record<string, string> = {
|
|
62
|
+
String: "string",
|
|
63
|
+
ID: "string",
|
|
64
|
+
Int: "number",
|
|
65
|
+
Float: "number",
|
|
66
|
+
Boolean: "boolean",
|
|
67
|
+
Picklist: "string",
|
|
68
|
+
MultiPicklist: "string",
|
|
69
|
+
Currency: "number",
|
|
70
|
+
Date: "string",
|
|
71
|
+
DateTime: "string",
|
|
72
|
+
Time: "string",
|
|
73
|
+
Double: "number",
|
|
74
|
+
Percent: "number",
|
|
75
|
+
Email: "string",
|
|
76
|
+
PhoneNumber: "string",
|
|
77
|
+
Url: "string",
|
|
78
|
+
TextArea: "string",
|
|
79
|
+
LongTextArea: "string",
|
|
80
|
+
RichTextArea: "string",
|
|
81
|
+
EncryptedString: "string",
|
|
82
|
+
Base64: "string",
|
|
83
|
+
IdOrRef: "string",
|
|
84
|
+
JSON: "unknown",
|
|
85
|
+
Long: "number",
|
|
86
|
+
BigInteger: "number",
|
|
87
|
+
BigDecimal: "number",
|
|
88
|
+
Short: "number",
|
|
89
|
+
Byte: "number",
|
|
90
|
+
Char: "number",
|
|
91
|
+
Latitude: "number",
|
|
92
|
+
Longitude: "number",
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const INPUT_SCALAR_MAP: Record<string, string> = {
|
|
96
|
+
...OUTPUT_SCALAR_MAP,
|
|
97
|
+
Currency: "number | string",
|
|
98
|
+
BigDecimal: "number | string",
|
|
99
|
+
Double: "number | string",
|
|
100
|
+
Percent: "number | string",
|
|
101
|
+
Longitude: "number | string",
|
|
102
|
+
Latitude: "number | string",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
interface PicklistTypeInfo {
|
|
106
|
+
typeName: string;
|
|
107
|
+
values: string[];
|
|
108
|
+
fieldLabel: string;
|
|
109
|
+
objectName: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Supported target languages for `codegen`.
|
|
114
|
+
* Currently only `typescript` is implemented; add new entries here when
|
|
115
|
+
* introducing additional emitters.
|
|
116
|
+
*/
|
|
117
|
+
export const SUPPORTED_CODEGEN_LANGUAGES = ["typescript"] as const;
|
|
118
|
+
export type CodegenLanguage = (typeof SUPPORTED_CODEGEN_LANGUAGES)[number];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Accepts either the canonical language name (`typescript`) or a common
|
|
122
|
+
* alias (e.g. `ts`), normalising to a `CodegenLanguage`. Returns `null`
|
|
123
|
+
* when the input does not match a supported language.
|
|
124
|
+
*/
|
|
125
|
+
export function normalizeCodegenLanguage(value: string): CodegenLanguage | null {
|
|
126
|
+
const v = value.trim().toLowerCase();
|
|
127
|
+
if (v === "typescript" || v === "ts") return "typescript";
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface CodegenOptions {
|
|
132
|
+
typeName?: string;
|
|
133
|
+
language?: CodegenLanguage;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function generateTypes(
|
|
137
|
+
session: QuerySession,
|
|
138
|
+
schema: GraphQLSchema,
|
|
139
|
+
options: CodegenOptions = {},
|
|
140
|
+
): string {
|
|
141
|
+
const language = options.language ?? "typescript";
|
|
142
|
+
if (language !== "typescript") {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Unsupported codegen language: "${language}". Supported languages: ${SUPPORTED_CODEGEN_LANGUAGES.join(", ")}.`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const typeName = options.typeName;
|
|
148
|
+
const rootChildren = getChildren(session, null);
|
|
149
|
+
if (rootChildren.length === 0) {
|
|
150
|
+
return `// No fields selected in this session.\n`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const picklists: PicklistTypeInfo[] = [];
|
|
154
|
+
const resultType = buildNodeType(session, schema, null, 1, session.operation, [], picklists);
|
|
155
|
+
|
|
156
|
+
const name = typeName ?? session.name ?? session.id;
|
|
157
|
+
const pascalName = toPascalCase(name);
|
|
158
|
+
|
|
159
|
+
const parts: string[] = [];
|
|
160
|
+
|
|
161
|
+
// Collect input type declarations first (may add picklist types)
|
|
162
|
+
const inputTypeDecls = new Map<string, string>();
|
|
163
|
+
if (session.variables.length > 0) {
|
|
164
|
+
for (const v of session.variables) {
|
|
165
|
+
collectInputTypeDeclarations(schema, v.type, inputTypeDecls, 0, session, picklists);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Picklist type aliases (emitted after input collection so mutation input picklists are included)
|
|
170
|
+
for (const pl of picklists) {
|
|
171
|
+
parts.push(`/** ${pl.objectName}.${pl.fieldLabel} picklist values */`);
|
|
172
|
+
const union = pl.values.map((v) => `"${v.replace(/"/g, '\\"')}"`).join(" | ");
|
|
173
|
+
parts.push(`type ${pl.typeName} = ${union};`);
|
|
174
|
+
parts.push("");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Input type declarations
|
|
178
|
+
if (session.variables.length > 0) {
|
|
179
|
+
for (const [, code] of inputTypeDecls) {
|
|
180
|
+
if (code) {
|
|
181
|
+
parts.push(code);
|
|
182
|
+
parts.push("");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Variables interface
|
|
188
|
+
if (session.variables.length > 0) {
|
|
189
|
+
parts.push(`export interface ${pascalName}Variables {`);
|
|
190
|
+
for (const v of session.variables) {
|
|
191
|
+
const tsType = variableToTsType(v, session, schema, picklists, inputTypeDecls);
|
|
192
|
+
parts.push(` ${v.name}${v.type.endsWith("!") ? "" : "?"}: ${tsType};`);
|
|
193
|
+
}
|
|
194
|
+
parts.push("}");
|
|
195
|
+
parts.push("");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Result interface
|
|
199
|
+
parts.push(`export interface ${pascalName}Result {`);
|
|
200
|
+
parts.push(resultType);
|
|
201
|
+
parts.push("}");
|
|
202
|
+
|
|
203
|
+
return parts.join("\n") + "\n";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildNodeType(
|
|
207
|
+
session: QuerySession,
|
|
208
|
+
schema: GraphQLSchema,
|
|
209
|
+
parentId: string | null,
|
|
210
|
+
depth: number,
|
|
211
|
+
operation: string,
|
|
212
|
+
currentSchemaPath: string[],
|
|
213
|
+
picklists: PicklistTypeInfo[],
|
|
214
|
+
): string {
|
|
215
|
+
const _indent = " ".repeat(depth);
|
|
216
|
+
const children = getChildren(session, parentId);
|
|
217
|
+
const lines: string[] = [];
|
|
218
|
+
|
|
219
|
+
for (const child of children) {
|
|
220
|
+
if (child.kind === "field") {
|
|
221
|
+
const fieldLine = buildFieldType(
|
|
222
|
+
session,
|
|
223
|
+
schema,
|
|
224
|
+
child,
|
|
225
|
+
depth,
|
|
226
|
+
operation,
|
|
227
|
+
currentSchemaPath,
|
|
228
|
+
picklists,
|
|
229
|
+
);
|
|
230
|
+
lines.push(fieldLine);
|
|
231
|
+
} else if (child.kind === "fragment") {
|
|
232
|
+
const fragLine = buildFragmentType(
|
|
233
|
+
session,
|
|
234
|
+
schema,
|
|
235
|
+
child,
|
|
236
|
+
depth,
|
|
237
|
+
operation,
|
|
238
|
+
currentSchemaPath,
|
|
239
|
+
picklists,
|
|
240
|
+
);
|
|
241
|
+
lines.push(fragLine);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return lines.join("\n");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildFieldType(
|
|
249
|
+
session: QuerySession,
|
|
250
|
+
schema: GraphQLSchema,
|
|
251
|
+
node: FieldProjectionNode,
|
|
252
|
+
depth: number,
|
|
253
|
+
operation: string,
|
|
254
|
+
parentSchemaPath: string[],
|
|
255
|
+
picklists: PicklistTypeInfo[],
|
|
256
|
+
): string {
|
|
257
|
+
const indent = " ".repeat(depth);
|
|
258
|
+
const fieldSchemaPath = [...parentSchemaPath, node.fieldName];
|
|
259
|
+
const displayName = node.alias ?? node.fieldName;
|
|
260
|
+
const subChildren = getChildren(session, node.id);
|
|
261
|
+
const hasOptional = node.directives.some((d) => d.name === "optional");
|
|
262
|
+
const optionalMarker = hasOptional ? "?" : "";
|
|
263
|
+
const undefinedSuffix = hasOptional ? " | undefined" : "";
|
|
264
|
+
|
|
265
|
+
// Resolve the type of this field
|
|
266
|
+
let wr: WalkerResult | null = null;
|
|
267
|
+
try {
|
|
268
|
+
wr = resolvePath(schema, operation as OperationType, fieldSchemaPath);
|
|
269
|
+
} catch {
|
|
270
|
+
return `${indent}${displayName}${optionalMarker}: unknown${undefinedSuffix};`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Get the raw field type from the schema to determine nullability and list-ness
|
|
274
|
+
const rawType = getRawFieldType(
|
|
275
|
+
schema,
|
|
276
|
+
operation as OperationType,
|
|
277
|
+
parentSchemaPath,
|
|
278
|
+
node.fieldName,
|
|
279
|
+
);
|
|
280
|
+
const { isList: schemaIsList } = unwrapTypeInfo(rawType);
|
|
281
|
+
let { nullable: schemaNullable } = unwrapTypeInfo(rawType);
|
|
282
|
+
|
|
283
|
+
// UIAPI connection edges never contain null nodes — the API returns fewer edges
|
|
284
|
+
// rather than edges with null nodes. Override the schema nullability for `node`
|
|
285
|
+
// fields inside `edges` arrays to avoid unnecessary null checks in generated types.
|
|
286
|
+
if (
|
|
287
|
+
node.fieldName === "node" &&
|
|
288
|
+
parentSchemaPath.length > 0 &&
|
|
289
|
+
parentSchemaPath[parentSchemaPath.length - 1] === "edges"
|
|
290
|
+
) {
|
|
291
|
+
schemaNullable = false;
|
|
292
|
+
}
|
|
293
|
+
// UIAPI always returns an `edges` array (possibly empty), never null.
|
|
294
|
+
// Override the schema nullability for `edges` fields inside connection types.
|
|
295
|
+
if (node.fieldName === "edges" && schemaIsList) {
|
|
296
|
+
schemaNullable = false;
|
|
297
|
+
}
|
|
298
|
+
const fieldDesc = getFieldDescription(
|
|
299
|
+
schema,
|
|
300
|
+
operation as OperationType,
|
|
301
|
+
parentSchemaPath,
|
|
302
|
+
node.fieldName,
|
|
303
|
+
);
|
|
304
|
+
const jsdocPrefix = fieldDesc ? `${indent}/** ${fieldDesc} */\n` : "";
|
|
305
|
+
|
|
306
|
+
if (wr.isLeaf) {
|
|
307
|
+
let tsType: string;
|
|
308
|
+
if (isEnumType(wr.type)) {
|
|
309
|
+
tsType = wr.type
|
|
310
|
+
.getValues()
|
|
311
|
+
.map((v) => `"${v.name}"`)
|
|
312
|
+
.join(" | ");
|
|
313
|
+
} else {
|
|
314
|
+
tsType = scalarToTs(wr.typeName);
|
|
315
|
+
}
|
|
316
|
+
const nullSuffix = schemaNullable ? " | null" : "";
|
|
317
|
+
return `${jsdocPrefix}${indent}${displayName}${optionalMarker}: ${tsType}${nullSuffix}${undefinedSuffix};`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (subChildren.length === 0) {
|
|
321
|
+
return `${jsdocPrefix}${indent}${displayName}${optionalMarker}: Record<string, unknown>${undefinedSuffix};`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const nested = buildNodeType(
|
|
325
|
+
session,
|
|
326
|
+
schema,
|
|
327
|
+
node.id,
|
|
328
|
+
depth + 1,
|
|
329
|
+
operation,
|
|
330
|
+
fieldSchemaPath,
|
|
331
|
+
picklists,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const enriched = tryEnrichPicklist(
|
|
335
|
+
session,
|
|
336
|
+
node,
|
|
337
|
+
parentSchemaPath,
|
|
338
|
+
wr,
|
|
339
|
+
picklists,
|
|
340
|
+
schema,
|
|
341
|
+
operation,
|
|
342
|
+
);
|
|
343
|
+
if (enriched) {
|
|
344
|
+
let result = enriched.replaceAll("__INDENT__", indent).replaceAll("__NAME__", displayName);
|
|
345
|
+
if (hasOptional) {
|
|
346
|
+
result = result.replace(`${displayName}:`, `${displayName}?:`);
|
|
347
|
+
result = result.replace(/;$/, `${undefinedSuffix};`);
|
|
348
|
+
}
|
|
349
|
+
return `${jsdocPrefix}${result}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const nullSuffix = schemaNullable ? " | null" : "";
|
|
353
|
+
|
|
354
|
+
if (schemaIsList) {
|
|
355
|
+
return `${jsdocPrefix}${indent}${displayName}${optionalMarker}: Array<{\n${nested}\n${indent}}>${nullSuffix}${undefinedSuffix};`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Check if children contain inline fragments — produce discriminated union
|
|
359
|
+
const fragmentChildren = subChildren.filter(
|
|
360
|
+
(c) => c.kind === "fragment",
|
|
361
|
+
) as FragmentProjectionNode[];
|
|
362
|
+
if (fragmentChildren.length > 0) {
|
|
363
|
+
const branches: string[] = [];
|
|
364
|
+
for (const frag of fragmentChildren) {
|
|
365
|
+
const fragLine = buildFragmentType(
|
|
366
|
+
session,
|
|
367
|
+
schema,
|
|
368
|
+
frag,
|
|
369
|
+
depth,
|
|
370
|
+
operation,
|
|
371
|
+
fieldSchemaPath,
|
|
372
|
+
picklists,
|
|
373
|
+
);
|
|
374
|
+
if (fragLine) branches.push(fragLine);
|
|
375
|
+
}
|
|
376
|
+
const fieldChildren = subChildren.filter((c) => c.kind === "field");
|
|
377
|
+
const fieldLines: string[] = [];
|
|
378
|
+
for (const fc of fieldChildren) {
|
|
379
|
+
fieldLines.push(
|
|
380
|
+
buildFieldType(
|
|
381
|
+
session,
|
|
382
|
+
schema,
|
|
383
|
+
fc as FieldProjectionNode,
|
|
384
|
+
depth + 1,
|
|
385
|
+
operation,
|
|
386
|
+
fieldSchemaPath,
|
|
387
|
+
picklists,
|
|
388
|
+
),
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
const _fieldBlock = fieldLines.length > 0 ? fieldLines.join("\n") + "\n" : "";
|
|
392
|
+
|
|
393
|
+
if (branches.length === 1) {
|
|
394
|
+
return `${jsdocPrefix}${indent}${displayName}${optionalMarker}: ${branches[0]}${nullSuffix}${undefinedSuffix};`;
|
|
395
|
+
}
|
|
396
|
+
const unionType = branches.join(" | ");
|
|
397
|
+
return `${jsdocPrefix}${indent}${displayName}${optionalMarker}: ${unionType}${nullSuffix}${undefinedSuffix};`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return `${jsdocPrefix}${indent}${displayName}${optionalMarker}: {\n${nested}\n${indent}}${nullSuffix}${undefinedSuffix};`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function buildFragmentType(
|
|
404
|
+
session: QuerySession,
|
|
405
|
+
schema: GraphQLSchema,
|
|
406
|
+
node: FragmentProjectionNode,
|
|
407
|
+
depth: number,
|
|
408
|
+
operation: string,
|
|
409
|
+
parentSchemaPath: string[],
|
|
410
|
+
picklists: PicklistTypeInfo[],
|
|
411
|
+
): string {
|
|
412
|
+
const indent = " ".repeat(depth);
|
|
413
|
+
const subChildren = getChildren(session, node.id);
|
|
414
|
+
|
|
415
|
+
if (subChildren.length === 0) {
|
|
416
|
+
return "";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const fragmentSchemaPath = [...parentSchemaPath, `[${node.onType}]`];
|
|
420
|
+
const nested = buildNodeType(
|
|
421
|
+
session,
|
|
422
|
+
schema,
|
|
423
|
+
node.id,
|
|
424
|
+
depth + 1,
|
|
425
|
+
operation,
|
|
426
|
+
fragmentSchemaPath,
|
|
427
|
+
picklists,
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
// Return as an object block with __typename for discriminated unions (no leading indent — parent handles it)
|
|
431
|
+
return `{\n${indent} __typename: "${node.onType}";\n${nested}\n${indent}}`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function tryEnrichPicklist(
|
|
435
|
+
session: QuerySession,
|
|
436
|
+
node: FieldProjectionNode,
|
|
437
|
+
parentSchemaPath: string[],
|
|
438
|
+
wr: WalkerResult,
|
|
439
|
+
picklists: PicklistTypeInfo[],
|
|
440
|
+
schema: GraphQLSchema,
|
|
441
|
+
operation: string,
|
|
442
|
+
): string | null {
|
|
443
|
+
// Check if this is a PicklistValue or PicklistAggregate type
|
|
444
|
+
if (!wr.typeName.includes("Picklist")) return null;
|
|
445
|
+
|
|
446
|
+
// Try to find the SObject and field name for this picklist
|
|
447
|
+
let sObjectName = detectSObjectFromPath(parentSchemaPath);
|
|
448
|
+
|
|
449
|
+
let objInfo = sObjectName ? getCachedObjectInfo(session.orgAlias, sObjectName) : null;
|
|
450
|
+
let plInfo = objInfo?.picklists.find((p) => p.apiName === node.fieldName);
|
|
451
|
+
|
|
452
|
+
// Fallback: resolve the parent schema path to get the actual type name.
|
|
453
|
+
// This handles parent relationships (e.g. Case→Account→Industry) and child
|
|
454
|
+
// relationships (e.g. Order→OrderItems→node→Status) where the root SObject
|
|
455
|
+
// detection returns the wrong SObject.
|
|
456
|
+
if ((!plInfo || plInfo.values.length === 0) && parentSchemaPath.length > 0) {
|
|
457
|
+
try {
|
|
458
|
+
const parentWr = resolvePath(schema, operation as OperationType, parentSchemaPath);
|
|
459
|
+
const parentType = parentWr.typeName;
|
|
460
|
+
if (parentType && parentType !== sObjectName) {
|
|
461
|
+
const altInfo = getCachedObjectInfo(session.orgAlias, parentType);
|
|
462
|
+
if (altInfo) {
|
|
463
|
+
const altPl = altInfo.picklists.find((p) => p.apiName === node.fieldName);
|
|
464
|
+
if (altPl && altPl.values.length > 0) {
|
|
465
|
+
sObjectName = parentType;
|
|
466
|
+
objInfo = altInfo;
|
|
467
|
+
plInfo = altPl;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
} catch {
|
|
472
|
+
/* ignore resolution failures */
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!sObjectName || !objInfo) return null;
|
|
477
|
+
if (!plInfo || plInfo.values.length === 0) return null;
|
|
478
|
+
|
|
479
|
+
const values = plInfo.values.map((v) => v.value).filter((v): v is string => v !== null);
|
|
480
|
+
if (values.length === 0) return null;
|
|
481
|
+
|
|
482
|
+
// Suppress single-"None" picklist unions — they're placeholder values, not meaningful constraints
|
|
483
|
+
if (values.length === 1 && values[0] === "None") return null;
|
|
484
|
+
|
|
485
|
+
const picklistTypeName = `${sObjectName}${toPascalCase(node.fieldName)}`;
|
|
486
|
+
|
|
487
|
+
if (!picklists.some((p) => p.typeName === picklistTypeName)) {
|
|
488
|
+
picklists.push({
|
|
489
|
+
typeName: picklistTypeName,
|
|
490
|
+
values,
|
|
491
|
+
fieldLabel: node.fieldName,
|
|
492
|
+
objectName: sObjectName,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Build the sub-fields with the enriched picklist type
|
|
497
|
+
const subChildren = getChildren(session, node.id);
|
|
498
|
+
if (subChildren.length === 0) return null;
|
|
499
|
+
|
|
500
|
+
// `PicklistValue` and `PicklistAggregate` both pass the typeName check
|
|
501
|
+
// above, but their children have different shapes. Branch on the exact
|
|
502
|
+
// type so each child renders correctly.
|
|
503
|
+
//
|
|
504
|
+
// PicklistAggregate children (min/max/count/countDistinct/value) wrap
|
|
505
|
+
// the picklist value in their own object: e.g. `min: PicklistValue`,
|
|
506
|
+
// not `min: Picklist`. Without this branch the loop would hit the
|
|
507
|
+
// `unknown` fallback for every child and silently violate FR-10.2 +
|
|
508
|
+
// FR-10.5 (e2e Gap 4: aggregate min/max emits `string | null` instead
|
|
509
|
+
// of the picklist literal union).
|
|
510
|
+
const isAggregate = wr.typeName === "PicklistAggregate";
|
|
511
|
+
const lines: string[] = [];
|
|
512
|
+
for (const child of subChildren) {
|
|
513
|
+
if (child.kind !== "field") continue;
|
|
514
|
+
const fc = child as FieldProjectionNode;
|
|
515
|
+
const name = fc.alias ?? fc.fieldName;
|
|
516
|
+
if (isAggregate) {
|
|
517
|
+
if (fc.fieldName === "min" || fc.fieldName === "max" || fc.fieldName === "value") {
|
|
518
|
+
// `value` here is the mode-style aggregation (rare). All three
|
|
519
|
+
// resolve to a PicklistValue wrapper around the picklist union.
|
|
520
|
+
lines.push(`__INDENT__ ${name}: { value: ${picklistTypeName} | null } | null;`);
|
|
521
|
+
} else if (fc.fieldName === "count" || fc.fieldName === "countDistinct") {
|
|
522
|
+
lines.push(`__INDENT__ ${name}: { value: number | null } | null;`);
|
|
523
|
+
} else {
|
|
524
|
+
lines.push(`__INDENT__ ${name}: unknown;`);
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
if (fc.fieldName === "value") {
|
|
528
|
+
lines.push(`__INDENT__ ${name}: ${picklistTypeName} | null;`);
|
|
529
|
+
} else if (fc.fieldName === "displayValue" || fc.fieldName === "label") {
|
|
530
|
+
lines.push(`__INDENT__ ${name}: string | null;`);
|
|
531
|
+
} else {
|
|
532
|
+
lines.push(`__INDENT__ ${name}: unknown;`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return `__INDENT____NAME__: {\n${lines.join("\n")}\n__INDENT__} | null;`;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function detectSObjectFromPath(schemaPath: string[]): string | null {
|
|
541
|
+
const queryIdx = schemaPath.indexOf("query");
|
|
542
|
+
if (queryIdx !== -1 && schemaPath[queryIdx + 1]) {
|
|
543
|
+
return schemaPath[queryIdx + 1];
|
|
544
|
+
}
|
|
545
|
+
const aggIdx = schemaPath.indexOf("aggregate");
|
|
546
|
+
if (aggIdx !== -1 && schemaPath[aggIdx + 1]) {
|
|
547
|
+
return schemaPath[aggIdx + 1];
|
|
548
|
+
}
|
|
549
|
+
// Mutation: look for *Create/*Update patterns
|
|
550
|
+
for (const seg of schemaPath) {
|
|
551
|
+
const match = seg.match(/^(\w+?)(?:Create|Update|Delete)$/);
|
|
552
|
+
if (match) return match[1];
|
|
553
|
+
}
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Unwraps a GraphQLOutputType to determine nullability and list-ness.
|
|
559
|
+
* Falls back to nullable:true when the raw type is unavailable (e.g. root
|
|
560
|
+
* query fields or unresolvable paths) as a safe default.
|
|
561
|
+
*/
|
|
562
|
+
function unwrapTypeInfo(rawType: GraphQLOutputType | null): { nullable: boolean; isList: boolean } {
|
|
563
|
+
if (!rawType) return { nullable: true, isList: false };
|
|
564
|
+
const nonNull = isNonNullType(rawType);
|
|
565
|
+
const inner = nonNull ? rawType.ofType : rawType;
|
|
566
|
+
return {
|
|
567
|
+
nullable: !nonNull,
|
|
568
|
+
isList: isListType(inner) || (isNonNullType(inner) && isListType(inner.ofType)),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function scalarToTs(typeName: string): string {
|
|
573
|
+
return OUTPUT_SCALAR_MAP[typeName] ?? "unknown";
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Large INPUT_OBJECT types (entity filters with many fields) are only expanded
|
|
578
|
+
* at depth 0 (the variable's own type). Small types (operators, value inputs)
|
|
579
|
+
* are always expanded regardless of depth. This prevents a combinatorial explosion
|
|
580
|
+
* when filter types cross-reference other entity filters (e.g. Case_Filter →
|
|
581
|
+
* Account_Filter → Contact_Filter → ...).
|
|
582
|
+
*/
|
|
583
|
+
const LARGE_TYPE_FIELD_THRESHOLD = 25;
|
|
584
|
+
const LARGE_TYPE_MAX_DEPTH = 1;
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Recursively collects TypeScript type declarations for all INPUT_OBJECT and ENUM
|
|
588
|
+
* types transitively referenced by the given GraphQL type string.
|
|
589
|
+
*/
|
|
590
|
+
function collectInputTypeDeclarations(
|
|
591
|
+
schema: GraphQLSchema,
|
|
592
|
+
graphqlTypeString: string,
|
|
593
|
+
declarations: Map<string, string>,
|
|
594
|
+
depth = 0,
|
|
595
|
+
session?: QuerySession,
|
|
596
|
+
picklists?: PicklistTypeInfo[],
|
|
597
|
+
): void {
|
|
598
|
+
const baseTypeName = graphqlTypeString.replace(/[![\]]/g, "");
|
|
599
|
+
|
|
600
|
+
if (declarations.has(baseTypeName)) return;
|
|
601
|
+
if (INPUT_SCALAR_MAP[baseTypeName]) return;
|
|
602
|
+
|
|
603
|
+
const schemaType = schema.getType(baseTypeName);
|
|
604
|
+
if (!schemaType) return;
|
|
605
|
+
if (isScalarType(schemaType)) return;
|
|
606
|
+
|
|
607
|
+
if (isEnumType(schemaType)) {
|
|
608
|
+
const values = schemaType
|
|
609
|
+
.getValues()
|
|
610
|
+
.map((v) => `"${v.name}"`)
|
|
611
|
+
.join(" | ");
|
|
612
|
+
declarations.set(baseTypeName, `type ${baseTypeName} = ${values};`);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (isInputObjectType(schemaType)) {
|
|
617
|
+
const fields = Object.values(schemaType.getFields());
|
|
618
|
+
|
|
619
|
+
// Only depth-limit types that are BOTH large themselves AND reference other
|
|
620
|
+
// large INPUT_OBJECT types. This targets the entity-filter cross-reference
|
|
621
|
+
// pattern (Case_Filter → Account_Filter → Contact_Filter → ...) while
|
|
622
|
+
// always expanding small operator types (IdOperators, DateTimeOperators)
|
|
623
|
+
// even when they reference large utility types like JoinInput.
|
|
624
|
+
const isLargeType = fields.length > LARGE_TYPE_FIELD_THRESHOLD;
|
|
625
|
+
if (isLargeType && depth >= LARGE_TYPE_MAX_DEPTH) {
|
|
626
|
+
const referencesOtherLargeInputTypes = fields.some((f) => {
|
|
627
|
+
const ft = getNamedType(f.type);
|
|
628
|
+
if (!ft || ft.name === baseTypeName) return false;
|
|
629
|
+
if (!isInputObjectType(ft)) return false;
|
|
630
|
+
return Object.keys(ft.getFields()).length > LARGE_TYPE_FIELD_THRESHOLD;
|
|
631
|
+
});
|
|
632
|
+
if (referencesOtherLargeInputTypes) {
|
|
633
|
+
// Emit a named type alias so references use the type name instead of
|
|
634
|
+
// Record<string, unknown>. The body is opaque but the name is preserved.
|
|
635
|
+
declarations.set(baseTypeName, `type ${baseTypeName} = Record<string, unknown>;`);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Register placeholder to break circular references (e.g. Case_Filter.and: [Case_Filter])
|
|
641
|
+
declarations.set(baseTypeName, "");
|
|
642
|
+
|
|
643
|
+
// Detect SObject name from representation types (e.g. CaseCreateRepresentation → Case)
|
|
644
|
+
const reprMatch = baseTypeName.match(/^(\w+?)(?:Create|Update)Representation$/);
|
|
645
|
+
const reprSObject = reprMatch ? reprMatch[1] : null;
|
|
646
|
+
const objInfo =
|
|
647
|
+
reprSObject && session ? getCachedObjectInfo(session.orgAlias, reprSObject) : null;
|
|
648
|
+
|
|
649
|
+
// Detect SObject name from filter types (e.g. Case_Filter → Case)
|
|
650
|
+
const filterMatch = baseTypeName.match(/^(\w+?)_Filter$/);
|
|
651
|
+
const filterSObject = filterMatch ? filterMatch[1] : null;
|
|
652
|
+
const filterObjInfo =
|
|
653
|
+
filterSObject && session ? getCachedObjectInfo(session.orgAlias, filterSObject) : null;
|
|
654
|
+
|
|
655
|
+
const lines: string[] = [];
|
|
656
|
+
for (const field of fields) {
|
|
657
|
+
const namedFieldType = getNamedType(field.type);
|
|
658
|
+
if (namedFieldType) {
|
|
659
|
+
collectInputTypeDeclarations(
|
|
660
|
+
schema,
|
|
661
|
+
namedFieldType.name,
|
|
662
|
+
declarations,
|
|
663
|
+
depth + 1,
|
|
664
|
+
session,
|
|
665
|
+
picklists,
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Enrich picklist fields in filter types with field-specific operator types
|
|
670
|
+
let tsType = inputTypeToTs(field.type, declarations);
|
|
671
|
+
if (filterObjInfo && picklists && tsType === "PicklistOperators") {
|
|
672
|
+
const plInfo = filterObjInfo.picklists.find((p) => p.apiName === field.name);
|
|
673
|
+
if (plInfo && plInfo.values.length > 0) {
|
|
674
|
+
const values = plInfo.values.map((v) => v.value).filter((v): v is string => v !== null);
|
|
675
|
+
if (values.length > 0 && !(values.length === 1 && values[0] === "None")) {
|
|
676
|
+
const picklistTypeName = `${filterSObject}${toPascalCase(field.name)}`;
|
|
677
|
+
if (!picklists.some((p) => p.typeName === picklistTypeName)) {
|
|
678
|
+
picklists.push({
|
|
679
|
+
typeName: picklistTypeName,
|
|
680
|
+
values,
|
|
681
|
+
fieldLabel: field.name,
|
|
682
|
+
objectName: filterSObject!,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
// Generate a field-specific operator type with the picklist union
|
|
686
|
+
const opsTypeName = `${picklistTypeName}Operators`;
|
|
687
|
+
if (!declarations.has(opsTypeName)) {
|
|
688
|
+
const opsLines = [
|
|
689
|
+
` eq?: ${picklistTypeName};`,
|
|
690
|
+
` ne?: ${picklistTypeName};`,
|
|
691
|
+
` in?: ${picklistTypeName}[];`,
|
|
692
|
+
` nin?: ${picklistTypeName}[];`,
|
|
693
|
+
` like?: string;`,
|
|
694
|
+
` lt?: string;`,
|
|
695
|
+
` gt?: string;`,
|
|
696
|
+
` lte?: string;`,
|
|
697
|
+
` gte?: string;`,
|
|
698
|
+
];
|
|
699
|
+
declarations.set(
|
|
700
|
+
opsTypeName,
|
|
701
|
+
`interface ${opsTypeName} {\n${opsLines.join("\n")}\n}`,
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
tsType = opsTypeName;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Enrich picklist fields in mutation representations with union types
|
|
710
|
+
if (objInfo && picklists && tsType === "string") {
|
|
711
|
+
const plInfo = objInfo.picklists.find((p) => p.apiName === field.name);
|
|
712
|
+
if (plInfo && plInfo.values.length > 0) {
|
|
713
|
+
const values = plInfo.values.map((v) => v.value).filter((v): v is string => v !== null);
|
|
714
|
+
if (values.length > 0 && !(values.length === 1 && values[0] === "None")) {
|
|
715
|
+
const picklistTypeName = `${reprSObject}${toPascalCase(field.name)}`;
|
|
716
|
+
if (picklists && !picklists.some((p) => p.typeName === picklistTypeName)) {
|
|
717
|
+
picklists.push({
|
|
718
|
+
typeName: picklistTypeName,
|
|
719
|
+
values,
|
|
720
|
+
fieldLabel: field.name,
|
|
721
|
+
objectName: reprSObject!,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
tsType = picklistTypeName;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
let optional = !isNonNullType(field.type);
|
|
730
|
+
// For Create representations, check ObjectInfo for required-on-create fields
|
|
731
|
+
if (optional && objInfo && baseTypeName.endsWith("CreateRepresentation")) {
|
|
732
|
+
const requiredFields = getRequiredCreateFields(objInfo);
|
|
733
|
+
if (requiredFields.some((f) => f.apiName === field.name)) {
|
|
734
|
+
optional = false;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
lines.push(` ${field.name}${optional ? "?" : ""}: ${tsType};`);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
declarations.set(baseTypeName, `interface ${baseTypeName} {\n${lines.join("\n")}\n}`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Maps a GraphQL input type (possibly wrapped in NonNull/List) to its TypeScript
|
|
746
|
+
* representation string. NonNull wrappers are stripped because optionality is
|
|
747
|
+
* handled at the field declaration site. Types not present in the declarations
|
|
748
|
+
* map (skipped due to depth limits) fall back to Record<string, unknown>.
|
|
749
|
+
*/
|
|
750
|
+
function inputTypeToTs(type: GraphQLType, declarations: Map<string, string>): string {
|
|
751
|
+
let t = type;
|
|
752
|
+
if (isNonNullType(t)) t = t.ofType;
|
|
753
|
+
|
|
754
|
+
if (isListType(t)) {
|
|
755
|
+
return `${inputTypeToTs(t.ofType, declarations)}[]`;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const named = getNamedType(t);
|
|
759
|
+
if (!named) return "unknown";
|
|
760
|
+
if (INPUT_SCALAR_MAP[named.name]) return INPUT_SCALAR_MAP[named.name];
|
|
761
|
+
if (declarations.has(named.name)) return named.name;
|
|
762
|
+
if (isInputObjectType(named)) return "Record<string, unknown>";
|
|
763
|
+
if (isEnumType(named)) return "string";
|
|
764
|
+
|
|
765
|
+
return "unknown";
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function variableToTsType(
|
|
769
|
+
v: VariableDefinition,
|
|
770
|
+
session: QuerySession,
|
|
771
|
+
schema: GraphQLSchema,
|
|
772
|
+
picklists: PicklistTypeInfo[],
|
|
773
|
+
inputDecls: Map<string, string>,
|
|
774
|
+
): string {
|
|
775
|
+
const baseType = v.type.replace(/[![\]]/g, "");
|
|
776
|
+
const isList = v.type.includes("[");
|
|
777
|
+
const isNN = v.type.endsWith("!");
|
|
778
|
+
|
|
779
|
+
let tsType: string;
|
|
780
|
+
if (INPUT_SCALAR_MAP[baseType]) {
|
|
781
|
+
tsType = INPUT_SCALAR_MAP[baseType];
|
|
782
|
+
} else if (inputDecls.has(baseType)) {
|
|
783
|
+
tsType = baseType;
|
|
784
|
+
} else {
|
|
785
|
+
const schemaType = schema.getType(baseType);
|
|
786
|
+
if (schemaType && isInputObjectType(schemaType)) {
|
|
787
|
+
tsType = "Record<string, unknown>";
|
|
788
|
+
} else {
|
|
789
|
+
tsType = "unknown";
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Enrich Picklist variables with union types by tracing the variable's binding
|
|
794
|
+
if (baseType === "Picklist" || baseType === "MultiPicklist") {
|
|
795
|
+
const enriched = enrichPicklistVariable(session, v.name, picklists);
|
|
796
|
+
if (enriched) tsType = enriched;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (isList) tsType = `${tsType}[]`;
|
|
800
|
+
if (!isNN) tsType += " | null";
|
|
801
|
+
|
|
802
|
+
return tsType;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Traces a Picklist variable's binding through the session's node args to find
|
|
807
|
+
* the SObject and field name, then returns the picklist union type alias.
|
|
808
|
+
* For example, $status bound to Case/@args/where/Status/eq → CaseStatus.
|
|
809
|
+
*
|
|
810
|
+
* Args are stored as flat key-value pairs on nodes. The `where` arg is a JSON
|
|
811
|
+
* string like `{"Status":{"eq":"$status"}}`. We search for `$varName` in the
|
|
812
|
+
* JSON and extract the field name from the enclosing key path.
|
|
813
|
+
*/
|
|
814
|
+
function enrichPicklistVariable(
|
|
815
|
+
session: QuerySession,
|
|
816
|
+
varName: string,
|
|
817
|
+
picklists: PicklistTypeInfo[],
|
|
818
|
+
): string | null {
|
|
819
|
+
const varRef = `$${varName}`;
|
|
820
|
+
for (const node of session.nodes) {
|
|
821
|
+
if (node.kind !== "field") continue;
|
|
822
|
+
const fieldNode = node as FieldProjectionNode;
|
|
823
|
+
for (const [argKey, argVal] of Object.entries(fieldNode.args)) {
|
|
824
|
+
if (!argVal.includes(varRef)) continue;
|
|
825
|
+
|
|
826
|
+
// For direct arg bindings like "after": "$after"
|
|
827
|
+
if (argVal === varRef && argKey !== "where") continue;
|
|
828
|
+
|
|
829
|
+
// For JSON where clauses, parse and find the field name containing $varRef
|
|
830
|
+
let fieldName: string | null = null;
|
|
831
|
+
if (argKey === "where") {
|
|
832
|
+
try {
|
|
833
|
+
fieldName = findPicklistFieldInWhere(JSON.parse(argVal), varRef);
|
|
834
|
+
} catch {
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (!fieldName) continue;
|
|
839
|
+
|
|
840
|
+
const sObjectName = detectSObjectFromPath(fieldNode.schemaPath);
|
|
841
|
+
if (!sObjectName) continue;
|
|
842
|
+
|
|
843
|
+
const objInfo = getCachedObjectInfo(session.orgAlias, sObjectName);
|
|
844
|
+
if (!objInfo) continue;
|
|
845
|
+
|
|
846
|
+
const plInfo = objInfo.picklists.find((p) => p.apiName === fieldName);
|
|
847
|
+
if (!plInfo || plInfo.values.length === 0) continue;
|
|
848
|
+
|
|
849
|
+
const values = plInfo.values.map((v) => v.value).filter((v): v is string => v !== null);
|
|
850
|
+
if (values.length === 0) continue;
|
|
851
|
+
if (values.length === 1 && values[0] === "None") continue;
|
|
852
|
+
|
|
853
|
+
const picklistTypeName = `${sObjectName}${toPascalCase(fieldName)}`;
|
|
854
|
+
if (!picklists.some((p) => p.typeName === picklistTypeName)) {
|
|
855
|
+
picklists.push({
|
|
856
|
+
typeName: picklistTypeName,
|
|
857
|
+
values,
|
|
858
|
+
fieldLabel: fieldName,
|
|
859
|
+
objectName: sObjectName,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
return picklistTypeName;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Recursively searches a parsed where clause object for a variable reference
|
|
870
|
+
* and returns the field name it's bound to.
|
|
871
|
+
* E.g. {"Status":{"eq":"$status"}} → "Status"
|
|
872
|
+
*/
|
|
873
|
+
function findPicklistFieldInWhere(obj: unknown, varRef: string): string | null {
|
|
874
|
+
if (typeof obj !== "object" || obj === null) return null;
|
|
875
|
+
for (const [key, val] of Object.entries(obj as Record<string, unknown>)) {
|
|
876
|
+
if (typeof val === "string" && val === varRef) {
|
|
877
|
+
// We're at the operator level (eq/ne/in). Return null — parent will return the field name.
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
if (typeof val === "object" && val !== null) {
|
|
881
|
+
// Check if this value directly contains the varRef at operator level
|
|
882
|
+
for (const [, opVal] of Object.entries(val as Record<string, unknown>)) {
|
|
883
|
+
if (opVal === varRef) return key;
|
|
884
|
+
}
|
|
885
|
+
// Recurse for nested and/or
|
|
886
|
+
const found = findPicklistFieldInWhere(val, varRef);
|
|
887
|
+
if (found) return found;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function toPascalCase(str: string): string {
|
|
894
|
+
return str
|
|
895
|
+
.replace(/[^a-zA-Z0-9]/g, " ")
|
|
896
|
+
.split(/\s+/)
|
|
897
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
898
|
+
.join("");
|
|
899
|
+
}
|