@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,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- graphiti traverses untyped schema/introspection JSON; see follow-up to replace with `unknown` + narrowing */
|
|
7
|
+
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import type { OrgAuth } from "./auth.js";
|
|
11
|
+
import { graphitiHome } from "./fs-utils.js";
|
|
12
|
+
import { executeGraphQL } from "./introspect.js";
|
|
13
|
+
|
|
14
|
+
// Re-read GRAPHITI_HOME on each call so tests redirecting via env vars
|
|
15
|
+
// see the current value, not a frozen import-time snapshot.
|
|
16
|
+
function cacheDir(): string {
|
|
17
|
+
return path.join(graphitiHome(), "cache", "objectInfos");
|
|
18
|
+
}
|
|
19
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
20
|
+
|
|
21
|
+
const MASTER_RECORD_TYPE_ID = "012000000000000AAA";
|
|
22
|
+
|
|
23
|
+
export interface PicklistValue {
|
|
24
|
+
value: string | null;
|
|
25
|
+
label: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PicklistFieldInfo {
|
|
29
|
+
apiName: string;
|
|
30
|
+
label: string;
|
|
31
|
+
required: boolean;
|
|
32
|
+
values: PicklistValue[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface FieldMetadata {
|
|
36
|
+
apiName: string;
|
|
37
|
+
label: string | null;
|
|
38
|
+
dataType: string | null;
|
|
39
|
+
required: boolean;
|
|
40
|
+
createable: boolean;
|
|
41
|
+
updateable: boolean;
|
|
42
|
+
calculated: boolean;
|
|
43
|
+
custom: boolean;
|
|
44
|
+
filterable: boolean;
|
|
45
|
+
sortable: boolean;
|
|
46
|
+
nameField: boolean;
|
|
47
|
+
reference: boolean;
|
|
48
|
+
relationshipName: string | null;
|
|
49
|
+
compound: boolean;
|
|
50
|
+
compoundFieldName: string | null;
|
|
51
|
+
defaultedOnCreate: boolean;
|
|
52
|
+
extraTypeInfo: string | null;
|
|
53
|
+
inlineHelpText: string | null;
|
|
54
|
+
precision: number;
|
|
55
|
+
scale: number;
|
|
56
|
+
referenceToInfos: { apiName: string; nameFields: string[] }[];
|
|
57
|
+
controllerName: string | null;
|
|
58
|
+
controllingFields: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ObjectInfoResult {
|
|
62
|
+
apiName: string;
|
|
63
|
+
label: string | null;
|
|
64
|
+
labelPlural: string | null;
|
|
65
|
+
createable: boolean;
|
|
66
|
+
deletable: boolean;
|
|
67
|
+
updateable: boolean;
|
|
68
|
+
queryable: boolean;
|
|
69
|
+
searchable: boolean;
|
|
70
|
+
custom: boolean;
|
|
71
|
+
keyPrefix: string | null;
|
|
72
|
+
nameFields: string[];
|
|
73
|
+
defaultRecordTypeId: string | null;
|
|
74
|
+
fields: FieldMetadata[];
|
|
75
|
+
childRelationships: {
|
|
76
|
+
childObjectApiName: string;
|
|
77
|
+
fieldName: string | null;
|
|
78
|
+
relationshipName: string | null;
|
|
79
|
+
}[];
|
|
80
|
+
recordTypeInfos: {
|
|
81
|
+
recordTypeId: string;
|
|
82
|
+
name: string | null;
|
|
83
|
+
available: boolean;
|
|
84
|
+
master: boolean;
|
|
85
|
+
defaultRecordTypeMapping: boolean;
|
|
86
|
+
}[];
|
|
87
|
+
picklists: PicklistFieldInfo[];
|
|
88
|
+
fetchedAt: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const OBJECT_INFO_QUERY = `
|
|
92
|
+
query ObjectInfoQuery($inputs: [ObjectInfoInput!]) {
|
|
93
|
+
uiapi {
|
|
94
|
+
objectInfos(objectInfoInputs: $inputs) {
|
|
95
|
+
ApiName
|
|
96
|
+
label
|
|
97
|
+
labelPlural
|
|
98
|
+
createable
|
|
99
|
+
deletable
|
|
100
|
+
updateable
|
|
101
|
+
queryable
|
|
102
|
+
searchable
|
|
103
|
+
custom
|
|
104
|
+
keyPrefix
|
|
105
|
+
nameFields
|
|
106
|
+
defaultRecordTypeId
|
|
107
|
+
recordTypeInfos {
|
|
108
|
+
recordTypeId
|
|
109
|
+
name
|
|
110
|
+
available
|
|
111
|
+
master
|
|
112
|
+
defaultRecordTypeMapping
|
|
113
|
+
}
|
|
114
|
+
childRelationships {
|
|
115
|
+
childObjectApiName
|
|
116
|
+
fieldName
|
|
117
|
+
relationshipName
|
|
118
|
+
}
|
|
119
|
+
fields {
|
|
120
|
+
ApiName
|
|
121
|
+
label
|
|
122
|
+
dataType
|
|
123
|
+
required
|
|
124
|
+
createable
|
|
125
|
+
updateable
|
|
126
|
+
calculated
|
|
127
|
+
custom
|
|
128
|
+
filterable
|
|
129
|
+
sortable
|
|
130
|
+
nameField
|
|
131
|
+
reference
|
|
132
|
+
relationshipName
|
|
133
|
+
compound
|
|
134
|
+
compoundFieldName
|
|
135
|
+
defaultedOnCreate
|
|
136
|
+
extraTypeInfo
|
|
137
|
+
inlineHelpText
|
|
138
|
+
precision
|
|
139
|
+
scale
|
|
140
|
+
controllerName
|
|
141
|
+
controllingFields
|
|
142
|
+
referenceToInfos {
|
|
143
|
+
ApiName
|
|
144
|
+
nameFields
|
|
145
|
+
}
|
|
146
|
+
... on PicklistField {
|
|
147
|
+
picklistValuesByRecordTypeIDs {
|
|
148
|
+
recordTypeID
|
|
149
|
+
picklistValues {
|
|
150
|
+
value
|
|
151
|
+
label
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}`;
|
|
159
|
+
|
|
160
|
+
// Process-lifetime in-memory cache. Unbounded by design: graphiti runs as
|
|
161
|
+
// short-lived CLI invocations and per-MCP-session servers, so the working
|
|
162
|
+
// set is naturally capped by the SObjects an agent walks during one session.
|
|
163
|
+
const memoryCache = new Map<string, ObjectInfoResult>();
|
|
164
|
+
|
|
165
|
+
function cacheKey(orgAlias: string, sObjectName: string): string {
|
|
166
|
+
return `${orgAlias}:${sObjectName}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Defense in depth: callers (the MCP boundary) already validate API-name
|
|
170
|
+
// charsets, but cache paths are also reachable from CLI/library callers, so
|
|
171
|
+
// re-validate here before joining and assert the resolved path stays inside
|
|
172
|
+
// the cache root.
|
|
173
|
+
const ORG_ALIAS_PATH_RE = /^[A-Za-z0-9_-]{1,80}$/;
|
|
174
|
+
const SOBJECT_NAME_PATH_RE = /^[A-Za-z][A-Za-z0-9_]{0,79}$/;
|
|
175
|
+
|
|
176
|
+
function cacheFilePath(orgAlias: string, sObjectName: string): string {
|
|
177
|
+
if (!ORG_ALIAS_PATH_RE.test(orgAlias)) {
|
|
178
|
+
throw new Error(`Invalid org alias for cache path: "${orgAlias}"`);
|
|
179
|
+
}
|
|
180
|
+
if (!SOBJECT_NAME_PATH_RE.test(sObjectName)) {
|
|
181
|
+
throw new Error(`Invalid SObject name for cache path: "${sObjectName}"`);
|
|
182
|
+
}
|
|
183
|
+
const rootResolved = path.resolve(cacheDir());
|
|
184
|
+
const resolved = path.resolve(rootResolved, orgAlias, `${sObjectName}.json`);
|
|
185
|
+
if (!resolved.startsWith(rootResolved + path.sep)) {
|
|
186
|
+
throw new Error("Refusing to access cache path outside cache root");
|
|
187
|
+
}
|
|
188
|
+
return resolved;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function readDiskCache(orgAlias: string, sObjectName: string): ObjectInfoResult | null {
|
|
192
|
+
let fp: string;
|
|
193
|
+
try {
|
|
194
|
+
fp = cacheFilePath(orgAlias, sObjectName);
|
|
195
|
+
} catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
if (!fs.existsSync(fp)) return null;
|
|
199
|
+
try {
|
|
200
|
+
const raw = JSON.parse(fs.readFileSync(fp, "utf-8")) as ObjectInfoResult;
|
|
201
|
+
const age = Date.now() - new Date(raw.fetchedAt).getTime();
|
|
202
|
+
// Reject NaN (corrupt or missing fetchedAt) so a hand-edited cache
|
|
203
|
+
// can't read as fresh forever.
|
|
204
|
+
if (Number.isNaN(age) || age > CACHE_TTL_MS) return null;
|
|
205
|
+
return raw;
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function writeDiskCache(orgAlias: string, sObjectName: string, data: ObjectInfoResult): void {
|
|
212
|
+
let filePath: string;
|
|
213
|
+
try {
|
|
214
|
+
filePath = cacheFilePath(orgAlias, sObjectName);
|
|
215
|
+
} catch {
|
|
216
|
+
return; // invalid inputs — refuse to write
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
220
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
221
|
+
} catch {
|
|
222
|
+
// Non-critical
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseObjectInfoResponse(raw: any): ObjectInfoResult {
|
|
227
|
+
const fields: FieldMetadata[] = (raw.fields ?? []).map((f: any) => ({
|
|
228
|
+
apiName: f.ApiName,
|
|
229
|
+
label: f.label ?? null,
|
|
230
|
+
dataType: f.dataType ?? null,
|
|
231
|
+
required: f.required ?? false,
|
|
232
|
+
createable: f.createable ?? false,
|
|
233
|
+
updateable: f.updateable ?? false,
|
|
234
|
+
calculated: f.calculated ?? false,
|
|
235
|
+
custom: f.custom ?? false,
|
|
236
|
+
filterable: f.filterable ?? false,
|
|
237
|
+
sortable: f.sortable ?? false,
|
|
238
|
+
nameField: f.nameField ?? false,
|
|
239
|
+
reference: f.reference ?? false,
|
|
240
|
+
relationshipName: f.relationshipName ?? null,
|
|
241
|
+
compound: f.compound ?? false,
|
|
242
|
+
compoundFieldName: f.compoundFieldName ?? null,
|
|
243
|
+
defaultedOnCreate: f.defaultedOnCreate ?? false,
|
|
244
|
+
extraTypeInfo: f.extraTypeInfo ?? null,
|
|
245
|
+
inlineHelpText: f.inlineHelpText ?? null,
|
|
246
|
+
precision: f.precision ?? 0,
|
|
247
|
+
scale: f.scale ?? 0,
|
|
248
|
+
referenceToInfos: (f.referenceToInfos ?? []).map((r: any) => ({
|
|
249
|
+
apiName: r.ApiName,
|
|
250
|
+
nameFields: r.nameFields ?? [],
|
|
251
|
+
})),
|
|
252
|
+
controllerName: f.controllerName ?? null,
|
|
253
|
+
controllingFields: f.controllingFields ?? [],
|
|
254
|
+
}));
|
|
255
|
+
|
|
256
|
+
const picklists: PicklistFieldInfo[] = (raw.fields ?? [])
|
|
257
|
+
.filter((f: any) => f.picklistValuesByRecordTypeIDs)
|
|
258
|
+
.map((f: any) => {
|
|
259
|
+
const allValues = (f.picklistValuesByRecordTypeIDs ?? []).flatMap(
|
|
260
|
+
(rt: any) => rt.picklistValues ?? [],
|
|
261
|
+
);
|
|
262
|
+
// Drop entries with no value rather than recording `value: null`.
|
|
263
|
+
// Keeps the on-disk cache shape identical to pre-change behavior so
|
|
264
|
+
// consumers that don't null-guard (commands/type.ts, commands/review.ts)
|
|
265
|
+
// can't read explicit nulls back out after a rollback.
|
|
266
|
+
return {
|
|
267
|
+
apiName: f.ApiName,
|
|
268
|
+
label: f.label ?? f.ApiName,
|
|
269
|
+
required: f.required ?? false,
|
|
270
|
+
values: allValues
|
|
271
|
+
.filter((v: any) => v?.value != null)
|
|
272
|
+
.map((v: any) => ({ value: v.value, label: v.label ?? null })),
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
apiName: raw.ApiName,
|
|
278
|
+
label: raw.label ?? null,
|
|
279
|
+
labelPlural: raw.labelPlural ?? null,
|
|
280
|
+
createable: raw.createable ?? false,
|
|
281
|
+
deletable: raw.deletable ?? false,
|
|
282
|
+
updateable: raw.updateable ?? false,
|
|
283
|
+
queryable: raw.queryable ?? false,
|
|
284
|
+
searchable: raw.searchable ?? false,
|
|
285
|
+
custom: raw.custom ?? false,
|
|
286
|
+
keyPrefix: raw.keyPrefix ?? null,
|
|
287
|
+
nameFields: raw.nameFields ?? [],
|
|
288
|
+
defaultRecordTypeId: raw.defaultRecordTypeId ?? null,
|
|
289
|
+
fields,
|
|
290
|
+
childRelationships: (raw.childRelationships ?? []).map((cr: any) => ({
|
|
291
|
+
childObjectApiName: cr.childObjectApiName,
|
|
292
|
+
fieldName: cr.fieldName ?? null,
|
|
293
|
+
relationshipName: cr.relationshipName ?? null,
|
|
294
|
+
})),
|
|
295
|
+
recordTypeInfos: (raw.recordTypeInfos ?? []).map((rt: any) => ({
|
|
296
|
+
recordTypeId: rt.recordTypeId,
|
|
297
|
+
name: rt.name ?? null,
|
|
298
|
+
available: rt.available ?? false,
|
|
299
|
+
master: rt.master ?? false,
|
|
300
|
+
defaultRecordTypeMapping: rt.defaultRecordTypeMapping ?? false,
|
|
301
|
+
})),
|
|
302
|
+
picklists,
|
|
303
|
+
fetchedAt: new Date().toISOString(),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function getObjectInfo(
|
|
308
|
+
auth: OrgAuth,
|
|
309
|
+
orgAlias: string,
|
|
310
|
+
sObjectName: string,
|
|
311
|
+
refresh = false,
|
|
312
|
+
): Promise<ObjectInfoResult> {
|
|
313
|
+
const key = cacheKey(orgAlias, sObjectName);
|
|
314
|
+
|
|
315
|
+
if (!refresh) {
|
|
316
|
+
const mem = memoryCache.get(key);
|
|
317
|
+
if (mem) {
|
|
318
|
+
const age = Date.now() - new Date(mem.fetchedAt).getTime();
|
|
319
|
+
if (age <= CACHE_TTL_MS) return mem;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const disk = readDiskCache(orgAlias, sObjectName);
|
|
323
|
+
if (disk) {
|
|
324
|
+
memoryCache.set(key, disk);
|
|
325
|
+
return disk;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Master record type is the union of all picklist values for the field;
|
|
330
|
+
// non-master record types only restrict (hide) values, never add them. So
|
|
331
|
+
// a single fetch with [MASTER_RECORD_TYPE_ID] returns the complete set.
|
|
332
|
+
const result = await executeGraphQL(auth, OBJECT_INFO_QUERY, {
|
|
333
|
+
inputs: [{ apiName: sObjectName, recordTypeIDs: [MASTER_RECORD_TYPE_ID] }],
|
|
334
|
+
});
|
|
335
|
+
const infos = result?.data?.uiapi?.objectInfos;
|
|
336
|
+
if (!infos || infos.length === 0) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`No objectInfo returned for "${sObjectName}". Check that the object exists and is accessible.`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const parsed = parseObjectInfoResponse(infos[0]);
|
|
343
|
+
memoryCache.set(key, parsed);
|
|
344
|
+
writeDiskCache(orgAlias, sObjectName, parsed);
|
|
345
|
+
return parsed;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function getCachedObjectInfo(
|
|
349
|
+
orgAlias: string,
|
|
350
|
+
sObjectName: string,
|
|
351
|
+
): ObjectInfoResult | null {
|
|
352
|
+
const key = cacheKey(orgAlias, sObjectName);
|
|
353
|
+
const mem = memoryCache.get(key);
|
|
354
|
+
if (mem) return mem;
|
|
355
|
+
const disk = readDiskCache(orgAlias, sObjectName);
|
|
356
|
+
if (disk) {
|
|
357
|
+
memoryCache.set(key, disk);
|
|
358
|
+
return disk;
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Seed the in-memory ObjectInfo cache directly. Lets callers prime codegen
|
|
364
|
+
// without going through the live UIAPI fetch in `getObjectInfo`. Used by
|
|
365
|
+
// tests today; the planned MCP intent-layer prewarm helper (W-22694063)
|
|
366
|
+
// will reuse the same write path after a successful network fetch.
|
|
367
|
+
export function setCachedObjectInfo(
|
|
368
|
+
orgAlias: string,
|
|
369
|
+
sObjectName: string,
|
|
370
|
+
info: ObjectInfoResult,
|
|
371
|
+
): void {
|
|
372
|
+
memoryCache.set(cacheKey(orgAlias, sObjectName), info);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function getRequiredCreateFields(info: ObjectInfoResult): FieldMetadata[] {
|
|
376
|
+
return info.fields.filter((f) => f.required && f.createable && !f.defaultedOnCreate);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function getPicklistValues(
|
|
380
|
+
info: ObjectInfoResult,
|
|
381
|
+
fieldName: string,
|
|
382
|
+
): PicklistValue[] | null {
|
|
383
|
+
const picklist = info.picklists.find((p) => p.apiName === fieldName);
|
|
384
|
+
return picklist?.values ?? null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function clearObjectInfoCache(orgAlias?: string): void {
|
|
388
|
+
if (orgAlias) {
|
|
389
|
+
for (const [key] of memoryCache) {
|
|
390
|
+
if (key.startsWith(`${orgAlias}:`)) memoryCache.delete(key);
|
|
391
|
+
}
|
|
392
|
+
if (!ORG_ALIAS_PATH_RE.test(orgAlias)) return;
|
|
393
|
+
const root = path.resolve(cacheDir());
|
|
394
|
+
const dir = path.resolve(root, orgAlias);
|
|
395
|
+
if (!dir.startsWith(root + path.sep)) return;
|
|
396
|
+
try {
|
|
397
|
+
fs.rmSync(dir, { recursive: true });
|
|
398
|
+
} catch {
|
|
399
|
+
/* ok */
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
memoryCache.clear();
|
|
403
|
+
try {
|
|
404
|
+
fs.rmSync(cacheDir(), { recursive: true });
|
|
405
|
+
} catch {
|
|
406
|
+
/* ok */
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Library API for translating dotted parent-field paths into projection
|
|
9
|
+
* nodes. Encapsulates two UIAPI conventions that every caller would
|
|
10
|
+
* otherwise re-implement:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Value-wrapper unwrapping.** UIAPI scalar fields are exposed as
|
|
13
|
+
* object types with a single `value` field (e.g. `Name { value }`).
|
|
14
|
+
* A caller asking for "Name" gets `Name { value }` — except for
|
|
15
|
+
* `Id`, which is a real scalar. The structural test for "is this
|
|
16
|
+
* a wrapper?" lives in `lib/uiapi.ts:isValueWrapperType` so every
|
|
17
|
+
* caller agrees on the rule.
|
|
18
|
+
*
|
|
19
|
+
* 2. **Polymorphic union expansion.** Relationship fields like `Owner`
|
|
20
|
+
* resolve to a union (`User | Group`). A path like "Owner.Name"
|
|
21
|
+
* cannot be selected directly; it must be expanded to inline
|
|
22
|
+
* fragments per union member that has the field. Members lacking
|
|
23
|
+
* the field are skipped silently — a partial selection is more
|
|
24
|
+
* useful than a hard failure.
|
|
25
|
+
*
|
|
26
|
+
* The MCP intent layer used to carry this logic locally, but it's a
|
|
27
|
+
* pure function of (schema, path) and other graphiti consumers (CLI
|
|
28
|
+
* helpers, future surfaces) need the same behavior.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { isUnionType, type GraphQLSchema } from "graphql";
|
|
32
|
+
import { selectLeaf, type QuerySession } from "./session.js";
|
|
33
|
+
import { isValueWrapperType } from "./uiapi.js";
|
|
34
|
+
import { getFragmentTargets, MutationContextError, resolvePath } from "./walker.js";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Selects a scalar field at `basePath`, applying UIAPI value-wrapper
|
|
38
|
+
* unwrapping. `Id` is selected as a leaf directly; value-wrapper types
|
|
39
|
+
* are selected as `<field>/value`. Falls back to a direct selection
|
|
40
|
+
* when schema resolution fails — the downstream renderer/validator
|
|
41
|
+
* will surface a clearer error than we could here.
|
|
42
|
+
*/
|
|
43
|
+
function selectScalarOrValue(
|
|
44
|
+
session: QuerySession,
|
|
45
|
+
schema: GraphQLSchema,
|
|
46
|
+
basePath: string[],
|
|
47
|
+
fieldName: string,
|
|
48
|
+
): void {
|
|
49
|
+
const fullPath = [...basePath, fieldName];
|
|
50
|
+
|
|
51
|
+
if (fieldName === "Id") {
|
|
52
|
+
selectLeaf(session, fullPath);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const wr = resolvePath(schema, session.operation, fullPath);
|
|
58
|
+
if (wr.isLeaf) {
|
|
59
|
+
selectLeaf(session, fullPath);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (isValueWrapperType(schema, wr.typeName)) {
|
|
63
|
+
selectLeaf(session, [...fullPath, "value"]);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
selectLeaf(session, fullPath);
|
|
67
|
+
} catch {
|
|
68
|
+
selectLeaf(session, fullPath);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Selects a dotted field path (e.g. "Owner.Name") rooted at `basePath`,
|
|
74
|
+
* automatically expanding any segment that resolves to a polymorphic
|
|
75
|
+
* union into inline fragments on each member that has the field.
|
|
76
|
+
*
|
|
77
|
+
* Behavior contract:
|
|
78
|
+
* - "Name" (single segment) → calls `selectScalarOrValue`.
|
|
79
|
+
* - "Account.Name" (no union segments) → walks the dotted path, then
|
|
80
|
+
* selects the leaf with value-wrapper unwrapping.
|
|
81
|
+
* - "Owner.Name" where Owner is a union → emits
|
|
82
|
+
* `Owner { ... on User { Name { value } } ... on Group { Name { value } } }`.
|
|
83
|
+
* Members that don't have `Name` are skipped silently.
|
|
84
|
+
* - "Owner.BadField" where no union member has `BadField` → throws.
|
|
85
|
+
* A silent no-op would render an empty selection; failing loudly
|
|
86
|
+
* lets the caller correct the field name.
|
|
87
|
+
* - If the union segment is followed by more path components (e.g.
|
|
88
|
+
* `Owner.Account.Name`), the remaining path is appended to each
|
|
89
|
+
* member's inline fragment.
|
|
90
|
+
*/
|
|
91
|
+
export function selectDottedFieldPath(
|
|
92
|
+
session: QuerySession,
|
|
93
|
+
schema: GraphQLSchema,
|
|
94
|
+
basePath: string[],
|
|
95
|
+
dotField: string,
|
|
96
|
+
): void {
|
|
97
|
+
const parts = dotField.split(".");
|
|
98
|
+
const fieldName = parts.pop();
|
|
99
|
+
if (!fieldName) {
|
|
100
|
+
throw new Error(`Empty field name in dotted path "${dotField}"`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Walk parent segments looking for a union; expand the first one we hit.
|
|
104
|
+
const currentPath = [...basePath];
|
|
105
|
+
for (let i = 0; i < parts.length; i++) {
|
|
106
|
+
currentPath.push(parts[i]!);
|
|
107
|
+
|
|
108
|
+
let resolved;
|
|
109
|
+
try {
|
|
110
|
+
resolved = resolvePath(schema, session.operation, currentPath);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
// Re-throw mutation-context errors — the walker produces a clear,
|
|
113
|
+
// actionable message that callers should see rather than a confusing
|
|
114
|
+
// downstream validation failure.
|
|
115
|
+
if (err instanceof MutationContextError) {
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
// Other path resolution failures (typo, unknown field) — bail out
|
|
119
|
+
// of union detection and fall back to a flat selection below.
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!isUnionType(resolved.type)) continue;
|
|
124
|
+
|
|
125
|
+
const targets = getFragmentTargets(schema, resolved.type);
|
|
126
|
+
const remaining = parts.slice(i + 1);
|
|
127
|
+
let matchedAny = false;
|
|
128
|
+
for (const target of targets) {
|
|
129
|
+
const memberPath = [...currentPath, `[${target}]`, ...remaining, fieldName];
|
|
130
|
+
try {
|
|
131
|
+
const fieldResolved = resolvePath(schema, session.operation, memberPath);
|
|
132
|
+
if (fieldResolved.isLeaf) {
|
|
133
|
+
selectLeaf(session, memberPath);
|
|
134
|
+
} else if (isValueWrapperType(schema, fieldResolved.typeName)) {
|
|
135
|
+
selectLeaf(session, [...memberPath, "value"]);
|
|
136
|
+
} else {
|
|
137
|
+
selectLeaf(session, memberPath);
|
|
138
|
+
}
|
|
139
|
+
matchedAny = true;
|
|
140
|
+
} catch {
|
|
141
|
+
// Field doesn't exist on this union member — skip it. A
|
|
142
|
+
// partial selection across members that DO have the field
|
|
143
|
+
// is more useful than failing the whole call.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!matchedAny) {
|
|
147
|
+
// No union member exposed the requested field. Unlike the
|
|
148
|
+
// per-member skip above, this means the caller asked for
|
|
149
|
+
// something nothing can resolve — a silent no-op would
|
|
150
|
+
// render an empty selection set. Fail loudly so the caller
|
|
151
|
+
// can correct the field name.
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Field "${[...remaining, fieldName].join(".")}" not found on any member of union ` +
|
|
154
|
+
`${resolved.type.name} (members: ${targets.join(", ")})`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const parentPath = [...basePath, ...parts];
|
|
161
|
+
selectScalarOrValue(session, schema, parentPath, fieldName);
|
|
162
|
+
}
|