@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,195 @@
|
|
|
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 fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { getOrgAuth as realGetOrgAuth, type OrgAuth } from "./auth.js";
|
|
10
|
+
import {
|
|
11
|
+
downloadSchema as realDownloadSchema,
|
|
12
|
+
normalizeInstanceUrl,
|
|
13
|
+
schemaCacheKeyForInstanceUrl,
|
|
14
|
+
schemaDir,
|
|
15
|
+
type SchemaMetadata,
|
|
16
|
+
} from "./introspect.js";
|
|
17
|
+
|
|
18
|
+
// FR-13.7 originally suggested a 10-30s introspection budget. Empirical
|
|
19
|
+
// smoke testing against real Salesforce UIAPI schemas measures ~135s for
|
|
20
|
+
// a ~40MB introspection. We size the stale-lock threshold and the waiter
|
|
21
|
+
// timeout for ~3× that observed worst case (~7 min): comfortable headroom
|
|
22
|
+
// for normal operation while keeping crashed-holder recovery within a few
|
|
23
|
+
// minutes. If introspection ever legitimately exceeds 5 min the design
|
|
24
|
+
// assumption ("priming completes in minutes, not tens of minutes") has
|
|
25
|
+
// changed and these constants should be revisited together (likely with
|
|
26
|
+
// a heartbeat that refreshes the lock dir's mtime while held).
|
|
27
|
+
const STALE_LOCK_MS = 7 * 60_000;
|
|
28
|
+
const POLL_MS = 100;
|
|
29
|
+
const MAX_WAIT_MS = 7 * 60_000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Acquire a filesystem advisory lock at `${finalPath}.lock` (a directory,
|
|
33
|
+
* since `mkdir` is atomic on POSIX), run `work`, then release the lock.
|
|
34
|
+
*
|
|
35
|
+
* If another process holds the lock, this polls every 100ms for the lock
|
|
36
|
+
* to be released. On release, if `finalPath` now exists, the work function
|
|
37
|
+
* is skipped (the other process already did it). If `finalPath` still does
|
|
38
|
+
* not exist (the other process aborted without writing), this acquires the
|
|
39
|
+
* lock and runs `work`.
|
|
40
|
+
*
|
|
41
|
+
* Contract: `work` will NOT run when `finalPath` exists at any point during
|
|
42
|
+
* the lock dance — including immediately after we acquire the lock (a holder
|
|
43
|
+
* may have finished and released the lock between our last EEXIST poll and
|
|
44
|
+
* our successful `mkdir`). In that case the lock is released and the
|
|
45
|
+
* function returns `undefined`.
|
|
46
|
+
*
|
|
47
|
+
* Stale locks (older than STALE_LOCK_MS) are reclaimed.
|
|
48
|
+
*/
|
|
49
|
+
export async function withSchemaLock<T>(
|
|
50
|
+
finalPath: string,
|
|
51
|
+
work: () => Promise<T>,
|
|
52
|
+
): Promise<T | undefined> {
|
|
53
|
+
const lockPath = `${finalPath}.lock`;
|
|
54
|
+
fs.mkdirSync(path.dirname(finalPath), { recursive: true });
|
|
55
|
+
|
|
56
|
+
const startedWaitingAt = Date.now();
|
|
57
|
+
while (true) {
|
|
58
|
+
try {
|
|
59
|
+
fs.mkdirSync(lockPath); // atomic; throws EEXIST if held
|
|
60
|
+
break;
|
|
61
|
+
} catch (e: unknown) {
|
|
62
|
+
const err = e as NodeJS.ErrnoException;
|
|
63
|
+
if (err.code !== "EEXIST") throw e;
|
|
64
|
+
|
|
65
|
+
// If the cache exists now, another holder finished — short-circuit.
|
|
66
|
+
if (fs.existsSync(finalPath)) return undefined;
|
|
67
|
+
|
|
68
|
+
// Stale-lock reclaim. Known TOCTOU: between this `statSync` and
|
|
69
|
+
// the `rmSync` below, another process may have already reclaimed
|
|
70
|
+
// the stale lock and re-created a fresh one — in which case we'd
|
|
71
|
+
// remove the fresh lock. Window is microseconds and the next
|
|
72
|
+
// `mkdirSync` retry self-heals (we'd re-acquire or re-block);
|
|
73
|
+
// not worth fcntl-level locking to close.
|
|
74
|
+
try {
|
|
75
|
+
const stat = fs.statSync(lockPath);
|
|
76
|
+
if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
|
|
77
|
+
// Recursive removal: a stray file inside the lock dir
|
|
78
|
+
// (e.g. macOS `.DS_Store`) would make `rmdirSync` throw
|
|
79
|
+
// ENOTEMPTY, which the bare catch below would swallow,
|
|
80
|
+
// leaving the stale lock in place.
|
|
81
|
+
fs.rmSync(lockPath, { recursive: true });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// Lock vanished between EEXIST and stat — loop and retry mkdir.
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (Date.now() - startedWaitingAt > MAX_WAIT_MS) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Timed out waiting ${MAX_WAIT_MS}ms for schema priming lock at ${lockPath}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// We now hold the lock. Re-check for the cache: a holder that finished
|
|
98
|
+
// between our last EEXIST poll and our successful mkdir may have just
|
|
99
|
+
// primed it. Avoid running `work` redundantly.
|
|
100
|
+
if (fs.existsSync(finalPath)) {
|
|
101
|
+
try {
|
|
102
|
+
fs.rmSync(lockPath, { recursive: true });
|
|
103
|
+
} catch {
|
|
104
|
+
// best-effort
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
return await work();
|
|
111
|
+
} finally {
|
|
112
|
+
// Recursive remove tolerates stray files inside the lock dir (e.g.
|
|
113
|
+
// macOS `.DS_Store`) that would otherwise make `rmdirSync` throw
|
|
114
|
+
// ENOTEMPTY and leak the lock until the 7-min stale timeout.
|
|
115
|
+
try {
|
|
116
|
+
fs.rmSync(lockPath, { recursive: true });
|
|
117
|
+
} catch {
|
|
118
|
+
// Already gone — fine.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface PrimeResult {
|
|
124
|
+
cached: boolean;
|
|
125
|
+
filePath: string;
|
|
126
|
+
durationMs: number;
|
|
127
|
+
/** Resolved instance URL (already auth-resolved). */
|
|
128
|
+
instanceUrl: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Dependencies that `primeSchemaWithLock` calls. Defaulted to the real
|
|
133
|
+
* graphiti implementations; tests pass stubs.
|
|
134
|
+
*/
|
|
135
|
+
export interface PrimeDeps {
|
|
136
|
+
getOrgAuth: (orgAlias: string) => Promise<OrgAuth>;
|
|
137
|
+
downloadSchema: (auth: OrgAuth) => Promise<SchemaMetadata>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const REAL_DEPS: PrimeDeps = {
|
|
141
|
+
getOrgAuth: realGetOrgAuth,
|
|
142
|
+
downloadSchema: realDownloadSchema,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Lazily prime the on-disk schema cache for `orgAlias`.
|
|
147
|
+
*
|
|
148
|
+
* - If `<HOME>/.graphiti/schemas/<hash>.json` already exists, returns
|
|
149
|
+
* `{cached: true}` immediately.
|
|
150
|
+
* - Otherwise: resolves auth via `deps.getOrgAuth` (throws verbatim if the
|
|
151
|
+
* alias is not authenticated, per FR-13.5), acquires the schema lock via
|
|
152
|
+
* `withSchemaLock`, calls `deps.downloadSchema(auth)` (which writes the
|
|
153
|
+
* cache atomically via `downloadSchema`'s internal use of
|
|
154
|
+
* `atomicWriteJson`), releases the lock, and returns
|
|
155
|
+
* `{cached: false, durationMs}`. Per FR-13.6, partial caches never reach
|
|
156
|
+
* disk because writes are atomic.
|
|
157
|
+
*
|
|
158
|
+
* Concurrent callers in the same or different processes serialize on the
|
|
159
|
+
* lock dir; only one performs introspection. Subsequent waiters observe
|
|
160
|
+
* the primed cache and short-circuit (FR-13.4).
|
|
161
|
+
*/
|
|
162
|
+
export async function primeSchemaWithLock(
|
|
163
|
+
orgAlias: string,
|
|
164
|
+
deps: PrimeDeps = REAL_DEPS,
|
|
165
|
+
): Promise<PrimeResult> {
|
|
166
|
+
// We must call `deps.getOrgAuth` first because it is the only injected
|
|
167
|
+
// source of the org's `instanceUrl`, which the cache key is derived from.
|
|
168
|
+
// In tests this returns a stub; in production it is memoized inside
|
|
169
|
+
// `auth.ts`'s in-process cache, so the org is resolved via `@salesforce/core`
|
|
170
|
+
// at most once per org alias per process.
|
|
171
|
+
const auth = await deps.getOrgAuth(orgAlias);
|
|
172
|
+
const instanceUrl = normalizeInstanceUrl(auth.instanceUrl);
|
|
173
|
+
const cacheKey = schemaCacheKeyForInstanceUrl(instanceUrl);
|
|
174
|
+
const filePath = path.join(schemaDir(), `${cacheKey}.json`);
|
|
175
|
+
|
|
176
|
+
if (fs.existsSync(filePath)) {
|
|
177
|
+
return { cached: true, filePath, durationMs: 0, instanceUrl };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const start = Date.now();
|
|
181
|
+
let primed = false;
|
|
182
|
+
await withSchemaLock(filePath, async () => {
|
|
183
|
+
// withSchemaLock re-checks `finalPath` after acquire and short-
|
|
184
|
+
// circuits if it now exists, so when our work runs we know we
|
|
185
|
+
// must do the introspection.
|
|
186
|
+
await deps.downloadSchema(auth);
|
|
187
|
+
primed = true;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (!primed) {
|
|
191
|
+
// Another process primed while we were waiting.
|
|
192
|
+
return { cached: true, filePath, durationMs: Date.now() - start, instanceUrl };
|
|
193
|
+
}
|
|
194
|
+
return { cached: false, filePath, durationMs: Date.now() - start, instanceUrl };
|
|
195
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
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 { QuerySession, ProjectionNode, DirectiveNode } from "./session.js";
|
|
8
|
+
import { getChildren, getEffectiveArgs } from "./session.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Renders a QuerySession's selection tree into a properly formatted GraphQL query string.
|
|
12
|
+
*/
|
|
13
|
+
export function renderQuery(session: QuerySession): string {
|
|
14
|
+
const parts: string[] = [];
|
|
15
|
+
|
|
16
|
+
// Operation line with variables
|
|
17
|
+
const varDefs =
|
|
18
|
+
session.variables.length > 0
|
|
19
|
+
? `(${session.variables
|
|
20
|
+
.map((v) => {
|
|
21
|
+
let def = `$${v.name}: ${v.type}`;
|
|
22
|
+
if (v.defaultValue !== undefined) def += ` = ${v.defaultValue}`;
|
|
23
|
+
return def;
|
|
24
|
+
})
|
|
25
|
+
.join(", ")})`
|
|
26
|
+
: "";
|
|
27
|
+
|
|
28
|
+
const operationKeyword = session.operation === "aggregate" ? "query" : session.operation;
|
|
29
|
+
const operationName = session.operationName ? ` ${session.operationName}` : "";
|
|
30
|
+
const operationBody = renderChildren(session, null, 1);
|
|
31
|
+
if (operationBody) {
|
|
32
|
+
parts.push(`${operationKeyword}${operationName}${varDefs} {`);
|
|
33
|
+
parts.push(operationBody);
|
|
34
|
+
parts.push("}");
|
|
35
|
+
} else {
|
|
36
|
+
parts.push(`${operationKeyword}${operationName}${varDefs} { }`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return parts.join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderChildren(session: QuerySession, parentId: string | null, depth: number): string {
|
|
43
|
+
const _indent = " ".repeat(depth);
|
|
44
|
+
const lines: string[] = [];
|
|
45
|
+
|
|
46
|
+
for (const child of getChildren(session, parentId)) {
|
|
47
|
+
if (child.kind === "field") {
|
|
48
|
+
lines.push(renderField(session, child, depth));
|
|
49
|
+
} else {
|
|
50
|
+
lines.push(renderInlineFragment(session, child, depth));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderField(
|
|
58
|
+
session: QuerySession,
|
|
59
|
+
node: Extract<ProjectionNode, { kind: "field" }>,
|
|
60
|
+
depth: number,
|
|
61
|
+
): string {
|
|
62
|
+
const indent = " ".repeat(depth);
|
|
63
|
+
let line = indent;
|
|
64
|
+
|
|
65
|
+
// Alias
|
|
66
|
+
if (node.alias) {
|
|
67
|
+
line += `${node.alias}: `;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
line += node.fieldName;
|
|
71
|
+
|
|
72
|
+
// Arguments
|
|
73
|
+
const argEntries = Object.entries(getEffectiveArgs(session, node));
|
|
74
|
+
if (argEntries.length > 0) {
|
|
75
|
+
const argParts = argEntries.map(([name, value]) => {
|
|
76
|
+
return `${name}: ${formatArgValue(value)}`;
|
|
77
|
+
});
|
|
78
|
+
line += `(${argParts.join(", ")})`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Directives
|
|
82
|
+
for (const dir of node.directives) {
|
|
83
|
+
line += ` ${renderDirective(dir)}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Sub-selections
|
|
87
|
+
const childNodes = getChildren(session, node.id);
|
|
88
|
+
const hasChildren = childNodes.length > 0;
|
|
89
|
+
|
|
90
|
+
if (hasChildren) {
|
|
91
|
+
const childContent = renderChildren(session, node.id, depth + 1);
|
|
92
|
+
line += ` {\n${childContent}\n${indent}}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return line;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function renderInlineFragment(
|
|
99
|
+
session: QuerySession,
|
|
100
|
+
frag: Extract<ProjectionNode, { kind: "fragment" }>,
|
|
101
|
+
depth: number,
|
|
102
|
+
): string {
|
|
103
|
+
const indent = " ".repeat(depth);
|
|
104
|
+
let line = `${indent}... on ${frag.onType}`;
|
|
105
|
+
|
|
106
|
+
for (const dir of frag.directives) {
|
|
107
|
+
line += ` ${renderDirective(dir)}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const childContent = renderChildren(session, frag.id, depth + 1);
|
|
111
|
+
if (childContent) {
|
|
112
|
+
line += ` {\n${childContent}\n${indent}}`;
|
|
113
|
+
} else {
|
|
114
|
+
line += " { }";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return line;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderDirective(dir: DirectiveNode): string {
|
|
121
|
+
const argEntries = Object.entries(dir.args);
|
|
122
|
+
if (argEntries.length === 0) {
|
|
123
|
+
return `@${dir.name}`;
|
|
124
|
+
}
|
|
125
|
+
const argParts = argEntries.map(([name, value]) => `${name}: ${formatArgValue(value)}`);
|
|
126
|
+
return `@${dir.name}(${argParts.join(", ")})`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Formats an argument value for rendering in GraphQL.
|
|
131
|
+
* Handles variable references ($varName), raw JSON objects, numbers, booleans, and strings.
|
|
132
|
+
*/
|
|
133
|
+
function formatArgValue(value: string): string {
|
|
134
|
+
const trimmed = value.trim();
|
|
135
|
+
|
|
136
|
+
// Variable reference
|
|
137
|
+
if (trimmed.startsWith("$")) return trimmed;
|
|
138
|
+
|
|
139
|
+
// Numeric
|
|
140
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) return trimmed;
|
|
141
|
+
|
|
142
|
+
// Boolean / null
|
|
143
|
+
if (trimmed === "true" || trimmed === "false" || trimmed === "null") return trimmed;
|
|
144
|
+
|
|
145
|
+
// Enum value (unquoted identifier)
|
|
146
|
+
if (/^[A-Z_][A-Z0-9_]*$/i.test(trimmed) && trimmed === trimmed.toUpperCase()) return trimmed;
|
|
147
|
+
|
|
148
|
+
// JSON object or array — convert to GraphQL literal syntax
|
|
149
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
150
|
+
return jsonToGraphQL(trimmed);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Quoted string — pass through
|
|
154
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) return trimmed;
|
|
155
|
+
|
|
156
|
+
// Default: wrap in quotes
|
|
157
|
+
return `"${trimmed.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Converts a JSON string to GraphQL object literal syntax.
|
|
162
|
+
* GraphQL uses unquoted keys: { Status: { ne: "Closed" } }
|
|
163
|
+
*/
|
|
164
|
+
function jsonToGraphQL(jsonStr: string): string {
|
|
165
|
+
try {
|
|
166
|
+
const parsed = JSON.parse(jsonStr);
|
|
167
|
+
return valueToGraphQL(parsed);
|
|
168
|
+
} catch {
|
|
169
|
+
// If not valid JSON, return as-is (might already be in GraphQL format)
|
|
170
|
+
return jsonStr;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function valueToGraphQL(value: unknown): string {
|
|
175
|
+
if (value === null || value === undefined) return "null";
|
|
176
|
+
if (typeof value === "boolean") return String(value);
|
|
177
|
+
if (typeof value === "number") return String(value);
|
|
178
|
+
if (typeof value === "string") {
|
|
179
|
+
if (value.startsWith("$")) return value;
|
|
180
|
+
// Emit uppercase identifiers as bare enum tokens (e.g. DESC, ASC, EVERYTHING)
|
|
181
|
+
if (/^[A-Z_][A-Z0-9_]*$/.test(value)) return value;
|
|
182
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
183
|
+
}
|
|
184
|
+
if (Array.isArray(value)) {
|
|
185
|
+
return `[${value.map(valueToGraphQL).join(", ")}]`;
|
|
186
|
+
}
|
|
187
|
+
if (typeof value === "object") {
|
|
188
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
189
|
+
const parts = entries.map(([k, v]) => `${k}: ${valueToGraphQL(v)}`);
|
|
190
|
+
return `{ ${parts.join(", ")} }`;
|
|
191
|
+
}
|
|
192
|
+
return String(value);
|
|
193
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
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 { COMMANDS, type CommandDef } from "./command-registry.js";
|
|
8
|
+
import type { QuerySession } from "./session.js";
|
|
9
|
+
import type { WalkerResult } from "./walker.js";
|
|
10
|
+
|
|
11
|
+
// ── Interactive mode flag ─────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
let _interactiveMode = false;
|
|
14
|
+
|
|
15
|
+
export function setInteractiveMode(on: boolean): void {
|
|
16
|
+
_interactiveMode = on;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isInteractiveMode(): boolean {
|
|
20
|
+
return _interactiveMode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── Backward-compatible re-export ─────────────────────────────────────────────
|
|
24
|
+
// QUERY_COMMANDS was the original registry. It now delegates to the canonical
|
|
25
|
+
// COMMANDS array in command-registry.ts so there is a single source of truth.
|
|
26
|
+
export { COMMANDS as QUERY_COMMANDS };
|
|
27
|
+
|
|
28
|
+
// ── Formatting helpers ───────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export function formatHelp(topic?: string): string {
|
|
31
|
+
const lines: string[] = [];
|
|
32
|
+
|
|
33
|
+
if (topic) {
|
|
34
|
+
const spec: CommandDef | undefined = COMMANDS.find((c) => c.name === topic);
|
|
35
|
+
if (!spec) {
|
|
36
|
+
return `Unknown help topic: "${topic}". Run \`help\` with no arguments for a full list.`;
|
|
37
|
+
}
|
|
38
|
+
lines.push(`${spec.name} — ${spec.summary}`);
|
|
39
|
+
lines.push("");
|
|
40
|
+
const usagePrefix = _interactiveMode ? "" : "graphiti ";
|
|
41
|
+
const usageStr = spec.usage.startsWith("graphiti ")
|
|
42
|
+
? spec.usage
|
|
43
|
+
: `${usagePrefix}${spec.usage}`;
|
|
44
|
+
lines.push(`Usage: ${usageStr}`);
|
|
45
|
+
if (spec.subcommands && spec.subcommands.length > 0) {
|
|
46
|
+
lines.push("");
|
|
47
|
+
lines.push("Subcommands:");
|
|
48
|
+
for (const sub of spec.subcommands) {
|
|
49
|
+
lines.push(` ${sub.usage.padEnd(32)} ${sub.description}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (topic === "ls") {
|
|
54
|
+
lines.push("");
|
|
55
|
+
lines.push("Search behavior (--search):");
|
|
56
|
+
lines.push(' Multiple terms are OR-matched: --search "Name Id" matches fields');
|
|
57
|
+
lines.push(' containing either "Name" or "Id". Each term is prefix-matched');
|
|
58
|
+
lines.push(' against CamelCase word segments, so "Id" matches "AccountId"');
|
|
59
|
+
lines.push(' but not "Hide".');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (topic === "alias" || topic === "mkdir") {
|
|
63
|
+
lines.push("");
|
|
64
|
+
lines.push("Multi-query composition:");
|
|
65
|
+
lines.push(" Use alias to create multiple named instances of the same field,");
|
|
66
|
+
lines.push(" each with different arguments. This lets you combine several queries");
|
|
67
|
+
lines.push(" into a single GraphQL request (e.g. dashboard tiles + lists).");
|
|
68
|
+
lines.push("");
|
|
69
|
+
lines.push(" # Full end-to-end example (from /query/uiapi/query):");
|
|
70
|
+
lines.push(" alias newCases Case");
|
|
71
|
+
lines.push(' set newCases/@args/where \'{"Status":{"eq":"New"}}\'');
|
|
72
|
+
lines.push(" set newCases/@args/first 10");
|
|
73
|
+
lines.push(" select newCases/edges/node/Id newCases/edges/node/Subject/value");
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push(" alias myCases Case");
|
|
76
|
+
lines.push(" set myCases/@args/scope MINE");
|
|
77
|
+
lines.push(" set myCases/@args/first 20");
|
|
78
|
+
lines.push(" select myCases/edges/node/Id myCases/edges/node/Status/value");
|
|
79
|
+
lines.push("");
|
|
80
|
+
lines.push(" show # see the combined query");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (topic === "chain") {
|
|
84
|
+
lines.push("");
|
|
85
|
+
lines.push("Note: session management commands (new, use, connect, describe,");
|
|
86
|
+
lines.push("interactive) must be run as separate commands before chaining.");
|
|
87
|
+
lines.push("Only session-scoped commands (cd, ls, select, set, var, etc.)");
|
|
88
|
+
lines.push("can be used inside a chain.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (topic === "set" || topic === "assign") {
|
|
92
|
+
lines.push("");
|
|
93
|
+
lines.push("Inline key=value syntax (preferred):");
|
|
94
|
+
lines.push(" set first=10 # set on current field");
|
|
95
|
+
lines.push(" set uiapi/query/Case first=10 scope=MINE # with absolute field path");
|
|
96
|
+
lines.push(' set where=\'{"Status":{"eq":"New"}}\' # JSON value');
|
|
97
|
+
lines.push("");
|
|
98
|
+
lines.push("Legacy pair syntax:");
|
|
99
|
+
lines.push(" set @args/first 10");
|
|
100
|
+
lines.push(' set @args/where/Name/like "Acme%"');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
lines.push("");
|
|
104
|
+
lines.push("Examples:");
|
|
105
|
+
for (const ex of spec.examples) {
|
|
106
|
+
const exPrefix = _interactiveMode ? " " : " ";
|
|
107
|
+
lines.push(`${exPrefix}${ex}`);
|
|
108
|
+
}
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
lines.push("Graphiti CLI — Progressive GraphQL query builder for Salesforce");
|
|
113
|
+
lines.push("");
|
|
114
|
+
lines.push(
|
|
115
|
+
"Session resolution: --session / -s flag > GRAPHITI_SESSION env var > ~/.graphiti/active",
|
|
116
|
+
);
|
|
117
|
+
lines.push("JSON output: --json flag or GRAPHITI_AGENT=1 env var");
|
|
118
|
+
lines.push("");
|
|
119
|
+
lines.push("Setup:");
|
|
120
|
+
for (const name of ["new", "use", "connect"]) {
|
|
121
|
+
const spec = COMMANDS.find((c) => c.name === name);
|
|
122
|
+
if (spec) lines.push(` ${spec.name.padEnd(12)} ${spec.summary}`);
|
|
123
|
+
}
|
|
124
|
+
lines.push("");
|
|
125
|
+
lines.push("Navigation:");
|
|
126
|
+
for (const name of ["pwd", "ls", "cd"]) {
|
|
127
|
+
const spec = COMMANDS.find((c) => c.name === name)!;
|
|
128
|
+
lines.push(` ${spec.name.padEnd(12)} ${spec.summary}`);
|
|
129
|
+
}
|
|
130
|
+
lines.push("");
|
|
131
|
+
lines.push("Query Building:");
|
|
132
|
+
for (const name of ["select", "drop", "alias", "optional", "undo"]) {
|
|
133
|
+
const spec = COMMANDS.find((c) => c.name === name)!;
|
|
134
|
+
lines.push(` ${spec.name.padEnd(12)} ${spec.summary}`);
|
|
135
|
+
}
|
|
136
|
+
lines.push("");
|
|
137
|
+
lines.push("Arguments & Variables:");
|
|
138
|
+
for (const name of ["set", "unset", "var"]) {
|
|
139
|
+
const spec = COMMANDS.find((c) => c.name === name)!;
|
|
140
|
+
lines.push(` ${spec.name.padEnd(12)} ${spec.summary}`);
|
|
141
|
+
}
|
|
142
|
+
lines.push("");
|
|
143
|
+
lines.push("Review & Execute:");
|
|
144
|
+
for (const name of ["show", "check", "run", "describe", "codegen"]) {
|
|
145
|
+
const spec = COMMANDS.find((c) => c.name === name)!;
|
|
146
|
+
lines.push(` ${spec.name.padEnd(12)} ${spec.summary}`);
|
|
147
|
+
}
|
|
148
|
+
lines.push("");
|
|
149
|
+
lines.push("Session Management:");
|
|
150
|
+
for (const name of ["sessions", "clone", "reset"]) {
|
|
151
|
+
const spec = COMMANDS.find((c) => c.name === name)!;
|
|
152
|
+
lines.push(` ${spec.name.padEnd(12)} ${spec.summary}`);
|
|
153
|
+
}
|
|
154
|
+
lines.push("");
|
|
155
|
+
lines.push("Other:");
|
|
156
|
+
const otherCommands = _interactiveMode ? ["chain", "help"] : ["chain", "help", "interactive"];
|
|
157
|
+
for (const name of otherCommands) {
|
|
158
|
+
const spec = COMMANDS.find((c) => c.name === name);
|
|
159
|
+
if (spec) lines.push(` ${spec.name.padEnd(12)} ${spec.summary}`);
|
|
160
|
+
}
|
|
161
|
+
lines.push("");
|
|
162
|
+
lines.push("Run `help <subcommand>` for detailed usage and examples.");
|
|
163
|
+
return lines.join("\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Next-steps hints ─────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
export interface NextStepsContext {
|
|
169
|
+
sessionId: string;
|
|
170
|
+
command: string;
|
|
171
|
+
walkerResult?: WalkerResult;
|
|
172
|
+
session?: QuerySession;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function formatNextSteps(ctx: NextStepsContext): string {
|
|
176
|
+
const { sessionId: sid, command, walkerResult: wr, session } = ctx;
|
|
177
|
+
const lines: string[] = [];
|
|
178
|
+
const p = (cmd: string, desc: string) => {
|
|
179
|
+
if (_interactiveMode) {
|
|
180
|
+
lines.push(` ${cmd.padEnd(36)} # ${desc}`);
|
|
181
|
+
} else {
|
|
182
|
+
lines.push(` graphiti query ${sid} ${cmd.padEnd(36)} # ${desc}`);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const isRoot = !session || session.navigationPath.length === 0;
|
|
187
|
+
const hasSelections = session ? session.nodes.length > 0 : false;
|
|
188
|
+
const hasArgs = wr && wr.args.length > 0;
|
|
189
|
+
const isLeaf = wr?.isLeaf ?? false;
|
|
190
|
+
const _hasFragments = wr && wr.possibleTypes.length > 0;
|
|
191
|
+
const hasFields = wr && !wr.isLeaf && wr.fields.length > 0;
|
|
192
|
+
|
|
193
|
+
lines.push("");
|
|
194
|
+
lines.push("Next steps:");
|
|
195
|
+
|
|
196
|
+
switch (command) {
|
|
197
|
+
case "new": {
|
|
198
|
+
p("cd query", "navigate into the query tree");
|
|
199
|
+
p("ls", "see available root fields");
|
|
200
|
+
p("interactive", "start an interactive session");
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
case "ls": {
|
|
205
|
+
if (isLeaf) {
|
|
206
|
+
if (!isRoot && session) {
|
|
207
|
+
const leaf = session.navigationPath[session.navigationPath.length - 1];
|
|
208
|
+
p(`cd ..`, "go up to parent directory");
|
|
209
|
+
p(`select ${leaf}`, "select this leaf field");
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
if (hasFields) {
|
|
213
|
+
const firstDir = wr!.fields.find((f) => f.typeKind !== "SCALAR" && f.typeKind !== "ENUM");
|
|
214
|
+
const firstLeaf = wr!.fields.find(
|
|
215
|
+
(f) => f.typeKind === "SCALAR" || f.typeKind === "ENUM",
|
|
216
|
+
);
|
|
217
|
+
if (firstDir) p(`cd ${firstDir.name}`, `navigate into ${firstDir.name}/`);
|
|
218
|
+
if (firstLeaf) p(`select ${firstLeaf.name}`, `add ${firstLeaf.name} to projection`);
|
|
219
|
+
const hasConnections = wr!.fields.some((f) => f.typeName.includes("Connection"));
|
|
220
|
+
if (hasConnections && firstDir)
|
|
221
|
+
p(`alias myAlias ${firstDir.name}`, "compose multiple queries via aliases");
|
|
222
|
+
}
|
|
223
|
+
if (hasArgs) p(`cd @args`, "navigate into arguments");
|
|
224
|
+
if (!isRoot) p(`cd ..`, "go up one level");
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
case "cd": {
|
|
230
|
+
p(`ls`, "see contents at this path");
|
|
231
|
+
if (hasArgs) p(`cd @args`, "configure field arguments");
|
|
232
|
+
if (hasFields) {
|
|
233
|
+
const firstLeaf = wr!.fields.find((f) => f.typeKind === "SCALAR" || f.typeKind === "ENUM");
|
|
234
|
+
if (firstLeaf) p(`select ${firstLeaf.name}`, `add ${firstLeaf.name} to projection`);
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case "select": {
|
|
240
|
+
p(`ls`, "see more available fields");
|
|
241
|
+
if (!isRoot) p(`cd ..`, "go up to parent");
|
|
242
|
+
if (hasSelections) {
|
|
243
|
+
p(`show`, "preview the current query string");
|
|
244
|
+
p(`check`, "validate the query");
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case "assign":
|
|
250
|
+
case "set": {
|
|
251
|
+
p(`show`, "preview query with arguments");
|
|
252
|
+
p(`ls`, "see other fields to set");
|
|
253
|
+
if (hasSelections) p(`check`, "validate the query");
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
case "define":
|
|
258
|
+
case "var": {
|
|
259
|
+
p(`cd /variables/$varName`, "navigate into the variable to set values");
|
|
260
|
+
p(`show`, "preview the query signature");
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
case "show": {
|
|
265
|
+
p(`check`, "validate the query");
|
|
266
|
+
if (hasSelections) p(`run`, "execute the query against the org");
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
case "validate":
|
|
271
|
+
case "check": {
|
|
272
|
+
if (hasSelections) p(`run`, "execute the validated query");
|
|
273
|
+
p(`show`, "preview the query string");
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
default: {
|
|
278
|
+
p(`ls`, "list contents at current path");
|
|
279
|
+
p(`show`, "preview the current query");
|
|
280
|
+
if (hasSelections) p(`check`, "validate the query");
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (lines.length <= 2) {
|
|
286
|
+
p(`help`, "show all available commands");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return lines.join("\n");
|
|
290
|
+
}
|