@salesforce/graphiti 11.57.3 → 11.58.0
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 +12 -0
- package/CHANGELOG.md +10 -0
- package/README.md +28 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/download-schema.d.ts +81 -0
- package/dist/lib/download-schema.js +79 -0
- package/dist/lib/download-schema.js.map +1 -0
- package/package.json +11 -1
- package/src/index.ts +25 -0
- package/src/lib/__tests__/download-schema.spec.ts +167 -0
- package/src/lib/download-schema.ts +143 -0
package/AGENT_GUIDE.md
CHANGED
|
@@ -40,6 +40,18 @@ Notes:
|
|
|
40
40
|
- Exception: a **401/403 auth failure** during introspection (expired/unauthorized session) errors with an `Auth:` prefix instead of the soft staleness warning — a dead session makes even the cached schema unusable, so re-authenticate (`sf org login web --alias <org>`) rather than retrying.
|
|
41
41
|
- ObjectInfo is keyed by org alias: refreshing alias `A` does not invalidate ObjectInfo cached under a different alias `B` for the same org (its 1-hour TTL heals it).
|
|
42
42
|
|
|
43
|
+
## Sharing the schema cache from code
|
|
44
|
+
|
|
45
|
+
Build tooling (codegen, IDE integrations) should not run its own introspection — import graphiti and reuse the shared cache:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { downloadSchemaSdl } from "@salesforce/graphiti";
|
|
49
|
+
|
|
50
|
+
await downloadSchemaSdl({ org: "<org>", outPath: "schema.graphql", maxAgeMs: 10 * 60_000 });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This primes the same instance-URL-keyed cache `connect` uses, writes canonical SDL to `outPath`, and (via `maxAgeMs`) re-introspects only when the cached schema is older than the gate — otherwise it serves the existing cache ("wait once"). See the README's "Programmatic API" section for the full option/result reference.
|
|
54
|
+
|
|
43
55
|
## Session Resolution
|
|
44
56
|
|
|
45
57
|
All session commands target the **active session** implicitly. Resolution order:
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [11.58.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.57.4...v11.58.0) (2026-08-14)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- **graphiti:** expose downloadSchemaSdl library API for shared schema cache @W-23545346@ ([#856](https://github.com/salesforce-experience-platform-emu/webapps/issues/856)) ([8a29abe](https://github.com/salesforce-experience-platform-emu/webapps/commit/8a29abe0e3925020b816a6bcd2352d5b3d7236d9))
|
|
11
|
+
|
|
12
|
+
## [11.57.4](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.57.3...v11.57.4) (2026-08-14)
|
|
13
|
+
|
|
14
|
+
**Note:** Version bump only for package @salesforce/graphiti
|
|
15
|
+
|
|
6
16
|
## [11.57.3](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.57.2...v11.57.3) (2026-08-14)
|
|
7
17
|
|
|
8
18
|
**Note:** Version bump only for package @salesforce/graphiti
|
package/README.md
CHANGED
|
@@ -207,6 +207,34 @@ graphiti caches three things per org: the on-disk introspection JSON, the in-mem
|
|
|
207
207
|
|
|
208
208
|
> **ObjectInfo is keyed by org alias, not instance URL.** Refreshing alias `A` does not invalidate ObjectInfo cached under a different alias `B` that points at the same org; refresh each alias you actively use, or let ObjectInfo's 1-hour TTL heal it.
|
|
209
209
|
|
|
210
|
+
## Programmatic API (library)
|
|
211
|
+
|
|
212
|
+
graphiti is primarily a CLI, but it also exposes a small, stable programmatic surface so other tools can **share the same schema cache** instead of running their own introspection. The canonical use is a codegen or IDE pipeline that needs an org's GraphQL SDL on disk:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { downloadSchemaSdl } from "@salesforce/graphiti";
|
|
216
|
+
|
|
217
|
+
// Prime the shared cache (if needed) and write canonical SDL for codegen.
|
|
218
|
+
await downloadSchemaSdl({
|
|
219
|
+
org: process.env.SF_TARGET_ORG, // alias or username
|
|
220
|
+
outPath: "schema.graphql",
|
|
221
|
+
maxAgeMs: 10 * 60_000, // re-introspect if the cached schema is older than 10 min
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`downloadSchemaSdl(options)` primes the org's schema through the same instance-URL-keyed cache the CLI and MCP server use, then serializes it with `printSchema`:
|
|
226
|
+
|
|
227
|
+
| Option | Description |
|
|
228
|
+
| -------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
229
|
+
| `org` | Org alias or username (the value you'd pass to `--target-org`). |
|
|
230
|
+
| `outPath` | Where to atomically write the SDL (parent dirs are created). Omit to only receive the SDL in the result. |
|
|
231
|
+
| `forceRefresh` | Re-download even if cached, clearing all of graphiti's caches coherently (same as `connect --refresh`). |
|
|
232
|
+
| `maxAgeMs` | Age gate: force a refresh when the cached schema is older than this. A fresh cache — or an uncached org — is left to lazy prime. |
|
|
233
|
+
|
|
234
|
+
Because the **schema cache has no TTL** — a plain `connect` serves any existing schema indefinitely (only the separate ObjectInfo cache expires, on its own 1-hour TTL) — callers that must not run against stale metadata, such as codegen or IDE tooling, should pass a small `maxAgeMs`. Whoever asks first (CLI, MCP, or this call) pays the one-time introspection; everyone else reads the same cache, so you still "wait once."
|
|
235
|
+
|
|
236
|
+
The result reports what happened — `sdl`, `instanceUrl`, `cacheFilePath`, `outPath`, `downloaded`, `refreshed`, and `refreshedDueToAge`. `downloadSchemaSdl` is the only supported export today; deep `@salesforce/graphiti/dist/...` imports are internal and may change without a major version bump.
|
|
237
|
+
|
|
210
238
|
## Development
|
|
211
239
|
|
|
212
240
|
```bash
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Public library entry point for `@salesforce/graphiti`.
|
|
8
|
+
*
|
|
9
|
+
* graphiti is primarily a CLI. This module exposes just the one function build
|
|
10
|
+
* tooling needs — {@link downloadSchemaSdl} — so a codegen/IDE pipeline can
|
|
11
|
+
* obtain an org's GraphQL SDL from graphiti's single instance-URL-keyed schema
|
|
12
|
+
* cache (`~/.graphiti/schemas/`) instead of running its own introspection.
|
|
13
|
+
*
|
|
14
|
+
* Only the names re-exported here are supported; everything else (reachable via
|
|
15
|
+
* deep `@salesforce/graphiti/dist/...` paths) is internal and may change without
|
|
16
|
+
* a major version bump. The surface is kept intentionally small — more will be
|
|
17
|
+
* exported as concrete consumers need it, not before.
|
|
18
|
+
*/
|
|
19
|
+
export { downloadSchemaSdl, type DownloadSchemaSdlOptions, type DownloadSchemaSdlResult, } from "./lib/download-schema.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Public library entry point for `@salesforce/graphiti`.
|
|
8
|
+
*
|
|
9
|
+
* graphiti is primarily a CLI. This module exposes just the one function build
|
|
10
|
+
* tooling needs — {@link downloadSchemaSdl} — so a codegen/IDE pipeline can
|
|
11
|
+
* obtain an org's GraphQL SDL from graphiti's single instance-URL-keyed schema
|
|
12
|
+
* cache (`~/.graphiti/schemas/`) instead of running its own introspection.
|
|
13
|
+
*
|
|
14
|
+
* Only the names re-exported here are supported; everything else (reachable via
|
|
15
|
+
* deep `@salesforce/graphiti/dist/...` paths) is internal and may change without
|
|
16
|
+
* a major version bump. The surface is kept intentionally small — more will be
|
|
17
|
+
* exported as concrete consumers need it, not before.
|
|
18
|
+
*/
|
|
19
|
+
export { downloadSchemaSdl, } from "./lib/download-schema.js";
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;;;;;;GAYG;AAEH,OAAO,EACN,iBAAiB,GAGjB,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
import { type PrimeDeps } from "./prime-schema.js";
|
|
7
|
+
export interface DownloadSchemaSdlOptions {
|
|
8
|
+
/** Org alias or username (the value normally passed to `--target-org`). */
|
|
9
|
+
org: string;
|
|
10
|
+
/**
|
|
11
|
+
* Where to write the canonical GraphQL SDL (e.g. `schema.graphql` for a
|
|
12
|
+
* codegen pipeline). Written atomically; parent directories are created as
|
|
13
|
+
* needed. Omit to skip the write and only receive the SDL in the result.
|
|
14
|
+
*/
|
|
15
|
+
outPath?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Re-download even if the org's schema is already cached. Routes through the
|
|
18
|
+
* shared refresh path so all of graphiti's caches (introspection JSON,
|
|
19
|
+
* in-memory parsed schema, ObjectInfo) are cleared coherently.
|
|
20
|
+
*/
|
|
21
|
+
forceRefresh?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Age gate. When the shared cache's schema is older than this many
|
|
24
|
+
* milliseconds, this call forces a refresh (equivalent to `forceRefresh`).
|
|
25
|
+
* A cache younger than the threshold — or a not-yet-primed org — is left to
|
|
26
|
+
* the normal lazy-prime path, so a fresh cache is still served without a
|
|
27
|
+
* second download ("wait once"). Undefined or `<= 0` disables the gate.
|
|
28
|
+
*
|
|
29
|
+
* graphiti's cache has no TTL of its own — a plain `connect` serves any
|
|
30
|
+
* existing schema indefinitely — so callers that must not run against stale
|
|
31
|
+
* metadata (codegen, IDE tooling) should pass a small `maxAgeMs`.
|
|
32
|
+
*/
|
|
33
|
+
maxAgeMs?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Injectable priming dependencies (auth + introspection download). Defaults
|
|
36
|
+
* to the real graphiti implementations; tests pass stubs.
|
|
37
|
+
*/
|
|
38
|
+
deps?: PrimeDeps;
|
|
39
|
+
}
|
|
40
|
+
export interface DownloadSchemaSdlResult {
|
|
41
|
+
/** The org's canonical GraphQL SDL (`printSchema` of the primed schema). */
|
|
42
|
+
sdl: string;
|
|
43
|
+
/** Resolved Salesforce instance URL — the key the shared cache is stored under. */
|
|
44
|
+
instanceUrl: string;
|
|
45
|
+
/** Absolute path of the shared introspection JSON cache. */
|
|
46
|
+
cacheFilePath: string;
|
|
47
|
+
/** Absolute path the SDL was written to, or `undefined` when `outPath` was omitted. */
|
|
48
|
+
outPath?: string;
|
|
49
|
+
/**
|
|
50
|
+
* True when this call performed a network introspection (a fresh prime or a
|
|
51
|
+
* refresh that this process ran). A cache hit — including a refresh that
|
|
52
|
+
* coalesced onto a concurrent peer's download — is `false`.
|
|
53
|
+
*/
|
|
54
|
+
downloaded: boolean;
|
|
55
|
+
/** True when a refresh was requested, whether explicitly or by the age gate. */
|
|
56
|
+
refreshed: boolean;
|
|
57
|
+
/** True when the age gate (`maxAgeMs`) is what triggered the refresh. */
|
|
58
|
+
refreshedDueToAge: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Prime the org's schema through graphiti's shared, lock-coalesced cache and
|
|
62
|
+
* serialize it to canonical SDL — the programmatic equivalent of
|
|
63
|
+
* `graphiti connect <org>` plus an SDL export.
|
|
64
|
+
*
|
|
65
|
+
* This is the sanctioned way for other tools (codegen, IDE integrations) to
|
|
66
|
+
* obtain an org's schema: it shares graphiti's single instance-URL-keyed cache
|
|
67
|
+
* (`~/.graphiti/schemas/`), so whoever asks first — the CLI, the MCP server, or
|
|
68
|
+
* this function — pays the one-time introspection cost, and everyone else reads
|
|
69
|
+
* the same cache.
|
|
70
|
+
*
|
|
71
|
+
* Priming semantics are inherited from {@link primeSchemaWithLock}: a lazy prime
|
|
72
|
+
* on an uncached org, a no-op on a cached one, and a coherent cache-clearing
|
|
73
|
+
* re-download on `forceRefresh`. The SDL is read back via {@link getSchema},
|
|
74
|
+
* which after a refresh rebuilds from the freshly-downloaded introspection JSON,
|
|
75
|
+
* so the returned SDL always matches what is on disk.
|
|
76
|
+
*
|
|
77
|
+
* @throws the underlying auth/schema error verbatim on a lazy-prime failure, or
|
|
78
|
+
* `SchemaRefreshError` when a forced refresh fails but a usable cache survives
|
|
79
|
+
* (see {@link primeSchemaWithLock}).
|
|
80
|
+
*/
|
|
81
|
+
export declare function downloadSchemaSdl(opts: DownloadSchemaSdlOptions): Promise<DownloadSchemaSdlResult>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { printSchema } from "graphql";
|
|
8
|
+
import { getOrgAuth } from "./auth.js";
|
|
9
|
+
import { atomicWriteText } from "./fs-utils.js";
|
|
10
|
+
import { getSchemaMetadata } from "./introspect.js";
|
|
11
|
+
import { primeSchemaWithLock } from "./prime-schema.js";
|
|
12
|
+
import { getSchema } from "./walker.js";
|
|
13
|
+
/**
|
|
14
|
+
* Prime the org's schema through graphiti's shared, lock-coalesced cache and
|
|
15
|
+
* serialize it to canonical SDL — the programmatic equivalent of
|
|
16
|
+
* `graphiti connect <org>` plus an SDL export.
|
|
17
|
+
*
|
|
18
|
+
* This is the sanctioned way for other tools (codegen, IDE integrations) to
|
|
19
|
+
* obtain an org's schema: it shares graphiti's single instance-URL-keyed cache
|
|
20
|
+
* (`~/.graphiti/schemas/`), so whoever asks first — the CLI, the MCP server, or
|
|
21
|
+
* this function — pays the one-time introspection cost, and everyone else reads
|
|
22
|
+
* the same cache.
|
|
23
|
+
*
|
|
24
|
+
* Priming semantics are inherited from {@link primeSchemaWithLock}: a lazy prime
|
|
25
|
+
* on an uncached org, a no-op on a cached one, and a coherent cache-clearing
|
|
26
|
+
* re-download on `forceRefresh`. The SDL is read back via {@link getSchema},
|
|
27
|
+
* which after a refresh rebuilds from the freshly-downloaded introspection JSON,
|
|
28
|
+
* so the returned SDL always matches what is on disk.
|
|
29
|
+
*
|
|
30
|
+
* @throws the underlying auth/schema error verbatim on a lazy-prime failure, or
|
|
31
|
+
* `SchemaRefreshError` when a forced refresh fails but a usable cache survives
|
|
32
|
+
* (see {@link primeSchemaWithLock}).
|
|
33
|
+
*/
|
|
34
|
+
export async function downloadSchemaSdl(opts) {
|
|
35
|
+
const { org, deps } = opts;
|
|
36
|
+
const maxAgeMs = opts.maxAgeMs;
|
|
37
|
+
let forceRefresh = !!opts.forceRefresh;
|
|
38
|
+
let refreshedDueToAge = false;
|
|
39
|
+
// Age gate: only when a max age is set and we are not already refreshing.
|
|
40
|
+
// Resolving auth here is the only way to learn the instance URL the cache is
|
|
41
|
+
// keyed by; auth is memoized per alias, so the subsequent resolution inside
|
|
42
|
+
// primeSchemaWithLock is free (and in tests the injected stub is trivial).
|
|
43
|
+
if (!forceRefresh && maxAgeMs !== undefined && maxAgeMs > 0) {
|
|
44
|
+
const getAuth = deps?.getOrgAuth ?? getOrgAuth;
|
|
45
|
+
const auth = await getAuth(org);
|
|
46
|
+
const meta = getSchemaMetadata(auth.instanceUrl);
|
|
47
|
+
if (meta) {
|
|
48
|
+
// A NaN age (unparseable timestamp) compares false — we treat an
|
|
49
|
+
// unreadable cache as "not stale" and let the cache-hit path serve it;
|
|
50
|
+
// getSchema self-heals from the JSON if the SDL side-file is bad.
|
|
51
|
+
const ageMs = Date.now() - new Date(meta.downloadedAt).getTime();
|
|
52
|
+
if (ageMs > maxAgeMs) {
|
|
53
|
+
forceRefresh = true;
|
|
54
|
+
refreshedDueToAge = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const prime = await primeSchemaWithLock(org, deps, { forceRefresh });
|
|
59
|
+
const { instanceUrl } = prime;
|
|
60
|
+
// getSchema rebuilds from the on-disk introspection JSON after a refresh
|
|
61
|
+
// (the refresh evicted the in-memory + SDL caches), so this is always the
|
|
62
|
+
// schema that was just primed — never a stale copy.
|
|
63
|
+
const sdl = printSchema(getSchema(instanceUrl));
|
|
64
|
+
let resolvedOut;
|
|
65
|
+
if (opts.outPath) {
|
|
66
|
+
resolvedOut = path.resolve(opts.outPath);
|
|
67
|
+
atomicWriteText(resolvedOut, sdl);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
sdl,
|
|
71
|
+
instanceUrl,
|
|
72
|
+
cacheFilePath: prime.filePath,
|
|
73
|
+
outPath: resolvedOut,
|
|
74
|
+
downloaded: !prime.cached,
|
|
75
|
+
refreshed: forceRefresh,
|
|
76
|
+
refreshedDueToAge,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=download-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download-schema.js","sourceRoot":"","sources":["../../src/lib/download-schema.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAkB,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAyDxC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACtC,IAA8B;IAE9B,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC;IAC3B,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;IAE/B,IAAI,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC;IACvC,IAAI,iBAAiB,GAAG,KAAK,CAAC;IAE9B,0EAA0E;IAC1E,6EAA6E;IAC7E,4EAA4E;IAC5E,2EAA2E;IAC3E,IAAI,CAAC,YAAY,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;QAC7D,MAAM,OAAO,GAAG,IAAI,EAAE,UAAU,IAAI,UAAU,CAAC;QAC/C,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,MAAM,IAAI,GAAG,iBAAiB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACjD,IAAI,IAAI,EAAE,CAAC;YACV,iEAAiE;YACjE,uEAAuE;YACvE,kEAAkE;YAClE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC;YACjE,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC;gBACtB,YAAY,GAAG,IAAI,CAAC;gBACpB,iBAAiB,GAAG,IAAI,CAAC;YAC1B,CAAC;QACF,CAAC;IACF,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,mBAAmB,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,YAAY,EAAE,CAAC,CAAC;IACrE,MAAM,EAAE,WAAW,EAAE,GAAG,KAAK,CAAC;IAE9B,yEAAyE;IACzE,0EAA0E;IAC1E,oDAAoD;IACpD,MAAM,GAAG,GAAG,WAAW,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC;IAEhD,IAAI,WAA+B,CAAC;IACpC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QAClB,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACzC,eAAe,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;IACnC,CAAC;IAED,OAAO;QACN,GAAG;QACH,WAAW;QACX,aAAa,EAAE,KAAK,CAAC,QAAQ;QAC7B,OAAO,EAAE,WAAW;QACpB,UAAU,EAAE,CAAC,KAAK,CAAC,MAAM;QACzB,SAAS,EAAE,YAAY;QACvB,iBAAiB;KACjB,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/graphiti",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.58.0",
|
|
4
4
|
"description": "Progressive GraphQL query builder CLI for Salesforce orgs",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
7
17
|
"bin": {
|
|
8
18
|
"graphiti": "./dist/cli.js",
|
|
9
19
|
"graphiti-mcp": "./dist/mcp/stdio.js"
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
* Public library entry point for `@salesforce/graphiti`.
|
|
9
|
+
*
|
|
10
|
+
* graphiti is primarily a CLI. This module exposes just the one function build
|
|
11
|
+
* tooling needs — {@link downloadSchemaSdl} — so a codegen/IDE pipeline can
|
|
12
|
+
* obtain an org's GraphQL SDL from graphiti's single instance-URL-keyed schema
|
|
13
|
+
* cache (`~/.graphiti/schemas/`) instead of running its own introspection.
|
|
14
|
+
*
|
|
15
|
+
* Only the names re-exported here are supported; everything else (reachable via
|
|
16
|
+
* deep `@salesforce/graphiti/dist/...` paths) is internal and may change without
|
|
17
|
+
* a major version bump. The surface is kept intentionally small — more will be
|
|
18
|
+
* exported as concrete consumers need it, not before.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
downloadSchemaSdl,
|
|
23
|
+
type DownloadSchemaSdlOptions,
|
|
24
|
+
type DownloadSchemaSdlResult,
|
|
25
|
+
} from "./lib/download-schema.js";
|
|
@@ -0,0 +1,167 @@
|
|
|
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 os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { buildSchema } from "graphql";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
12
|
+
import { makeNoopPrimeDeps } from "../../__tests__/helpers/prime-deps.js";
|
|
13
|
+
import { downloadSchemaSdl } from "../download-schema.js";
|
|
14
|
+
import { schemaCacheKeyForInstanceUrl, schemaDir } from "../introspect.js";
|
|
15
|
+
import { type PrimeDeps } from "../prime-schema.js";
|
|
16
|
+
import { clearSchemaCache } from "../walker.js";
|
|
17
|
+
|
|
18
|
+
const ORG = "download-sdl-org";
|
|
19
|
+
const ORG_URL = "https://download-sdl-org.my.salesforce.com";
|
|
20
|
+
// A schema with a recognizable field so we can assert the emitted SDL is the
|
|
21
|
+
// one we primed, not an empty stub.
|
|
22
|
+
const SCHEMA = buildSchema(`type Query { hello: String }`);
|
|
23
|
+
|
|
24
|
+
describe("lib/download-schema", () => {
|
|
25
|
+
let tmpRoot: string;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-download-sdl-"));
|
|
29
|
+
process.env.GRAPHITI_HOME = tmpRoot;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
delete process.env.GRAPHITI_HOME;
|
|
34
|
+
// getSchema populates a module-level parsed-schema cache keyed by instance
|
|
35
|
+
// URL; clear it so a shared ORG_URL can't leak a schema between tests.
|
|
36
|
+
clearSchemaCache();
|
|
37
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function cacheFile(): string {
|
|
41
|
+
return path.join(schemaDir(), `${schemaCacheKeyForInstanceUrl(ORG_URL)}.json`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Wrap real deps with a download counter, mirroring connect.spec.ts. */
|
|
45
|
+
function counting(base: PrimeDeps): { deps: PrimeDeps; calls: () => number } {
|
|
46
|
+
let calls = 0;
|
|
47
|
+
return {
|
|
48
|
+
deps: {
|
|
49
|
+
getOrgAuth: base.getOrgAuth,
|
|
50
|
+
downloadSchema: async (a) => {
|
|
51
|
+
calls++;
|
|
52
|
+
return base.downloadSchema(a);
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
calls: () => calls,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
it("primes a fresh (uncached) org, writes SDL to outPath, and reports downloaded", async () => {
|
|
60
|
+
const { deps, calls } = counting(makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA));
|
|
61
|
+
const outPath = path.join(tmpRoot, "schema.graphql");
|
|
62
|
+
|
|
63
|
+
const result = await downloadSchemaSdl({ org: ORG, outPath, deps });
|
|
64
|
+
|
|
65
|
+
expect(calls()).toBe(1); // fresh org → one introspection
|
|
66
|
+
expect(result.downloaded).toBe(true);
|
|
67
|
+
expect(result.refreshed).toBe(false);
|
|
68
|
+
expect(result.refreshedDueToAge).toBe(false);
|
|
69
|
+
expect(result.instanceUrl).toBe(ORG_URL);
|
|
70
|
+
expect(fs.existsSync(cacheFile())).toBe(true); // shared cache populated
|
|
71
|
+
expect(result.outPath).toBe(path.resolve(outPath));
|
|
72
|
+
expect(fs.readFileSync(outPath, "utf-8")).toBe(result.sdl);
|
|
73
|
+
expect(result.sdl).toMatch(/type Query/);
|
|
74
|
+
expect(result.sdl).toMatch(/hello: String/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("second call is a cache hit: no re-download, still returns and writes the SDL (wait once)", async () => {
|
|
78
|
+
const base = makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
|
|
79
|
+
await downloadSchemaSdl({ org: ORG, deps: base }); // prime once
|
|
80
|
+
|
|
81
|
+
const { deps, calls } = counting(base);
|
|
82
|
+
const outPath = path.join(tmpRoot, "schema.graphql");
|
|
83
|
+
const result = await downloadSchemaSdl({ org: ORG, outPath, deps });
|
|
84
|
+
|
|
85
|
+
expect(calls()).toBe(0); // cache hit — no second introspection
|
|
86
|
+
expect(result.downloaded).toBe(false);
|
|
87
|
+
expect(result.refreshed).toBe(false);
|
|
88
|
+
expect(fs.readFileSync(outPath, "utf-8")).toBe(result.sdl);
|
|
89
|
+
expect(result.sdl).toMatch(/type Query/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("omitting outPath returns the SDL without writing an export file", async () => {
|
|
93
|
+
const base = makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
|
|
94
|
+
const exportPath = path.join(tmpRoot, "schema.graphql");
|
|
95
|
+
const result = await downloadSchemaSdl({ org: ORG, deps: base });
|
|
96
|
+
|
|
97
|
+
expect(result.outPath).toBeUndefined();
|
|
98
|
+
expect(result.sdl).toMatch(/type Query/);
|
|
99
|
+
// With no outPath, downloadSchemaSdl writes no export. (getSchema still
|
|
100
|
+
// maintains its own `<cacheKey>.graphql` cache side-file under schemaDir,
|
|
101
|
+
// but never the caller-named export path.)
|
|
102
|
+
expect(fs.existsSync(exportPath)).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("maxAgeMs forces a refresh when the cached schema is older than the gate", async () => {
|
|
106
|
+
const base = makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
|
|
107
|
+
await downloadSchemaSdl({ org: ORG, deps: base }); // prime once (fresh)
|
|
108
|
+
|
|
109
|
+
// Backdate the shared cache so it reads as ~20 min old. getSchemaMetadata
|
|
110
|
+
// derives `downloadedAt` from the file mtime, so this is what the gate sees.
|
|
111
|
+
const past = new Date(Date.now() - 20 * 60_000);
|
|
112
|
+
fs.utimesSync(cacheFile(), past, past);
|
|
113
|
+
|
|
114
|
+
const { deps, calls } = counting(base);
|
|
115
|
+
const outPath = path.join(tmpRoot, "schema.graphql");
|
|
116
|
+
const result = await downloadSchemaSdl({
|
|
117
|
+
org: ORG,
|
|
118
|
+
outPath,
|
|
119
|
+
maxAgeMs: 10 * 60_000,
|
|
120
|
+
deps,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(calls()).toBe(1); // stale → forced re-download
|
|
124
|
+
expect(result.downloaded).toBe(true);
|
|
125
|
+
expect(result.refreshed).toBe(true);
|
|
126
|
+
expect(result.refreshedDueToAge).toBe(true);
|
|
127
|
+
expect(fs.readFileSync(outPath, "utf-8")).toBe(result.sdl);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("maxAgeMs leaves a fresh cache untouched — wait once is preserved", async () => {
|
|
131
|
+
const base = makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
|
|
132
|
+
await downloadSchemaSdl({ org: ORG, deps: base }); // prime once (just now)
|
|
133
|
+
|
|
134
|
+
const { deps, calls } = counting(base);
|
|
135
|
+
const result = await downloadSchemaSdl({ org: ORG, maxAgeMs: 10 * 60_000, deps });
|
|
136
|
+
|
|
137
|
+
expect(calls()).toBe(0); // young cache → no refresh
|
|
138
|
+
expect(result.downloaded).toBe(false);
|
|
139
|
+
expect(result.refreshed).toBe(false);
|
|
140
|
+
expect(result.refreshedDueToAge).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("maxAgeMs on an uncached org lazily primes (no false 'stale') ", async () => {
|
|
144
|
+
const { deps, calls } = counting(makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA));
|
|
145
|
+
// No prior cache: the gate finds no metadata and must not force a refresh;
|
|
146
|
+
// the normal lazy prime downloads exactly once.
|
|
147
|
+
const result = await downloadSchemaSdl({ org: ORG, maxAgeMs: 10 * 60_000, deps });
|
|
148
|
+
|
|
149
|
+
expect(calls()).toBe(1);
|
|
150
|
+
expect(result.downloaded).toBe(true);
|
|
151
|
+
expect(result.refreshed).toBe(false);
|
|
152
|
+
expect(result.refreshedDueToAge).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("forceRefresh re-downloads even a fresh cache; refreshedDueToAge stays false", async () => {
|
|
156
|
+
const base = makeNoopPrimeDeps(ORG, ORG_URL, SCHEMA);
|
|
157
|
+
await downloadSchemaSdl({ org: ORG, deps: base }); // prime once
|
|
158
|
+
|
|
159
|
+
const { deps, calls } = counting(base);
|
|
160
|
+
const result = await downloadSchemaSdl({ org: ORG, forceRefresh: true, deps });
|
|
161
|
+
|
|
162
|
+
expect(calls()).toBe(1); // explicit refresh re-introspects
|
|
163
|
+
expect(result.downloaded).toBe(true);
|
|
164
|
+
expect(result.refreshed).toBe(true);
|
|
165
|
+
expect(result.refreshedDueToAge).toBe(false); // it was explicit, not the gate
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { printSchema } from "graphql";
|
|
9
|
+
import { getOrgAuth } from "./auth.js";
|
|
10
|
+
import { atomicWriteText } from "./fs-utils.js";
|
|
11
|
+
import { getSchemaMetadata } from "./introspect.js";
|
|
12
|
+
import { type PrimeDeps, primeSchemaWithLock } from "./prime-schema.js";
|
|
13
|
+
import { getSchema } from "./walker.js";
|
|
14
|
+
|
|
15
|
+
export interface DownloadSchemaSdlOptions {
|
|
16
|
+
/** Org alias or username (the value normally passed to `--target-org`). */
|
|
17
|
+
org: string;
|
|
18
|
+
/**
|
|
19
|
+
* Where to write the canonical GraphQL SDL (e.g. `schema.graphql` for a
|
|
20
|
+
* codegen pipeline). Written atomically; parent directories are created as
|
|
21
|
+
* needed. Omit to skip the write and only receive the SDL in the result.
|
|
22
|
+
*/
|
|
23
|
+
outPath?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Re-download even if the org's schema is already cached. Routes through the
|
|
26
|
+
* shared refresh path so all of graphiti's caches (introspection JSON,
|
|
27
|
+
* in-memory parsed schema, ObjectInfo) are cleared coherently.
|
|
28
|
+
*/
|
|
29
|
+
forceRefresh?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Age gate. When the shared cache's schema is older than this many
|
|
32
|
+
* milliseconds, this call forces a refresh (equivalent to `forceRefresh`).
|
|
33
|
+
* A cache younger than the threshold — or a not-yet-primed org — is left to
|
|
34
|
+
* the normal lazy-prime path, so a fresh cache is still served without a
|
|
35
|
+
* second download ("wait once"). Undefined or `<= 0` disables the gate.
|
|
36
|
+
*
|
|
37
|
+
* graphiti's cache has no TTL of its own — a plain `connect` serves any
|
|
38
|
+
* existing schema indefinitely — so callers that must not run against stale
|
|
39
|
+
* metadata (codegen, IDE tooling) should pass a small `maxAgeMs`.
|
|
40
|
+
*/
|
|
41
|
+
maxAgeMs?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Injectable priming dependencies (auth + introspection download). Defaults
|
|
44
|
+
* to the real graphiti implementations; tests pass stubs.
|
|
45
|
+
*/
|
|
46
|
+
deps?: PrimeDeps;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DownloadSchemaSdlResult {
|
|
50
|
+
/** The org's canonical GraphQL SDL (`printSchema` of the primed schema). */
|
|
51
|
+
sdl: string;
|
|
52
|
+
/** Resolved Salesforce instance URL — the key the shared cache is stored under. */
|
|
53
|
+
instanceUrl: string;
|
|
54
|
+
/** Absolute path of the shared introspection JSON cache. */
|
|
55
|
+
cacheFilePath: string;
|
|
56
|
+
/** Absolute path the SDL was written to, or `undefined` when `outPath` was omitted. */
|
|
57
|
+
outPath?: string;
|
|
58
|
+
/**
|
|
59
|
+
* True when this call performed a network introspection (a fresh prime or a
|
|
60
|
+
* refresh that this process ran). A cache hit — including a refresh that
|
|
61
|
+
* coalesced onto a concurrent peer's download — is `false`.
|
|
62
|
+
*/
|
|
63
|
+
downloaded: boolean;
|
|
64
|
+
/** True when a refresh was requested, whether explicitly or by the age gate. */
|
|
65
|
+
refreshed: boolean;
|
|
66
|
+
/** True when the age gate (`maxAgeMs`) is what triggered the refresh. */
|
|
67
|
+
refreshedDueToAge: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Prime the org's schema through graphiti's shared, lock-coalesced cache and
|
|
72
|
+
* serialize it to canonical SDL — the programmatic equivalent of
|
|
73
|
+
* `graphiti connect <org>` plus an SDL export.
|
|
74
|
+
*
|
|
75
|
+
* This is the sanctioned way for other tools (codegen, IDE integrations) to
|
|
76
|
+
* obtain an org's schema: it shares graphiti's single instance-URL-keyed cache
|
|
77
|
+
* (`~/.graphiti/schemas/`), so whoever asks first — the CLI, the MCP server, or
|
|
78
|
+
* this function — pays the one-time introspection cost, and everyone else reads
|
|
79
|
+
* the same cache.
|
|
80
|
+
*
|
|
81
|
+
* Priming semantics are inherited from {@link primeSchemaWithLock}: a lazy prime
|
|
82
|
+
* on an uncached org, a no-op on a cached one, and a coherent cache-clearing
|
|
83
|
+
* re-download on `forceRefresh`. The SDL is read back via {@link getSchema},
|
|
84
|
+
* which after a refresh rebuilds from the freshly-downloaded introspection JSON,
|
|
85
|
+
* so the returned SDL always matches what is on disk.
|
|
86
|
+
*
|
|
87
|
+
* @throws the underlying auth/schema error verbatim on a lazy-prime failure, or
|
|
88
|
+
* `SchemaRefreshError` when a forced refresh fails but a usable cache survives
|
|
89
|
+
* (see {@link primeSchemaWithLock}).
|
|
90
|
+
*/
|
|
91
|
+
export async function downloadSchemaSdl(
|
|
92
|
+
opts: DownloadSchemaSdlOptions,
|
|
93
|
+
): Promise<DownloadSchemaSdlResult> {
|
|
94
|
+
const { org, deps } = opts;
|
|
95
|
+
const maxAgeMs = opts.maxAgeMs;
|
|
96
|
+
|
|
97
|
+
let forceRefresh = !!opts.forceRefresh;
|
|
98
|
+
let refreshedDueToAge = false;
|
|
99
|
+
|
|
100
|
+
// Age gate: only when a max age is set and we are not already refreshing.
|
|
101
|
+
// Resolving auth here is the only way to learn the instance URL the cache is
|
|
102
|
+
// keyed by; auth is memoized per alias, so the subsequent resolution inside
|
|
103
|
+
// primeSchemaWithLock is free (and in tests the injected stub is trivial).
|
|
104
|
+
if (!forceRefresh && maxAgeMs !== undefined && maxAgeMs > 0) {
|
|
105
|
+
const getAuth = deps?.getOrgAuth ?? getOrgAuth;
|
|
106
|
+
const auth = await getAuth(org);
|
|
107
|
+
const meta = getSchemaMetadata(auth.instanceUrl);
|
|
108
|
+
if (meta) {
|
|
109
|
+
// A NaN age (unparseable timestamp) compares false — we treat an
|
|
110
|
+
// unreadable cache as "not stale" and let the cache-hit path serve it;
|
|
111
|
+
// getSchema self-heals from the JSON if the SDL side-file is bad.
|
|
112
|
+
const ageMs = Date.now() - new Date(meta.downloadedAt).getTime();
|
|
113
|
+
if (ageMs > maxAgeMs) {
|
|
114
|
+
forceRefresh = true;
|
|
115
|
+
refreshedDueToAge = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const prime = await primeSchemaWithLock(org, deps, { forceRefresh });
|
|
121
|
+
const { instanceUrl } = prime;
|
|
122
|
+
|
|
123
|
+
// getSchema rebuilds from the on-disk introspection JSON after a refresh
|
|
124
|
+
// (the refresh evicted the in-memory + SDL caches), so this is always the
|
|
125
|
+
// schema that was just primed — never a stale copy.
|
|
126
|
+
const sdl = printSchema(getSchema(instanceUrl));
|
|
127
|
+
|
|
128
|
+
let resolvedOut: string | undefined;
|
|
129
|
+
if (opts.outPath) {
|
|
130
|
+
resolvedOut = path.resolve(opts.outPath);
|
|
131
|
+
atomicWriteText(resolvedOut, sdl);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
sdl,
|
|
136
|
+
instanceUrl,
|
|
137
|
+
cacheFilePath: prime.filePath,
|
|
138
|
+
outPath: resolvedOut,
|
|
139
|
+
downloaded: !prime.cached,
|
|
140
|
+
refreshed: forceRefresh,
|
|
141
|
+
refreshedDueToAge,
|
|
142
|
+
};
|
|
143
|
+
}
|