/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import path from "node:path"; import { printSchema } from "graphql"; import { getOrgAuth } from "./auth.js"; import { atomicWriteText } from "./fs-utils.js"; import { getSchemaMetadata } from "./introspect.js"; import { type PrimeDeps, primeSchemaWithLock } from "./prime-schema.js"; import { getSchema } from "./walker.js"; export interface DownloadSchemaSdlOptions { /** Org alias or username (the value normally passed to `--target-org`). */ org: string; /** * Where to write the canonical GraphQL SDL (e.g. `schema.graphql` for a * codegen pipeline). Written atomically; parent directories are created as * needed. Omit to skip the write and only receive the SDL in the result. */ outPath?: string; /** * Re-download even if the org's schema is already cached. Routes through the * shared refresh path so all of graphiti's caches (introspection JSON, * in-memory parsed schema, ObjectInfo) are cleared coherently. */ forceRefresh?: boolean; /** * Age gate. When the shared cache's schema is older than this many * milliseconds, this call forces a refresh (equivalent to `forceRefresh`). * A cache younger than the threshold — or a not-yet-primed org — is left to * the normal lazy-prime path, so a fresh cache is still served without a * second download ("wait once"). Undefined or `<= 0` disables the gate. * * graphiti's cache has no TTL of its own — a plain `connect` serves any * existing schema indefinitely — so callers that must not run against stale * metadata (codegen, IDE tooling) should pass a small `maxAgeMs`. */ maxAgeMs?: number; /** * Injectable priming dependencies (auth + introspection download). Defaults * to the real graphiti implementations; tests pass stubs. */ deps?: PrimeDeps; } export interface DownloadSchemaSdlResult { /** The org's canonical GraphQL SDL (`printSchema` of the primed schema). */ sdl: string; /** Resolved Salesforce instance URL — the key the shared cache is stored under. */ instanceUrl: string; /** Absolute path of the shared introspection JSON cache. */ cacheFilePath: string; /** Absolute path the SDL was written to, or `undefined` when `outPath` was omitted. */ outPath?: string; /** * True when this call performed a network introspection (a fresh prime or a * refresh that this process ran). A cache hit — including a refresh that * coalesced onto a concurrent peer's download — is `false`. */ downloaded: boolean; /** True when a refresh was requested, whether explicitly or by the age gate. */ refreshed: boolean; /** True when the age gate (`maxAgeMs`) is what triggered the refresh. */ refreshedDueToAge: boolean; } /** * Prime the org's schema through graphiti's shared, lock-coalesced cache and * serialize it to canonical SDL — the programmatic equivalent of * `graphiti connect ` plus an SDL export. * * This is the sanctioned way for other tools (codegen, IDE integrations) to * obtain an org's schema: it shares graphiti's single instance-URL-keyed cache * (`~/.graphiti/schemas/`), so whoever asks first — the CLI, the MCP server, or * this function — pays the one-time introspection cost, and everyone else reads * the same cache. * * Priming semantics are inherited from {@link primeSchemaWithLock}: a lazy prime * on an uncached org, a no-op on a cached one, and a coherent cache-clearing * re-download on `forceRefresh`. The SDL is read back via {@link getSchema}, * which after a refresh rebuilds from the freshly-downloaded introspection JSON, * so the returned SDL always matches what is on disk. * * @throws the underlying auth/schema error verbatim on a lazy-prime failure, or * `SchemaRefreshError` when a forced refresh fails but a usable cache survives * (see {@link primeSchemaWithLock}). */ export async function downloadSchemaSdl( opts: DownloadSchemaSdlOptions, ): Promise { const { org, deps } = opts; const maxAgeMs = opts.maxAgeMs; let forceRefresh = !!opts.forceRefresh; let refreshedDueToAge = false; // Age gate: only when a max age is set and we are not already refreshing. // Resolving auth here is the only way to learn the instance URL the cache is // keyed by; auth is memoized per alias, so the subsequent resolution inside // primeSchemaWithLock is free (and in tests the injected stub is trivial). if (!forceRefresh && maxAgeMs !== undefined && maxAgeMs > 0) { const getAuth = deps?.getOrgAuth ?? getOrgAuth; const auth = await getAuth(org); const meta = getSchemaMetadata(auth.instanceUrl); if (meta) { // A NaN age (unparseable timestamp) compares false — we treat an // unreadable cache as "not stale" and let the cache-hit path serve it; // getSchema self-heals from the JSON if the SDL side-file is bad. const ageMs = Date.now() - new Date(meta.downloadedAt).getTime(); if (ageMs > maxAgeMs) { forceRefresh = true; refreshedDueToAge = true; } } } const prime = await primeSchemaWithLock(org, deps, { forceRefresh }); const { instanceUrl } = prime; // getSchema rebuilds from the on-disk introspection JSON after a refresh // (the refresh evicted the in-memory + SDL caches), so this is always the // schema that was just primed — never a stale copy. const sdl = printSchema(getSchema(instanceUrl)); let resolvedOut: string | undefined; if (opts.outPath) { resolvedOut = path.resolve(opts.outPath); atomicWriteText(resolvedOut, sdl); } return { sdl, instanceUrl, cacheFilePath: prime.filePath, outPath: resolvedOut, downloaded: !prime.cached, refreshed: forceRefresh, refreshedDueToAge, }; }