@databricks/appkit 0.72.0 → 0.74.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/CLAUDE.md +24 -0
- package/NOTICE.md +1 -0
- package/dist/appkit/package.js +1 -1
- package/dist/beta.d.ts +11 -8
- package/dist/beta.js +7 -6
- package/dist/cli/commands/agent/eval.js +85 -16
- package/dist/cli/commands/agent/eval.js.map +1 -1
- package/dist/connectors/index.js +1 -1
- package/dist/connectors/mlflow/auth.d.ts +11 -1
- package/dist/connectors/mlflow/auth.d.ts.map +1 -1
- package/dist/connectors/mlflow/auth.js +22 -2
- package/dist/connectors/mlflow/auth.js.map +1 -1
- package/dist/connectors/mlflow/index.d.ts +2 -0
- package/dist/database/errors.js +15 -5
- package/dist/database/errors.js.map +1 -1
- package/dist/database/runtime/data-path.d.ts +7 -0
- package/dist/database/runtime/data-path.d.ts.map +1 -0
- package/dist/database/runtime/data-path.js.map +1 -1
- package/dist/database/runtime/engine/drizzle-data-path.js +7 -5
- package/dist/database/runtime/engine/drizzle-data-path.js.map +1 -1
- package/dist/database/schema-builder/define-schema.d.ts +1 -1
- package/dist/database/schema-builder/define-schema.js +1 -1
- package/dist/database/schema-builder/define-schema.js.map +1 -1
- package/dist/errors/database-validation.d.ts +23 -0
- package/dist/errors/database-validation.d.ts.map +1 -0
- package/dist/errors/database-validation.js +24 -0
- package/dist/errors/database-validation.js.map +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/evals/dataset.d.ts +49 -0
- package/dist/evals/dataset.d.ts.map +1 -0
- package/dist/evals/dataset.js +51 -0
- package/dist/evals/dataset.js.map +1 -0
- package/dist/evals/define-eval.d.ts +4 -2
- package/dist/evals/define-eval.d.ts.map +1 -1
- package/dist/evals/define-eval.js +5 -1
- package/dist/evals/define-eval.js.map +1 -1
- package/dist/evals/discover.d.ts +15 -1
- package/dist/evals/discover.d.ts.map +1 -1
- package/dist/evals/discover.js +26 -2
- package/dist/evals/discover.js.map +1 -1
- package/dist/evals/http-driver.d.ts.map +1 -1
- package/dist/evals/http-driver.js +82 -55
- package/dist/evals/http-driver.js.map +1 -1
- package/dist/evals/index.d.ts +14 -0
- package/dist/evals/index.js +6 -5
- package/dist/evals/judge.d.ts +1 -0
- package/dist/evals/judge.d.ts.map +1 -1
- package/dist/evals/mlflow-report.d.ts +1 -0
- package/dist/evals/mlflow-report.d.ts.map +1 -1
- package/dist/evals/mlflow-run.d.ts +2 -0
- package/dist/evals/mlflow-run.d.ts.map +1 -1
- package/dist/evals/report.d.ts +16 -1
- package/dist/evals/report.d.ts.map +1 -1
- package/dist/evals/report.js +64 -2
- package/dist/evals/report.js.map +1 -1
- package/dist/evals/run-eval.d.ts +8 -0
- package/dist/evals/run-eval.d.ts.map +1 -1
- package/dist/evals/run-eval.js +54 -5
- package/dist/evals/run-eval.js.map +1 -1
- package/dist/evals/run-evals.d.ts +41 -3
- package/dist/evals/run-evals.d.ts.map +1 -1
- package/dist/evals/run-evals.js +215 -34
- package/dist/evals/run-evals.js.map +1 -1
- package/dist/evals/types.d.ts +80 -6
- package/dist/evals/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +1 -1
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugins/database/crud/contract.js +17 -8
- package/dist/plugins/database/crud/contract.js.map +1 -1
- package/dist/plugins/database/crud/exposure.js +63 -22
- package/dist/plugins/database/crud/exposure.js.map +1 -1
- package/dist/plugins/database/crud/request.js +50 -0
- package/dist/plugins/database/crud/request.js.map +1 -0
- package/dist/plugins/database/crud/response.js +77 -0
- package/dist/plugins/database/crud/response.js.map +1 -0
- package/dist/plugins/database/crud/routes.js +71 -52
- package/dist/plugins/database/crud/routes.js.map +1 -1
- package/dist/plugins/database/database.d.ts +6 -4
- package/dist/plugins/database/database.d.ts.map +1 -1
- package/dist/plugins/database/database.js +46 -16
- package/dist/plugins/database/database.js.map +1 -1
- package/dist/plugins/database/defaults.js +5 -1
- package/dist/plugins/database/defaults.js.map +1 -1
- package/dist/plugins/database/entity-client.js +143 -10
- package/dist/plugins/database/entity-client.js.map +1 -1
- package/dist/plugins/database/entity-types.d.ts +1 -1
- package/dist/plugins/database/hooks.d.ts +38 -0
- package/dist/plugins/database/hooks.d.ts.map +1 -0
- package/dist/plugins/database/index.d.ts +3 -2
- package/dist/plugins/database/lifecycle.js +67 -28
- package/dist/plugins/database/lifecycle.js.map +1 -1
- package/dist/plugins/database/scope.js +58 -0
- package/dist/plugins/database/scope.js.map +1 -0
- package/dist/plugins/database/types.d.ts +40 -12
- package/dist/plugins/database/types.d.ts.map +1 -1
- package/dist/plugins/server/index.js +2 -2
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js +3 -3
- package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
- package/dist/plugins/server/static-server.js +3 -3
- package/dist/plugins/server/static-server.js.map +1 -1
- package/dist/plugins/server/utils.js +3 -3
- package/dist/plugins/server/utils.js.map +1 -1
- package/dist/plugins/server/vite-dev-server.js +4 -4
- package/dist/plugins/server/vite-dev-server.js.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts +87 -87
- package/dist/type-generator/database/generate.js +3 -3
- package/dist/type-generator/database/generate.js.map +1 -1
- package/dist/type-generator/migration.js +2 -2
- package/dist/type-generator/migration.js.map +1 -1
- package/dist/type-generator/serving/server-file-extractor.js +3 -3
- package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
- package/docs/api/appkit/Class.AppKitError.md +1 -0
- package/docs/api/appkit/Class.DatabaseValidationError.md +191 -0
- package/docs/api/appkit/Function.defineEvalConfig.md +18 -0
- package/docs/api/appkit/Function.defineSchema.md +1 -1
- package/docs/api/appkit/Function.discoverEvalConfigs.md +18 -0
- package/docs/api/appkit/Function.formatResultsJUnit.md +18 -0
- package/docs/api/appkit/Function.formatResultsJson.md +18 -0
- package/docs/api/appkit/Function.readEvalDataset.md +21 -0
- package/docs/api/appkit/Function.resolveWorkspaceClient.md +18 -0
- package/docs/api/appkit/Function.runWithRetries.md +28 -0
- package/docs/api/appkit/Function.userTurns.md +20 -0
- package/docs/api/appkit/Interface.AssertionHandle.md +1 -1
- package/docs/api/appkit/Interface.DatabaseValidationIssue.md +21 -0
- package/docs/api/appkit/Interface.DatasetRow.md +21 -0
- package/docs/api/appkit/Interface.DiscoveredEvalConfig.md +25 -0
- package/docs/api/appkit/Interface.DriveResult.md +28 -0
- package/docs/api/appkit/Interface.EntityMutationHooks.md +173 -0
- package/docs/api/appkit/Interface.EvalDefinition.md +50 -0
- package/docs/api/appkit/Interface.EvalDriver.md +26 -5
- package/docs/api/appkit/Interface.EvalResult.md +11 -0
- package/docs/api/appkit/Interface.EvalSummary.md +11 -0
- package/docs/api/appkit/Interface.HookApp.md +12 -0
- package/docs/api/appkit/Interface.HookContext.md +21 -0
- package/docs/api/appkit/Interface.ReadEvalDatasetOptions.md +34 -0
- package/docs/api/appkit/Interface.ReadSerializerContext.md +21 -0
- package/docs/api/appkit/Interface.RunEvalOptions.md +22 -0
- package/docs/api/appkit/Interface.RunEvalsOptions.md +45 -1
- package/docs/api/appkit/Interface.TestContext.md +67 -8
- package/docs/api/appkit/TypeAlias.DatabaseApiConfig.md +53 -0
- package/docs/api/appkit/TypeAlias.DatabaseApiWriteOperation.md +8 -0
- package/docs/api/appkit/TypeAlias.DatabaseApiWritesConfig.md +49 -0
- package/docs/api/appkit/TypeAlias.DatabaseExports.md +3 -3
- package/docs/api/appkit/TypeAlias.EntityHooks.md +25 -0
- package/docs/api/appkit/TypeAlias.IDatabaseConfig.md +16 -5
- package/docs/api/appkit/TypeAlias.ReadSerializer.md +19 -0
- package/docs/api/appkit/TypeAlias.TransactionClient.md +19 -0
- package/docs/api/appkit.md +142 -119
- package/docs/plugins/database.md +144 -0
- package/llms.txt +24 -0
- package/package.json +2 -2
- package/sbom.cdx.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-evals.js","names":[],"sources":["../../src/evals/run-evals.ts"],"sourcesContent":["import { pathToFileURL } from \"node:url\";\n\nimport { MlflowClient } from \"../connectors/mlflow\";\nimport { type DiscoveredEval, discoverEvalFiles } from \"./discover\";\nimport { createHttpDriver } from \"./http-driver\";\nimport { configureJudge, teardownJudge } from \"./judge\";\nimport { type ReportOutcome, reportToMlflow } from \"./mlflow-report\";\nimport { createEvalRun, type FinishOutcome, finishEvalRun } from \"./mlflow-run\";\nimport { mapPool } from \"./pool\";\nimport { runEval } from \"./run-eval\";\nimport type { EvalDefinition, EvalResult } from \"./types\";\n\nexport interface RunEvalsOptions {\n /** Project root containing `server/agents/`. Defaults to `process.cwd()`. */\n rootDir?: string;\n /** Base URL of the running app to drive, e.g. `http://localhost:3000`. */\n baseUrl: string;\n /** Substring filter on `<agent>/<id>` (or an exact agent id). */\n filter?: string;\n /** Soft assertion failures also fail the eval. */\n strict?: boolean;\n /** Extra request headers for the driver (e.g. auth for a deployed app). */\n headers?: Record<string, string>;\n /** Per-turn wall-clock timeout (ms) before a turn is failed. Defaults to 120s. */\n timeoutMs?: number;\n /**\n * Max evals to drive concurrently. Each eval opens one stream to the app as\n * the same user, so keep this at or below the app's\n * `maxConcurrentStreamsPerUser` (default 5) or the surplus streams hit the\n * 429 guard. Defaults to 4; clamped to `[1, total]`.\n */\n concurrency?: number;\n /**\n * When set, create a native MLflow \"Evaluation run\": each eval's trace is\n * linked to the run, pass/fail is written as feedback, and aggregate metrics\n * are logged. Requires Databricks creds + the target experiment.\n */\n mlflow?: {\n host: string;\n token: string;\n experimentId: string;\n /** SQL warehouse id for writing assessments to UC-backed (V4) traces. */\n sqlWarehouseId?: string;\n };\n /**\n * When set, enable `t.judge.*` LLM-as-judge scoring via autoevals against a\n * Databricks serving endpoint (`model`).\n */\n judge?: { host: string; token: string; model: string };\n /** Wall-clock timestamp (ms) for run create/finish — pass `Date.now()`. */\n now?: number;\n /** Progress callback, invoked as evals are discovered, started, and finished. */\n onEvent?: (event: EvalProgress) => void;\n}\n\nexport type EvalProgress =\n | { type: \"discovered\"; total: number }\n | { type: \"run-created\"; runId: string }\n | { type: \"start\"; id: string; index: number; total: number }\n | { type: \"result\"; result: EvalResult; index: number; total: number };\n\nexport interface EvalRunSummary {\n results: EvalResult[];\n /** Present when an MLflow evaluation run was created. */\n mlflow?: { runId: string; report: ReportOutcome; finish: FinishOutcome };\n}\n\n/**\n * Load a `*.eval.ts` file and return its default-exported {@link EvalDefinition}.\n * Uses tsx's programmatic loader so TypeScript eval files run without a build\n * step. The specifier is indirected so the type checker doesn't try to resolve\n * tsx's internal entry.\n */\nasync function loadEval(file: string): Promise<EvalDefinition> {\n const tsxApi = \"tsx/esm/api\";\n let tsImport: (specifier: string, parentURL: string) => Promise<unknown>;\n try {\n ({ tsImport } = (await import(tsxApi)) as {\n tsImport: (specifier: string, parentURL: string) => Promise<unknown>;\n });\n } catch {\n throw new Error(\n \"Running .eval.ts files requires `tsx`. Install it as a dev dependency (`pnpm add -D tsx`).\",\n );\n }\n\n const mod = await tsImport(pathToFileURL(file).href, import.meta.url);\n const def = resolveEvalDefault(mod);\n if (!def) {\n throw new Error(`${file}: must default-export defineEval({ test })`);\n }\n return def;\n}\n\n/**\n * Unwrap the eval default export across module-interop shapes. Depending on\n * whether the eval file is treated as ESM or CJS, the value lands at\n * `mod.default` (ESM), `mod.default.default` (CJS `__esModule` double-wrap), or\n * `mod` itself. Returns the first candidate that looks like an eval.\n */\nexport function resolveEvalDefault(mod: unknown): EvalDefinition | undefined {\n let candidate: unknown = mod;\n for (let i = 0; i < 4 && candidate; i++) {\n if (typeof (candidate as EvalDefinition).test === \"function\") {\n return candidate as EvalDefinition;\n }\n candidate = (candidate as { default?: unknown }).default;\n }\n return undefined;\n}\n\n/**\n * Load and run a single discovered eval. Never throws — a load/run failure\n * becomes a non-passing {@link EvalResult} so one bad eval can't abort the run.\n */\nasync function runOne(\n d: DiscoveredEval,\n id: string,\n runId: string | undefined,\n options: RunEvalsOptions,\n): Promise<EvalResult> {\n try {\n const def = await loadEval(d.file);\n const driver = createHttpDriver({\n baseUrl: options.baseUrl,\n agent: def.agent ?? d.agent,\n headers: options.headers,\n mlflowRunId: runId,\n timeoutMs: options.timeoutMs,\n });\n return await runEval(def, { id, driver, strict: options.strict });\n } catch (err) {\n return {\n id,\n assertions: [],\n passed: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\n/** Configure the LLM judge when judge creds were supplied; otherwise a no-op. */\nasync function maybeConfigureJudge(options: RunEvalsOptions): Promise<void> {\n if (!options.judge) return;\n await configureJudge({\n client: new MlflowClient(options.judge.host, options.judge.token),\n token: options.judge.token,\n model: options.judge.model,\n });\n}\n\n/**\n * Report per-eval assessments and finish the MLflow run, when one was created.\n * Returns the run summary, or `undefined` when there was no run to finalize.\n */\nasync function finalizeMlflow(\n client: MlflowClient | undefined,\n runId: string | undefined,\n results: EvalResult[],\n options: RunEvalsOptions,\n): Promise<EvalRunSummary[\"mlflow\"]> {\n if (!client || !runId) return undefined;\n // reportToMlflow is not supposed to throw, but if it ever does the run must\n // still be finished — otherwise it hangs in RUNNING forever.\n let report: ReportOutcome = { written: 0, skipped: 0, failures: [] };\n try {\n report = await reportToMlflow(\n client,\n results,\n options.mlflow?.sqlWarehouseId,\n );\n } catch (err) {\n report.failures.push({\n traceId: \"(report)\",\n error: err instanceof Error ? err.message : String(err),\n });\n }\n const finish = await finishEvalRun(client, {\n runId,\n results,\n endTime: options.now ?? Date.now(),\n });\n return { runId, report, finish };\n}\n\n/**\n * Default max evals in flight. Each eval opens one stream to the app as the\n * same user; the server caps concurrent streams per user at 5 by default\n * (`maxConcurrentStreamsPerUser`), so 4 leaves headroom under that limit.\n */\nconst DEFAULT_CONCURRENCY = 4;\n\n/**\n * Discover, load, and run every eval under each agent's `evals/` dir, driving\n * the agents on a running app. Never throws for an individual eval — load/run\n * failures become non-passing {@link EvalResult}s.\n */\nexport async function runEvalsInDir(\n options: RunEvalsOptions,\n): Promise<EvalRunSummary> {\n const root = options.rootDir ?? process.cwd();\n const now = options.now ?? Date.now();\n let discovered = discoverEvalFiles(root);\n\n if (options.filter) {\n const f = options.filter;\n discovered = discovered.filter(\n (d) => d.agent === f || `${d.agent}/${d.id}`.includes(f),\n );\n }\n\n const emit = options.onEvent ?? (() => {});\n const total = discovered.length;\n emit({ type: \"discovered\", total });\n\n // The judge sets OPENAI_* env vars globally (autoevals reads them per call),\n // so tear them down in `finally` once the run is over — pass or throw — so\n // the bearer doesn't linger in process.env.\n await maybeConfigureJudge(options);\n try {\n // Create the MLflow evaluation run up front so each eval's trace can be\n // linked to it as it runs. One client is shared by run create/finish and\n // the per-trace assessment writes.\n let runId: string | undefined;\n let mlflowClient: MlflowClient | undefined;\n if (options.mlflow) {\n mlflowClient = new MlflowClient(\n options.mlflow.host,\n options.mlflow.token,\n );\n runId = await createEvalRun(mlflowClient, {\n experimentId: options.mlflow.experimentId,\n runName: `appkit-eval ${new Date(now).toISOString()}`,\n startTime: now,\n });\n emit({ type: \"run-created\", runId });\n }\n\n // Run evals through a bounded pool so independent turns overlap instead of\n // summing their latencies. runOne never throws, so a pool worker never\n // rejects; results preserve discovery order (mapPool writes by index).\n const results = await mapPool(\n discovered,\n options.concurrency ?? DEFAULT_CONCURRENCY,\n async (d, index) => {\n const id = `${d.agent}/${d.id}`;\n emit({ type: \"start\", id, index, total });\n const result = await runOne(d, id, runId, options);\n emit({ type: \"result\", result, index, total });\n return result;\n },\n );\n\n const summary: EvalRunSummary = { results };\n const mlflow = await finalizeMlflow(mlflowClient, runId, results, options);\n if (mlflow) summary.mlflow = mlflow;\n return summary;\n } finally {\n teardownJudge();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAyEA,eAAe,SAAS,MAAuC;CAC7D,MAAM,SAAS;CACf,IAAI;AACJ,KAAI;AACF,GAAC,CAAE,YAAc,MAAM,OAAO;SAGxB;AACN,QAAM,IAAI,MACR,6FACD;;CAIH,MAAM,MAAM,mBADA,MAAM,SAAS,cAAc,KAAK,CAAC,MAAM,OAAO,KAAK,IAAI,CAClC;AACnC,KAAI,CAAC,IACH,OAAM,IAAI,MAAM,GAAG,KAAK,4CAA4C;AAEtE,QAAO;;;;;;;;AAST,SAAgB,mBAAmB,KAA0C;CAC3E,IAAI,YAAqB;AACzB,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,WAAW,KAAK;AACvC,MAAI,OAAQ,UAA6B,SAAS,WAChD,QAAO;AAET,cAAa,UAAoC;;;;;;;AASrD,eAAe,OACb,GACA,IACA,OACA,SACqB;AACrB,KAAI;EACF,MAAM,MAAM,MAAM,SAAS,EAAE,KAAK;AAQlC,SAAO,MAAM,QAAQ,KAAK;GAAE;GAAI,QAPjB,iBAAiB;IAC9B,SAAS,QAAQ;IACjB,OAAO,IAAI,SAAS,EAAE;IACtB,SAAS,QAAQ;IACjB,aAAa;IACb,WAAW,QAAQ;IACpB,CAAC;GACsC,QAAQ,QAAQ;GAAQ,CAAC;UAC1D,KAAK;AACZ,SAAO;GACL;GACA,YAAY,EAAE;GACd,QAAQ;GACR,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;GACxD;;;;AAKL,eAAe,oBAAoB,SAAyC;AAC1E,KAAI,CAAC,QAAQ,MAAO;AACpB,OAAM,eAAe;EACnB,QAAQ,IAAI,aAAa,QAAQ,MAAM,MAAM,QAAQ,MAAM,MAAM;EACjE,OAAO,QAAQ,MAAM;EACrB,OAAO,QAAQ,MAAM;EACtB,CAAC;;;;;;AAOJ,eAAe,eACb,QACA,OACA,SACA,SACmC;AACnC,KAAI,CAAC,UAAU,CAAC,MAAO,QAAO;CAG9B,IAAI,SAAwB;EAAE,SAAS;EAAG,SAAS;EAAG,UAAU,EAAE;EAAE;AACpE,KAAI;AACF,WAAS,MAAM,eACb,QACA,SACA,QAAQ,QAAQ,eACjB;UACM,KAAK;AACZ,SAAO,SAAS,KAAK;GACnB,SAAS;GACT,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;GACxD,CAAC;;CAEJ,MAAM,SAAS,MAAM,cAAc,QAAQ;EACzC;EACA;EACA,SAAS,QAAQ,OAAO,KAAK,KAAK;EACnC,CAAC;AACF,QAAO;EAAE;EAAO;EAAQ;EAAQ;;;;;;;AAQlC,MAAM,sBAAsB;;;;;;AAO5B,eAAsB,cACpB,SACyB;CACzB,MAAM,OAAO,QAAQ,WAAW,QAAQ,KAAK;CAC7C,MAAM,MAAM,QAAQ,OAAO,KAAK,KAAK;CACrC,IAAI,aAAa,kBAAkB,KAAK;AAExC,KAAI,QAAQ,QAAQ;EAClB,MAAM,IAAI,QAAQ;AAClB,eAAa,WAAW,QACrB,MAAM,EAAE,UAAU,KAAK,GAAG,EAAE,MAAM,GAAG,EAAE,KAAK,SAAS,EAAE,CACzD;;CAGH,MAAM,OAAO,QAAQ,kBAAkB;CACvC,MAAM,QAAQ,WAAW;AACzB,MAAK;EAAE,MAAM;EAAc;EAAO,CAAC;AAKnC,OAAM,oBAAoB,QAAQ;AAClC,KAAI;EAIF,IAAI;EACJ,IAAI;AACJ,MAAI,QAAQ,QAAQ;AAClB,kBAAe,IAAI,aACjB,QAAQ,OAAO,MACf,QAAQ,OAAO,MAChB;AACD,WAAQ,MAAM,cAAc,cAAc;IACxC,cAAc,QAAQ,OAAO;IAC7B,SAAS,eAAe,IAAI,KAAK,IAAI,CAAC,aAAa;IACnD,WAAW;IACZ,CAAC;AACF,QAAK;IAAE,MAAM;IAAe;IAAO,CAAC;;EAMtC,MAAM,UAAU,MAAM,QACpB,YACA,QAAQ,eAAe,qBACvB,OAAO,GAAG,UAAU;GAClB,MAAM,KAAK,GAAG,EAAE,MAAM,GAAG,EAAE;AAC3B,QAAK;IAAE,MAAM;IAAS;IAAI;IAAO;IAAO,CAAC;GACzC,MAAM,SAAS,MAAM,OAAO,GAAG,IAAI,OAAO,QAAQ;AAClD,QAAK;IAAE,MAAM;IAAU;IAAQ;IAAO;IAAO,CAAC;AAC9C,UAAO;IAEV;EAED,MAAM,UAA0B,EAAE,SAAS;EAC3C,MAAM,SAAS,MAAM,eAAe,cAAc,OAAO,SAAS,QAAQ;AAC1E,MAAI,OAAQ,SAAQ,SAAS;AAC7B,SAAO;WACC;AACR,iBAAe"}
|
|
1
|
+
{"version":3,"file":"run-evals.js","names":["sleep"],"sources":["../../src/evals/run-evals.ts"],"sourcesContent":["import { setTimeout as sleep } from \"node:timers/promises\";\nimport { pathToFileURL } from \"node:url\";\n\nimport { MlflowClient } from \"../connectors/mlflow\";\nimport type { WorkspaceClient } from \"../workspace-client\";\nimport { type DatasetRow, readEvalDataset } from \"./dataset\";\nimport {\n type DiscoveredEval,\n discoverEvalConfigs,\n discoverEvalFiles,\n} from \"./discover\";\nimport { createHttpDriver } from \"./http-driver\";\nimport { configureJudge, teardownJudge } from \"./judge\";\nimport { type ReportOutcome, reportToMlflow } from \"./mlflow-report\";\nimport { createEvalRun, type FinishOutcome, finishEvalRun } from \"./mlflow-run\";\nimport { mapPool } from \"./pool\";\nimport { runEval } from \"./run-eval\";\nimport type { EvalConfig, EvalDefinition, EvalResult } from \"./types\";\n\nexport interface RunEvalsOptions {\n /** Project root containing `server/agents/`. Defaults to `process.cwd()`. */\n rootDir?: string;\n /** Base URL of the running app to drive, e.g. `http://localhost:3000`. */\n baseUrl: string;\n /** Substring filter on `<agent>/<id>` (or an exact agent id). */\n filter?: string;\n /**\n * Only run evals whose `tags` intersect this list. Empty/undefined runs all.\n * Tags live on the eval def, so filtering happens after each file is loaded.\n */\n tags?: string[];\n /** Soft assertion failures also fail the eval. */\n strict?: boolean;\n /** Extra request headers for the driver (e.g. auth for a deployed app). */\n headers?: Record<string, string>;\n /**\n * Max evals to drive concurrently. Each eval opens one stream to the app as\n * the same user, so keep this at or below the app's\n * `maxConcurrentStreamsPerUser` (default 5) or the surplus streams hit the\n * 429 guard. Defaults to 4; clamped to `[1, total]`.\n */\n concurrency?: number;\n /**\n * When set, create a native MLflow \"Evaluation run\": each eval's trace is\n * linked to the run, pass/fail is written as feedback, and aggregate metrics\n * are logged. Requires Databricks creds + the target experiment.\n */\n mlflow?: {\n host: string;\n token: string;\n experimentId: string;\n /** SQL warehouse id for writing assessments to UC-backed (V4) traces. */\n sqlWarehouseId?: string;\n };\n /**\n * When set, enable `t.judge.*` LLM-as-judge scoring via autoevals against a\n * Databricks serving endpoint (`model`).\n */\n judge?: { host: string; token: string; model: string };\n /**\n * Workspace client used to read managed evaluation datasets (for evals that\n * declare `dataset`). Required alongside {@link warehouseId} for those evals.\n */\n workspaceClient?: WorkspaceClient;\n /** SQL warehouse id used to read managed evaluation datasets. */\n warehouseId?: string;\n /** Wall-clock timestamp (ms) for run create/finish — pass `Date.now()`. */\n now?: number;\n /**\n * Default per-eval timeout (ms): `runEval` races the whole test against it and\n * it also caps each driver turn. A per-eval `def.timeoutMs` overrides it, and\n * it wins over an agent's `evals.config.ts` `timeoutMs`. Unbounded when unset.\n */\n timeoutMs?: number;\n /**\n * Re-run an eval up to this many extra times when it fails on infrastructure —\n * a thrown error/timeout (`result.error`) or a transport/agent turn failure\n * (`result.infraFailure`). Assertion failures are never retried. Defaults to `0`.\n */\n retries?: number;\n /** Progress callback, invoked as evals are discovered, started, and finished. */\n onEvent?: (event: EvalProgress) => void;\n}\n\nexport type EvalProgress =\n | { type: \"discovered\"; total: number }\n | { type: \"run-created\"; runId: string }\n | { type: \"start\"; id: string; index: number; total: number }\n | { type: \"result\"; result: EvalResult; index: number; total: number };\n\nexport interface EvalRunSummary {\n results: EvalResult[];\n /** Present when an MLflow evaluation run was created. */\n mlflow?: { runId: string; report: ReportOutcome; finish: FinishOutcome };\n}\n\n/**\n * Import a TypeScript file with tsx's programmatic loader so eval files run\n * without a build step. The specifier is indirected so the type checker doesn't\n * try to resolve tsx's internal entry.\n */\nasync function tsImportFile(file: string): Promise<unknown> {\n const tsxApi = \"tsx/esm/api\";\n let tsImport: (specifier: string, parentURL: string) => Promise<unknown>;\n try {\n ({ tsImport } = (await import(tsxApi)) as {\n tsImport: (specifier: string, parentURL: string) => Promise<unknown>;\n });\n } catch {\n throw new Error(\n \"Running .eval.ts files requires `tsx`. Install it as a dev dependency (`pnpm add -D tsx`).\",\n );\n }\n return tsImport(pathToFileURL(file).href, import.meta.url);\n}\n\n/**\n * Load a `*.eval.ts` file and return its default-exported {@link EvalDefinition}.\n */\nasync function loadEval(file: string): Promise<EvalDefinition> {\n const mod = await tsImportFile(file);\n const def = resolveEvalDefault(mod);\n if (!def) {\n throw new Error(`${file}: must default-export defineEval({ test })`);\n }\n return def;\n}\n\n/**\n * Load an `evals.config.ts` file and return its default-exported\n * {@link EvalConfig}. A malformed/missing default surfaces as `undefined` so a\n * bad config never aborts a whole run.\n */\nasync function loadEvalConfig(file: string): Promise<EvalConfig | undefined> {\n const mod = await tsImportFile(file);\n return resolveConfigDefault(mod);\n}\n\n/**\n * Unwrap the config default export across module-interop shapes (see\n * {@link resolveEvalDefault}). A config has no `.test`, so the first plain\n * object reached through the `default` chain is taken as the config.\n */\nexport function resolveConfigDefault(mod: unknown): EvalConfig | undefined {\n let candidate: unknown = mod;\n // `i < 4` bounds the chain; no visited-set needed (cf. resolveEvalDefault).\n for (let i = 0; i < 4 && candidate; i++) {\n const next = (candidate as { default?: unknown }).default;\n if (next === undefined) {\n return typeof candidate === \"object\"\n ? (candidate as EvalConfig)\n : undefined;\n }\n candidate = next;\n }\n return undefined;\n}\n\n/**\n * Unwrap the eval default export across module-interop shapes. Depending on\n * whether the eval file is treated as ESM or CJS, the value lands at\n * `mod.default` (ESM), `mod.default.default` (CJS `__esModule` double-wrap), or\n * `mod` itself. Returns the first candidate that looks like an eval.\n */\nexport function resolveEvalDefault(mod: unknown): EvalDefinition | undefined {\n let candidate: unknown = mod;\n for (let i = 0; i < 4 && candidate; i++) {\n if (typeof (candidate as EvalDefinition).test === \"function\") {\n return candidate as EvalDefinition;\n }\n candidate = (candidate as { default?: unknown }).default;\n }\n return undefined;\n}\n\n/**\n * Run one eval turn against a fresh driver. Never throws — a run failure becomes\n * a non-passing {@link EvalResult} so one bad eval can't abort the run. `row`\n * binds the current managed-dataset row (see {@link resolveDatasetRows}), or is\n * `undefined` for a plain single-run eval.\n */\nasync function runOne(\n d: DiscoveredEval,\n id: string,\n def: EvalDefinition,\n row: DatasetRow | undefined,\n runId: string | undefined,\n options: RunEvalsOptions,\n): Promise<EvalResult> {\n try {\n // Each attempt builds a fresh driver, so a retry never inherits the failed\n // attempt's thread. (runWithRetries defines what counts as retryable.)\n return await runWithRetries(options.retries ?? 0, () =>\n runEval(def, {\n id,\n driver: createHttpDriver({\n baseUrl: options.baseUrl,\n agent: def.agent ?? d.agent,\n headers: options.headers,\n mlflowRunId: runId,\n // Cap the driver turn at the eval's effective timeout (runEval's signal also aborts it).\n timeoutMs: def.timeoutMs ?? options.timeoutMs,\n }),\n strict: options.strict,\n row,\n timeoutMs: options.timeoutMs,\n }),\n );\n } catch (err) {\n return {\n id,\n assertions: [],\n passed: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\n/**\n * Resolve the rows a (possibly dataset-driven) eval runs over. A plain eval\n * yields a single `undefined` row; a dataset eval reads its Unity Catalog table\n * via {@link readEvalDataset}. On misconfiguration or read failure, returns a\n * single `undefined` row plus an `error`, so the eval still surfaces one result.\n */\nexport async function resolveDatasetRows(\n def: EvalDefinition,\n options: RunEvalsOptions,\n): Promise<{ rows: Array<DatasetRow | undefined>; error?: string }> {\n if (!def.dataset) return { rows: [undefined] };\n if (!options.workspaceClient || !options.warehouseId) {\n return {\n rows: [undefined],\n error:\n \"dataset eval requires a workspace client and warehouse (pass --warehouse-id)\",\n };\n }\n try {\n const rows = await readEvalDataset(options.workspaceClient, {\n table: def.dataset.table,\n warehouseId: options.warehouseId,\n limit: def.dataset.limit,\n });\n if (rows.length === 0) {\n return {\n rows: [undefined],\n error: `dataset \"${def.dataset.table}\" returned no rows`,\n };\n }\n return { rows };\n } catch (err) {\n return {\n rows: [undefined],\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\n/**\n * Run one already-loaded eval (from the `loaded` pre-pass), expanding a\n * dataset-driven eval into one run per row. Appends one result per row to\n * `results`, emitting `start`/`result` around each. Never throws: a load error\n * (carried in `loadError`) or a dataset-read failure surfaces as a non-passing\n * result. `total` counts eval files, not rows — per-row detail is carried in the\n * result id (`[row i/n]`).\n */\nasync function runDiscovered(\n d: DiscoveredEval,\n def: EvalDefinition,\n loadError: string | undefined,\n index: number,\n total: number,\n runId: string | undefined,\n options: RunEvalsOptions,\n emit: (event: EvalProgress) => void,\n results: EvalResult[],\n): Promise<void> {\n const id = `${d.agent}/${d.id}`;\n\n // Load failed in the pre-pass (def is a placeholder) → one non-passing result.\n if (loadError) {\n emit({ type: \"start\", id, index, total });\n const result: EvalResult = {\n id,\n assertions: [],\n passed: false,\n error: loadError,\n };\n results.push(result);\n emit({ type: \"result\", result, index, total });\n return;\n }\n\n const { rows, error: datasetError } = await resolveDatasetRows(def, options);\n\n for (let r = 0; r < rows.length; r++) {\n const rowId =\n def.dataset && rows.length > 1\n ? `${id} [row ${r + 1}/${rows.length}]`\n : id;\n emit({ type: \"start\", id: rowId, index, total });\n const result: EvalResult = datasetError\n ? { id: rowId, assertions: [], passed: false, error: datasetError }\n : await runOne(d, rowId, def, rows[r], runId, options);\n results.push(result);\n emit({ type: \"result\", result, index, total });\n }\n}\n\n/** Base delay (ms) before the first retry; doubled per attempt, full-jittered, capped. */\nconst DEFAULT_RETRY_BASE_DELAY_MS = 250;\n/** Ceiling for a single retry backoff wait (ms). */\nconst MAX_RETRY_DELAY_MS = 5_000;\n\n/**\n * Run `attempt` up to `1 + retries` times, stopping as soon as it returns a\n * result that is neither a thrown error / per-eval timeout (`error`) nor a\n * transport/agent turn failure (`infraFailure`). Assertion failures set\n * neither, so a failed-but-completed eval is returned on the first try and\n * never retried. Returns the last result when every attempt failed on infra.\n *\n * Between attempts it waits a full-jittered exponential backoff (infra flakes\n * are overload-correlated). `retries` is coerced to a finite non-negative\n * integer; `baseDelayMs: 0` disables the wait (tests).\n */\nexport async function runWithRetries(\n retries: number,\n attempt: (attemptNumber: number) => Promise<EvalResult>,\n options: { baseDelayMs?: number } = {},\n): Promise<EvalResult> {\n const baseDelayMs = options.baseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;\n const maxRetries = Number.isFinite(retries)\n ? Math.max(0, Math.floor(retries))\n : 0;\n const maxAttempts = 1 + maxRetries;\n let result: EvalResult;\n for (let n = 1; ; n++) {\n result = await attempt(n);\n const infraFailed = result.error !== undefined || result.infraFailure;\n if (!infraFailed || n >= maxAttempts) return result;\n if (baseDelayMs > 0) {\n // Full jitter: a random wait in [0, min(cap, base * 2^(n-1))].\n const ceiling = Math.min(baseDelayMs * 2 ** (n - 1), MAX_RETRY_DELAY_MS);\n await sleep(Math.random() * ceiling);\n }\n }\n}\n\n/**\n * Whether an eval's `tags` satisfy a `--tag` filter: `true` when the filter is\n * empty/undefined (no filtering), otherwise only when the eval shares at least\n * one tag with it. An eval with no tags never matches a non-empty filter.\n */\nexport function matchesTags(\n defTags: string[] | undefined,\n filterTags: string[] | undefined,\n): boolean {\n if (!filterTags || filterTags.length === 0) return true;\n return defTags?.some((t) => filterTags.includes(t)) ?? false;\n}\n\n/** Configure the LLM judge when judge creds were supplied; otherwise a no-op. */\nasync function maybeConfigureJudge(options: RunEvalsOptions): Promise<void> {\n if (!options.judge) return;\n await configureJudge({\n client: new MlflowClient(options.judge.host, options.judge.token),\n token: options.judge.token,\n model: options.judge.model,\n });\n}\n\n/**\n * Report per-eval assessments and finish the MLflow run, when one was created.\n * Returns the run summary, or `undefined` when there was no run to finalize.\n */\nasync function finalizeMlflow(\n client: MlflowClient | undefined,\n runId: string | undefined,\n results: EvalResult[],\n options: RunEvalsOptions,\n): Promise<EvalRunSummary[\"mlflow\"]> {\n if (!client || !runId) return undefined;\n // reportToMlflow is not supposed to throw, but if it ever does the run must\n // still be finished — otherwise it hangs in RUNNING forever.\n let report: ReportOutcome = { written: 0, skipped: 0, failures: [] };\n try {\n report = await reportToMlflow(\n client,\n results,\n options.mlflow?.sqlWarehouseId,\n );\n } catch (err) {\n report.failures.push({\n traceId: \"(report)\",\n error: err instanceof Error ? err.message : String(err),\n });\n }\n const finish = await finishEvalRun(client, {\n runId,\n results,\n endTime: options.now ?? Date.now(),\n });\n return { runId, report, finish };\n}\n\n/**\n * Default max evals in flight. Each eval opens one stream to the app as the\n * same user; the server caps concurrent streams per user at 5 by default\n * (`maxConcurrentStreamsPerUser`), so 4 leaves headroom under that limit.\n */\nconst DEFAULT_CONCURRENCY = 4;\n\n/**\n * Resolve the work-pool width: `--concurrency` wins; else the lowest\n * `maxConcurrency` any *participating* agent's `evals.config.ts` requests (all\n * evals share one per-user stream budget, so the most conservative ceiling\n * governs); else {@link DEFAULT_CONCURRENCY}.\n */\nexport function deriveConcurrency(\n activeAgents: Set<string>,\n configs: Map<string, EvalConfig>,\n cliConcurrency: number | undefined,\n): number {\n const configMin = [...configs.entries()]\n .filter(([agent]) => activeAgents.has(agent))\n .map(([, c]) => c.maxConcurrency)\n .filter((n): n is number => typeof n === \"number\")\n .reduce<number | undefined>(\n (min, n) => (min === undefined ? n : Math.min(min, n)),\n undefined,\n );\n return cliConcurrency ?? configMin ?? DEFAULT_CONCURRENCY;\n}\n\n/**\n * Discover, load, and run every eval under each agent's `evals/` dir, driving\n * the agents on a running app. Never throws for an individual eval — load/run\n * failures become non-passing {@link EvalResult}s.\n */\nexport async function runEvalsInDir(\n options: RunEvalsOptions,\n): Promise<EvalRunSummary> {\n const root = options.rootDir ?? process.cwd();\n const now = options.now ?? Date.now();\n let discovered = discoverEvalFiles(root);\n\n if (options.filter) {\n const f = options.filter;\n discovered = discovered.filter(\n (d) => d.agent === f || `${d.agent}/${d.id}`.includes(f),\n );\n }\n\n const emit = options.onEvent ?? (() => {});\n\n // Load each agent's `evals.config.ts` (best-effort, per-agent): its settings\n // apply only to that agent's evals. A malformed/missing config never aborts\n // the run — the agent just falls back to CLI options and built-in defaults.\n const configs = new Map<string, EvalConfig>();\n for (const c of discoverEvalConfigs(root)) {\n try {\n const cfg = await loadEvalConfig(c.file);\n if (cfg) configs.set(c.agent, cfg);\n } catch {\n // Ignore: fall back to CLI options / defaults for this agent.\n }\n }\n\n // Load each eval def and apply the `--tag` filter up front. Tags live on the\n // def, so a tag miss removes the eval entirely (like the substring filter\n // excludes files) rather than surfacing as a result. Load failures are kept\n // so a broken file still reports as a non-passing result.\n const loaded: Array<{\n d: DiscoveredEval;\n def: EvalDefinition;\n loadError?: string;\n }> = [];\n for (const d of discovered) {\n let def: EvalDefinition;\n try {\n def = await loadEval(d.file);\n } catch (err) {\n loaded.push({\n d,\n // No def loaded; placeholder def is never run (error short-circuits).\n def: { test: () => {} },\n loadError: err instanceof Error ? err.message : String(err),\n });\n continue;\n }\n if (!matchesTags(def.tags, options.tags)) continue;\n loaded.push({ d, def });\n }\n\n // Pool width from participating agents' configs (see {@link deriveConcurrency}).\n const activeAgents = new Set(loaded.map((l) => l.d.agent));\n const concurrency = deriveConcurrency(\n activeAgents,\n configs,\n options.concurrency,\n );\n\n const total = loaded.length;\n emit({ type: \"discovered\", total });\n\n // The judge sets OPENAI_* env vars globally (autoevals reads them per call),\n // so tear them down in `finally` once the run is over — pass or throw — so\n // the bearer doesn't linger in process.env.\n await maybeConfigureJudge(options);\n try {\n // Create the MLflow evaluation run up front so each eval's trace can be\n // linked to it as it runs. One client is shared by run create/finish and\n // the per-trace assessment writes.\n let runId: string | undefined;\n let mlflowClient: MlflowClient | undefined;\n if (options.mlflow) {\n mlflowClient = new MlflowClient(\n options.mlflow.host,\n options.mlflow.token,\n );\n runId = await createEvalRun(mlflowClient, {\n experimentId: options.mlflow.experimentId,\n runName: `appkit-eval ${new Date(now).toISOString()}`,\n startTime: now,\n });\n emit({ type: \"run-created\", runId });\n }\n\n // Run each loaded (tag-filtered) eval through the bounded pool — one in-flight\n // stream per eval, so the pool respects the server's per-user stream cap (see\n // mapPool/concurrency). A dataset eval expands into per-row runs that execute\n // serially within its slot; results preserve discovery order (mapPool writes\n // by index) and row order within each file. Per-agent timeout is folded into\n // the file's options (CLI wins over `evals.config.ts`; `def.timeoutMs` still\n // overrides, applied inside runEval). `total` counts eval files, not dataset\n // rows — per-row detail is carried in the result id (`[row i/n]`).\n const perFile = await mapPool(\n loaded,\n concurrency,\n async ({ d, def, loadError }, index) => {\n const fileResults: EvalResult[] = [];\n const fileOptions: RunEvalsOptions = {\n ...options,\n timeoutMs: options.timeoutMs ?? configs.get(d.agent)?.timeoutMs,\n };\n await runDiscovered(\n d,\n def,\n loadError,\n index,\n total,\n runId,\n fileOptions,\n emit,\n fileResults,\n );\n return fileResults;\n },\n );\n const results = perFile.flat();\n\n const summary: EvalRunSummary = { results };\n const mlflow = await finalizeMlflow(mlflowClient, runId, results, options);\n if (mlflow) summary.mlflow = mlflow;\n return summary;\n } finally {\n teardownJudge();\n }\n}\n"],"mappings":"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"}
|
package/dist/evals/types.d.ts
CHANGED
|
@@ -38,7 +38,11 @@ interface AssertionHandle {
|
|
|
38
38
|
gate(): AssertionHandle;
|
|
39
39
|
/** Demote to a tracked metric — doesn't fail unless running with `strict`. */
|
|
40
40
|
soft(): AssertionHandle;
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Set the pass threshold for a scored assertion: it passes only when the
|
|
43
|
+
* score is at least `threshold`. Keeps the current severity (gate unless also
|
|
44
|
+
* chained with `.soft()`).
|
|
45
|
+
*/
|
|
42
46
|
atLeast(threshold: number): AssertionHandle;
|
|
43
47
|
}
|
|
44
48
|
/** What a driver returns for a single `t.send`. */
|
|
@@ -47,6 +51,11 @@ interface DriveResult {
|
|
|
47
51
|
reply: string;
|
|
48
52
|
/** Names of tools the agent called during the turn. */
|
|
49
53
|
toolCalls: string[];
|
|
54
|
+
/** Tool calls with their parsed arguments, in call order. */
|
|
55
|
+
toolCallDetails: Array<{
|
|
56
|
+
name: string;
|
|
57
|
+
args: Record<string, unknown>;
|
|
58
|
+
}>;
|
|
50
59
|
/** Whether the turn completed without an agent/stream error. */
|
|
51
60
|
succeeded: boolean;
|
|
52
61
|
/** Thread/session id, when the driver exposes one. */
|
|
@@ -59,29 +68,65 @@ interface DriveResult {
|
|
|
59
68
|
* app's agents endpoint; future drivers (in-process) implement the same shape.
|
|
60
69
|
*/
|
|
61
70
|
interface EvalDriver {
|
|
62
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Drive one turn. `options.signal`, when provided, aborts the in-flight turn:
|
|
73
|
+
* the runner passes its per-eval timeout signal so a timed-out eval cancels
|
|
74
|
+
* the request instead of leaking a live stream.
|
|
75
|
+
*/
|
|
76
|
+
send(message: string, options?: {
|
|
77
|
+
signal?: AbortSignal;
|
|
78
|
+
}): Promise<DriveResult>;
|
|
79
|
+
/**
|
|
80
|
+
* Drop the current conversation so the next `send` starts a fresh thread.
|
|
81
|
+
* Optional: drivers without a session concept omit it.
|
|
82
|
+
*/
|
|
83
|
+
reset?(): void;
|
|
63
84
|
}
|
|
64
85
|
/** The `t` context passed to an eval's `test` function. */
|
|
65
86
|
interface TestContext {
|
|
66
87
|
/** Send a user message to the agent and capture its response. */
|
|
67
88
|
send(message: string): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Start a fresh conversation: the next `send` opens a new thread with no
|
|
91
|
+
* history. Use to run several independent one-shot checks in one test.
|
|
92
|
+
* Consecutive `send`s (without a `reset`) stay in one multi-turn conversation.
|
|
93
|
+
*/
|
|
94
|
+
reset(): void;
|
|
68
95
|
/** The last assistant reply. */
|
|
69
96
|
readonly reply: string;
|
|
70
97
|
/** Tools called during the last turn. */
|
|
71
98
|
readonly toolCalls: string[];
|
|
72
99
|
/** The current session/thread id, if any. */
|
|
73
100
|
readonly sessionId: string | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* The current dataset row's `inputs` when the eval is dataset-driven (see
|
|
103
|
+
* {@link EvalDefinition.dataset}); `{}` for a plain single-run eval.
|
|
104
|
+
*/
|
|
105
|
+
readonly input: Record<string, unknown>;
|
|
106
|
+
/**
|
|
107
|
+
* The current dataset row's `expectations` (ground truth / guidelines), or
|
|
108
|
+
* `undefined` when the row has none or the eval isn't dataset-driven.
|
|
109
|
+
*/
|
|
110
|
+
readonly expected: Record<string, unknown> | undefined;
|
|
74
111
|
/** Assert the last turn completed successfully (gate by default). */
|
|
75
112
|
succeeded(): AssertionHandle;
|
|
76
113
|
/** Assert a tool was called during the run (gate by default). */
|
|
77
114
|
calledTool(name: string): AssertionHandle;
|
|
115
|
+
/**
|
|
116
|
+
* Assert a tool was called with arguments that deep-contain `expected`: every
|
|
117
|
+
* key in `expected` must equal the actual argument (recursively for nested
|
|
118
|
+
* objects; arrays match element-for-element), so extra arguments are ignored.
|
|
119
|
+
* Gate by default.
|
|
120
|
+
*/
|
|
121
|
+
calledToolWith(name: string, expected: Record<string, unknown>): AssertionHandle;
|
|
78
122
|
/** Assert a value against a matcher, e.g. `t.check(t.reply, includes("Sunny"))`. */
|
|
79
123
|
check(value: string, matcher: Matcher): AssertionHandle;
|
|
80
124
|
/**
|
|
81
125
|
* LLM-as-judge scoring of the last reply (via autoevals → a Databricks judge
|
|
82
|
-
* model). Each returns a scored
|
|
83
|
-
* to
|
|
84
|
-
* judge to be configured
|
|
126
|
+
* model). Each returns a scored assertion that gates by default (a miss fails
|
|
127
|
+
* the eval); chain `.atLeast(n)` to change the pass threshold or `.soft()` to
|
|
128
|
+
* demote to a tracked-only metric. Requires the judge to be configured
|
|
129
|
+
* (`--judge-model`).
|
|
85
130
|
*/
|
|
86
131
|
judge: {
|
|
87
132
|
/** Score factuality of the reply against an expected reference. */factuality(expected: string): Promise<AssertionHandle>; /** Score whether the reply answers the question, per optional `criteria`. */
|
|
@@ -103,9 +148,33 @@ interface EvalDefinition {
|
|
|
103
148
|
description?: string;
|
|
104
149
|
/** Target agent id. Defaults to the eval's parent `server/agents/<id>` dir. */
|
|
105
150
|
agent?: string;
|
|
151
|
+
/** Free-form tags for filtering (see the runner's `tags` / `--tag` option). */
|
|
152
|
+
tags?: string[];
|
|
153
|
+
/**
|
|
154
|
+
* Per-eval timeout (ms): `runEval` races the test against it and records a
|
|
155
|
+
* non-passing result instead of hanging. Overrides the runner/CLI default.
|
|
156
|
+
*/
|
|
157
|
+
timeoutMs?: number;
|
|
158
|
+
/**
|
|
159
|
+
* Run this eval once per row of a Databricks managed evaluation dataset (a
|
|
160
|
+
* Unity Catalog `catalog.schema.table` with `inputs`/`expectations` columns).
|
|
161
|
+
* Each row is bound to `t.input`/`t.expected`. Requires the runner to have a
|
|
162
|
+
* workspace client + warehouse (`--warehouse-id`). Omit for a single-run eval.
|
|
163
|
+
*/
|
|
164
|
+
dataset?: {
|
|
165
|
+
table: string;
|
|
166
|
+
limit?: number;
|
|
167
|
+
};
|
|
106
168
|
/** The eval body: drive the agent and assert on its behavior. */
|
|
107
169
|
test(t: TestContext): Promise<void> | void;
|
|
108
170
|
}
|
|
171
|
+
/** Per-directory config from `evals.config.ts` (see {@link defineEvalConfig}). */
|
|
172
|
+
interface EvalConfig {
|
|
173
|
+
/** Max evals to run concurrently. */
|
|
174
|
+
maxConcurrency?: number;
|
|
175
|
+
/** Default per-eval timeout. */
|
|
176
|
+
timeoutMs?: number;
|
|
177
|
+
}
|
|
109
178
|
/** The outcome of running one eval. */
|
|
110
179
|
interface EvalResult {
|
|
111
180
|
id: string;
|
|
@@ -119,9 +188,14 @@ interface EvalResult {
|
|
|
119
188
|
passed: boolean;
|
|
120
189
|
/** Set when the eval threw before completing. */
|
|
121
190
|
error?: string;
|
|
191
|
+
/**
|
|
192
|
+
* A turn failed at the transport/agent level (`succeeded: false`), not on an
|
|
193
|
+
* assertion — a retryable infra flake, distinct from `error`.
|
|
194
|
+
*/
|
|
195
|
+
infraFailure?: boolean;
|
|
122
196
|
/** MLflow trace id of the eval's last turn, for attaching assessments. */
|
|
123
197
|
traceId?: string;
|
|
124
198
|
}
|
|
125
199
|
//#endregion
|
|
126
|
-
export { AssertionHandle, AssertionResult, CustomJudgeSpec, DriveResult, EvalDefinition, EvalDriver, EvalResult, MatchResult, Matcher, Severity, TestContext };
|
|
200
|
+
export { AssertionHandle, AssertionResult, CustomJudgeSpec, DriveResult, EvalConfig, EvalDefinition, EvalDriver, EvalResult, MatchResult, Matcher, Severity, TestContext };
|
|
127
201
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../../src/evals/types.ts"],"mappings":";;AAWA;;;;;;;;;UAAiB,WAAA;EACf,IAAA;;EAEA,KAAA;EAMkD;EAJlD,MAAA;AAAA;;KAIU,OAAA,IAAW,KAAA,aAAkB,WAAA;;KAG7B,QAAA;;UAGK,eAAA;EACf,KAAA;EACA,QAAA,EAAU,QAAA;EACV,IAAA;EACA,KAAA;EACA,MAAA;AAAA;;;;AAQF;;UAAiB,eAAA;EAEP;EAAR,IAAA,IAAQ,eAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../../src/evals/types.ts"],"mappings":";;AAWA;;;;;;;;;UAAiB,WAAA;EACf,IAAA;;EAEA,KAAA;EAMkD;EAJlD,MAAA;AAAA;;KAIU,OAAA,IAAW,KAAA,aAAkB,WAAA;;KAG7B,QAAA;;UAGK,eAAA;EACf,KAAA;EACA,QAAA,EAAU,QAAA;EACV,IAAA;EACA,KAAA;EACA,MAAA;AAAA;;;;AAQF;;UAAiB,eAAA;EAEP;EAAR,IAAA,IAAQ,eAAA;EAQoB;EAN5B,IAAA,IAAQ,eAAA;EAMmC;;;;;EAA3C,OAAA,CAAQ,SAAA,WAAoB,eAAA;AAAA;;UAIb,WAAA;EAJ4B;EAM3C,KAAA;EAF0B;EAI1B,SAAA;EAEsB;EAAtB,eAAA,EAAiB,KAAA;IAAQ,IAAA;IAAc,IAAA,EAAM,MAAA;EAAA;EAApB;EAEzB,SAAA;EAF6C;EAI7C,SAAA;EAAA;EAEA,OAAA;AAAA;;AAOF;;;UAAiB,UAAA;EASJ;;;;;EAHX,IAAA,CACE,OAAA,UACA,OAAA;IAAY,MAAA,GAAS,WAAA;EAAA,IACpB,OAAA,CAAQ,WAAA;EADT;;;;EAMF,KAAA;AAAA;AAIF;AAAA,UAAiB,WAAA;;EAEf,IAAA,CAAK,OAAA,WAAkB,OAAA;EAiBP;;;;;EAXhB,KAAA;EAgC8B;EAAA,SA9BrB,KAAA;EAwC+B;EAAA,SAtC/B,SAAA;EAwC6B;EAAA,SAtC7B,SAAA;EAwCM;;;;EAAA,SAnCN,KAAA,EAAO,MAAA;EAjBhB;;;;EAAA,SAsBS,QAAA,EAAU,MAAA;EAZV;EAcT,SAAA,IAAa,eAAA;EAPJ;EAST,UAAA,CAAW,IAAA,WAAe,eAAA;EAJjB;;;;;;EAWT,cAAA,CACE,IAAA,UACA,QAAA,EAAU,MAAA,oBACT,eAAA;EAHH;EAKA,KAAA,CAAM,KAAA,UAAe,OAAA,EAAS,OAAA,GAAU,eAAA;EAH5B;;;;;;;EAWZ,KAAA;IAAA,mEAEE,UAAA,CAAW,QAAA,WAAmB,OAAA,CAAQ,eAAA,GAA3B;IAEX,QAAA,CAAS,QAAA,WAAmB,OAAA,CAAQ,eAAA,GAFE;IAItC,MAAA,CAAO,IAAA,EAAM,eAAA,GAAkB,OAAA,CAAQ,eAAA;EAAA;EAFX;EAK9B,IAAA,CAAK,MAAA;AAAA;;UAIU,eAAA;EACf,IAAA;EACA,cAAA;EACA,YAAA,EAAc,MAAA;AAAA;;UAIC,cAAA;EAPA;EASf,WAAA;;EAEA,KAAA;EAVA;EAYA,IAAA;EAVA;;;;EAeA,SAAA;EAX6B;;;;;;EAkB7B,OAAA;IAAY,KAAA;IAAe,KAAA;EAAA;EAE3B;EAAA,IAAA,CAAK,CAAA,EAAG,WAAA,GAAc,OAAA;AAAA;;UAIP,UAAA;EAJc;EAM7B,cAAA;EAFyB;EAIzB,SAAA;AAAA;;UAIe,UAAA;EACf,EAAA;EACA,WAAA;EAG2B;EAD3B,OAAA;IAAY,MAAA;EAAA;EACZ,UAAA,EAAY,eAAA;EAAZ;EAEA,MAAA;EAAA;EAEA,KAAA;EAKA;;;;EAAA,YAAA;;EAEA,OAAA;AAAA"}
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { AppKitError } from "./errors/base.js";
|
|
|
24
24
|
import { AuthenticationError } from "./errors/authentication.js";
|
|
25
25
|
import { ConfigurationError } from "./errors/configuration.js";
|
|
26
26
|
import { ConnectionError } from "./errors/connection.js";
|
|
27
|
+
import { DatabaseValidationError, DatabaseValidationIssue } from "./errors/database-validation.js";
|
|
27
28
|
import { ExecutionError } from "./errors/execution.js";
|
|
28
29
|
import { InitializationError } from "./errors/initialization.js";
|
|
29
30
|
import { ServerError } from "./errors/server.js";
|
|
@@ -53,4 +54,4 @@ import "./plugins/ga-exports.generated.js";
|
|
|
53
54
|
import { extractServingEndpoints, findServerFile } from "./type-generator/serving/server-file-extractor.js";
|
|
54
55
|
import { appKitServingTypesPlugin } from "./type-generator/serving/vite-plugin.js";
|
|
55
56
|
import { appKitTypesPlugin } from "./type-generator/vite-plugin.js";
|
|
56
|
-
export { ApiError, AppKitError, AuthenticationError, type BasePluginConfig, type CacheConfig, CacheManager, type ConfigSchema, ConfigurationError, ConnectionError, type Counter, type DatabaseCredential, type DatabaseRegistry, type EndpointConfig, ExecutionError, type ExecutionResult, type FileAction, type FilePolicy, type FilePolicyUser, type FileResource, type GenerateDatabaseCredentialRequest, type Histogram, type IAppRouter, type IJobsConfig, type ITelemetry, InitializationError, type JobAPI, type JobConfig, type JobsConnectorConfig, type JobsExport, type LakebasePool, type LakebasePoolConfig, type LakebasePoolManager, Plugin, type PluginData, type PluginManifest, PolicyDeniedError, READ_ACTIONS, type RequestedClaims, RequestedClaimsPermissionSet, type RequestedResource, type ResourceEntry, type ResourceFieldEntry, type ResourcePermission, ResourceRegistry, type ResourceRequirement, ResourceType, ServerError, type ServingEndpointEntry, type ServingEndpointRegistry, type ServingFactory, SeverityNumber, type Span, SpanStatusCode, type StreamExecutionSettings, type TelemetryConfig, type ToPlugin, TunnelError, ValidationError, type ValidationResult, WRITE_ACTIONS, type WorkspaceClient, type WorkspaceClientOptions, analytics, appKitServingTypesPlugin, appKitTypesPlugin, createApp, createLakebasePool, createLakebasePoolManager, createWorkspaceClient, defineManifest, extractServingEndpoints, files, findServerFile, generateDatabaseCredential, genie, getExecutionContext, getLakebaseOrmConfig, getLakebasePgConfig, getPluginManifest, getResourceRequirements, getUsernameWithApiLookup, getWorkspaceClient, isSQLTypeMarker, jobs, lakebase, server, serving, sql, toPlugin };
|
|
57
|
+
export { ApiError, AppKitError, AuthenticationError, type BasePluginConfig, type CacheConfig, CacheManager, type ConfigSchema, ConfigurationError, ConnectionError, type Counter, type DatabaseCredential, type DatabaseRegistry, DatabaseValidationError, type DatabaseValidationIssue, type EndpointConfig, ExecutionError, type ExecutionResult, type FileAction, type FilePolicy, type FilePolicyUser, type FileResource, type GenerateDatabaseCredentialRequest, type Histogram, type IAppRouter, type IJobsConfig, type ITelemetry, InitializationError, type JobAPI, type JobConfig, type JobsConnectorConfig, type JobsExport, type LakebasePool, type LakebasePoolConfig, type LakebasePoolManager, Plugin, type PluginData, type PluginManifest, PolicyDeniedError, READ_ACTIONS, type RequestedClaims, RequestedClaimsPermissionSet, type RequestedResource, type ResourceEntry, type ResourceFieldEntry, type ResourcePermission, ResourceRegistry, type ResourceRequirement, ResourceType, ServerError, type ServingEndpointEntry, type ServingEndpointRegistry, type ServingFactory, SeverityNumber, type Span, SpanStatusCode, type StreamExecutionSettings, type TelemetryConfig, type ToPlugin, TunnelError, ValidationError, type ValidationResult, WRITE_ACTIONS, type WorkspaceClient, type WorkspaceClientOptions, analytics, appKitServingTypesPlugin, appKitTypesPlugin, createApp, createLakebasePool, createLakebasePoolManager, createWorkspaceClient, defineManifest, extractServingEndpoints, files, findServerFile, generateDatabaseCredential, genie, getExecutionContext, getLakebaseOrmConfig, getLakebasePgConfig, getPluginManifest, getResourceRequirements, getUsernameWithApiLookup, getWorkspaceClient, isSQLTypeMarker, jobs, lakebase, server, serving, sql, toPlugin };
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { AppKitError } from "./errors/base.js";
|
|
|
6
6
|
import { AuthenticationError } from "./errors/authentication.js";
|
|
7
7
|
import { ConfigurationError } from "./errors/configuration.js";
|
|
8
8
|
import { ConnectionError } from "./errors/connection.js";
|
|
9
|
+
import { DatabaseValidationError } from "./errors/database-validation.js";
|
|
9
10
|
import { ExecutionError } from "./errors/execution.js";
|
|
10
11
|
import { InitializationError } from "./errors/initialization.js";
|
|
11
12
|
import { ServerError } from "./errors/server.js";
|
|
@@ -39,4 +40,4 @@ import { server } from "./plugins/server/index.js";
|
|
|
39
40
|
import { serving } from "./plugins/serving/serving.js";
|
|
40
41
|
import "./plugins/ga-exports.generated.js";
|
|
41
42
|
|
|
42
|
-
export { ApiError, AppKitError, AuthenticationError, CacheManager, ConfigurationError, ConnectionError, ExecutionError, InitializationError, Plugin, PolicyDeniedError, READ_ACTIONS, RequestedClaimsPermissionSet, ResourceRegistry, ResourceType, ServerError, SeverityNumber, SpanStatusCode, TunnelError, ValidationError, WRITE_ACTIONS, analytics, appKitServingTypesPlugin, appKitTypesPlugin, createApp, createLakebasePool, createLakebasePoolManager, createWorkspaceClient, defineManifest, extractServingEndpoints, files, findServerFile, generateDatabaseCredential, genie, getExecutionContext, getLakebaseOrmConfig, getLakebasePgConfig, getPluginManifest, getResourceRequirements, getUsernameWithApiLookup, getWorkspaceClient, isSQLTypeMarker, jobs, lakebase, server, serving, sql, toPlugin };
|
|
43
|
+
export { ApiError, AppKitError, AuthenticationError, CacheManager, ConfigurationError, ConnectionError, DatabaseValidationError, ExecutionError, InitializationError, Plugin, PolicyDeniedError, READ_ACTIONS, RequestedClaimsPermissionSet, ResourceRegistry, ResourceType, ServerError, SeverityNumber, SpanStatusCode, TunnelError, ValidationError, WRITE_ACTIONS, analytics, appKitServingTypesPlugin, appKitTypesPlugin, createApp, createLakebasePool, createLakebasePoolManager, createWorkspaceClient, defineManifest, extractServingEndpoints, files, findServerFile, generateDatabaseCredential, genie, getExecutionContext, getLakebaseOrmConfig, getLakebasePgConfig, getPluginManifest, getResourceRequirements, getUsernameWithApiLookup, getWorkspaceClient, isSQLTypeMarker, jobs, lakebase, server, serving, sql, toPlugin };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../../src/plugin/plugin.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBA0NsB,MAAA,iBACJ,gBAAA,GAAmB,gBAAA,aACxB,UAAA;EAAA,UA4BW,MAAA,EAAQ,OAAA;EAAA,UA3BpB,OAAA;EAAA,UACA,KAAA,EAAQ,YAAA;EAAA,UACR,GAAA,EAAK,UAAA;EAAA,UACL,aAAA,EAAe,aAAA;EAAA,UACf,aAAA,EAAe,aAAA;EAAA,UACf,SAAA,EAAY,UAAA;EAAA,UACZ,OAAA,GAAU,aAAA;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../../src/plugin/plugin.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBA0NsB,MAAA,iBACJ,gBAAA,GAAmB,gBAAA,aACxB,UAAA;EAAA,UA4BW,MAAA,EAAQ,OAAA;EAAA,UA3BpB,OAAA;EAAA,UACA,KAAA,EAAQ,YAAA;EAAA,UACR,GAAA,EAAK,UAAA;EAAA,UACL,aAAA,EAAe,aAAA;EAAA,UACf,aAAA,EAAe,aAAA;EAAA,UACf,SAAA,EAAY,UAAA;EAAA,UACZ,OAAA,GAAU,aAAA;EA0SO;EAAA,QAvSnB,mBAAA;EAwSG;EAAA,QArSH,oBAAA;EAsSN;;;;;;EAAA,OA9RK,KAAA,EAAO,WAAA;EA+W0B;;;EA1WxC,IAAA;cAEsB,MAAA,EAAQ,OAAA;EAAA,QAoBtB,gBAAA;EAuVG;;;;;;;;EAlUX,aAAA,CACE,IAAA;IACE,OAAA;IACA,eAAA,GAAkB,gBAAA;EAAA;EAgBtB,YAAA,CAAa,CAAA,EAAG,OAAA,CAAQ,MAAA;EAIlB,KAAA,CAAA,GAAK,OAAA;EAEX,YAAA,CAAA,GAAgB,iBAAA;EAIhB,uBAAA,CAAA,GAA2B,WAAA;EAI3B,qBAAA,CAAA;EAgbyB;;;;;;;;;;;;;;;;;;;;;;;;EApZzB,OAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiDA,YAAA,CAAA,GAAgB,MAAA;;;;;;;;;;;YAcN,aAAA,CAAc,GAAA,EAAK,OAAA,CAAQ,OAAA;;;;;;;;;;;EAiBrC,MAAA,CAAO,GAAA,EAAK,OAAA,CAAQ,OAAA;;;;;;;;;;;;;;UAyDZ,kBAAA;EAAA,UAoCQ,aAAA,GAAA,CACd,GAAA,EAAK,YAAA,EACL,EAAA,EAAI,oBAAA,CAAqB,CAAA,GACzB,OAAA,EAAS,uBAAA,EACT,OAAA,YAAgB,OAAA;;;;;;;;;;YAgFF,OAAA,GAAA,CACd,EAAA,GAAK,MAAA,GAAS,WAAA,KAAgB,OAAA,CAAQ,CAAA,GACtC,OAAA,EAAS,uBAAA,EACT,OAAA,YACC,OAAA,CAAQ,eAAA,CAAgB,CAAA;EAAA,UAmDjB,gBAAA,CAAiB,IAAA,UAAc,IAAA;EAAA,UAI/B,KAAA,YAAA,CACR,MAAA,EAAQ,OAAA,CAAQ,MAAA,EAChB,MAAA,EAAQ,WAAA;EAAA,QAeF,qBAAA;EAAA,QAaA,kBAAA;EAAA,QAqCM,wBAAA;EAAA,QAqBN,iBAAA;AAAA"}
|
package/dist/plugin/plugin.js
CHANGED
|
@@ -382,7 +382,7 @@ var Plugin = class {
|
|
|
382
382
|
const raw = value.call(target);
|
|
383
383
|
if (raw == null) return {};
|
|
384
384
|
if (typeof raw === "function") return raw;
|
|
385
|
-
if (isPlainObject(raw)) return wrapExportFunctions(raw, wrapCall);
|
|
385
|
+
if (isPlainObject(raw)) return wrapExportFunctions(raw, (fn) => wrapCall(fn.bind(target)));
|
|
386
386
|
return raw;
|
|
387
387
|
};
|
|
388
388
|
return wrapCall(value.bind(target));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","names":["otelContext","context"],"sources":["../../src/plugin/plugin.ts"],"sourcesContent":["import { createContextKey, context as otelContext } from \"@opentelemetry/api\";\nimport type express from \"express\";\nimport type {\n BasePlugin,\n BasePluginConfig,\n IAppResponse,\n PluginEndpointMap,\n PluginExecuteConfig,\n PluginExecutionSettings,\n PluginPhase,\n RouteConfig,\n StreamExecuteHandler,\n StreamExecutionSettings,\n} from \"shared\";\nimport { camelToKebab } from \"shared\";\n\nimport { AppManager } from \"../app\";\nimport { CacheManager } from \"../cache\";\nimport { getCurrentUserId, runInUserContext, ServiceContext } from \"../context\";\nimport type { PluginContext } from \"../core/plugin-context\";\nimport { AppKitError, AuthenticationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { StreamManager } from \"../stream\";\nimport {\n type ITelemetry,\n normalizeTelemetryOptions,\n TelemetryManager,\n} from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { forwardAsyncErrors } from \"../utils/safe-handler\";\nimport { DevFileReader } from \"./dev-reader\";\nimport type { ExecutionResult } from \"./execution-result\";\nimport { CacheInterceptor } from \"./interceptors/cache\";\nimport { RetryInterceptor } from \"./interceptors/retry\";\nimport { TelemetryInterceptor } from \"./interceptors/telemetry\";\nimport { TimeoutInterceptor } from \"./interceptors/timeout\";\nimport type {\n ExecutionInterceptor,\n InterceptorContext,\n} from \"./interceptors/types\";\n\nconst logger = createLogger(\"plugin\");\n\n/**\n * OTel context key for marking OBO dev mode fallback.\n * Set when asUser() is called in development mode without a user token.\n */\nconst DEV_OBO_FALLBACK_KEY = createContextKey(\"appkit.devOboFallback\");\n\n/**\n * Returns true if `value` is a plain object literal (not an array, Date,\n * class instance, etc.). Used to decide whether to recurse into nested\n * export shapes when wrapping functions.\n *\n * @internal exported so the AppKit core can reuse the same predicate for\n * its `bindExportMethods` walk; not part of the public package surface.\n */\nexport function isPlainObject(\n value: unknown,\n): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Returns a deep copy of `exports` where every function has been replaced\n * with `wrap(fn)`, walking into nested plain objects.\n *\n * Used by the asUser proxy to make the user context follow function\n * references that escape the proxy via `exports()`. The original input is\n * not mutated, so plugins that memoize `exports()` are safe — each call\n * through the proxy yields an independent, freshly wrapped view.\n */\nfunction wrapExportFunctions(\n exports: Record<string, unknown>,\n wrap: (fn: (...a: unknown[]) => unknown) => (...a: unknown[]) => unknown,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(exports)) {\n const val = exports[key];\n if (typeof val === \"function\") {\n result[key] = wrap(val as (...a: unknown[]) => unknown);\n } else if (isPlainObject(val)) {\n result[key] = wrapExportFunctions(val, wrap);\n } else {\n result[key] = val;\n }\n }\n return result;\n}\n\n/**\n * Returns true if the current execution is an OBO dev mode fallback\n * (asUser() was called but fell back to service principal due to missing token).\n */\nexport function isDevOboFallback(): boolean {\n return otelContext.active().getValue(DEV_OBO_FALLBACK_KEY) === true;\n}\n\n/**\n * Narrow an unknown thrown value to an Error that carries a numeric\n * `statusCode` property (e.g. `ApiError` from `@databricks/sdk-experimental`).\n */\nfunction hasHttpStatusCode(\n error: unknown,\n): error is Error & { statusCode: number } {\n return (\n error instanceof Error &&\n \"statusCode\" in error &&\n typeof (error as Record<string, unknown>).statusCode === \"number\"\n );\n}\n\n/**\n * Methods that should not be proxied by asUser().\n * These are lifecycle/internal methods that don't make sense\n * to execute in a user context.\n */\nconst EXCLUDED_FROM_PROXY = new Set([\n // Lifecycle methods\n \"setup\",\n \"shutdown\",\n \"attachContext\",\n \"injectRoutes\",\n \"getEndpoints\",\n \"getSkipBodyParsingPaths\",\n \"abortActiveOperations\",\n \"clientConfig\",\n // asUser itself - prevent chaining like .asUser().asUser()\n \"asUser\",\n // Internal methods\n \"constructor\",\n]);\n\n/**\n * Base abstract class for creating AppKit plugins.\n *\n * All plugins must declare a static `manifest` property with their metadata\n * and resource requirements. The manifest defines:\n * - `required` resources: Always needed for the plugin to function\n * - `optional` resources: May be needed depending on plugin configuration\n *\n * ## Static vs Runtime Resource Requirements\n *\n * The manifest is static and doesn't know the plugin's runtime configuration.\n * For resources that become required based on config options, plugins can\n * implement a static `getResourceRequirements(config)` method.\n *\n * At runtime, this method is called with the actual config to determine\n * which \"optional\" resources should be treated as \"required\".\n *\n * @example Basic plugin with static requirements\n * ```typescript\n * import { Plugin, toPlugin, PluginManifest, ResourceType } from '@databricks/appkit';\n *\n * const myManifest: PluginManifest = {\n * name: 'myPlugin',\n * displayName: 'My Plugin',\n * description: 'Does something awesome',\n * resources: {\n * required: [\n * { type: ResourceType.SQL_WAREHOUSE, alias: 'warehouse', ... }\n * ],\n * optional: []\n * }\n * };\n *\n * class MyPlugin extends Plugin<MyConfig> {\n * static manifest = myManifest;\n * }\n * ```\n *\n * @example Plugin with config-dependent resources\n * ```typescript\n * interface MyConfig extends BasePluginConfig {\n * enableCaching?: boolean;\n * }\n *\n * const myManifest: PluginManifest = {\n * name: 'myPlugin',\n * resources: {\n * required: [\n * { type: ResourceType.SQL_WAREHOUSE, alias: 'warehouse', ... }\n * ],\n * optional: [\n * // Database is optional in the static manifest\n * { type: ResourceType.DATABASE, alias: 'cache', description: 'Required if caching enabled', ... }\n * ]\n * }\n * };\n *\n * class MyPlugin extends Plugin<MyConfig> {\n * static manifest = myManifest<\"myPlugin\">;\n *\n * // Runtime method: converts optional resources to required based on config\n * static getResourceRequirements(config: MyConfig) {\n * const resources = [];\n * if (config.enableCaching) {\n * // When caching is enabled, Database becomes required\n * resources.push({\n * type: ResourceType.DATABASE,\n * alias: 'cache',\n * resourceKey: 'database',\n * description: 'Cache storage for query results',\n * permission: 'CAN_CONNECT_AND_CREATE',\n * fields: {\n * instance_name: { env: 'DATABRICKS_CACHE_INSTANCE' },\n * database_name: { env: 'DATABRICKS_CACHE_DB' },\n * },\n * required: true // Mark as required at runtime\n * });\n * }\n * return resources;\n * }\n * }\n * ```\n */\nexport abstract class Plugin<\n TConfig extends BasePluginConfig = BasePluginConfig,\n> implements BasePlugin {\n protected isReady = false;\n protected cache!: CacheManager;\n protected app: AppManager;\n protected devFileReader: DevFileReader;\n protected streamManager: StreamManager;\n protected telemetry!: ITelemetry;\n protected context?: PluginContext;\n\n /** Registered endpoints for this plugin */\n private registeredEndpoints: PluginEndpointMap = {};\n\n /** Paths that opt out of JSON body parsing (e.g. file upload routes) */\n private skipBodyParsingPaths: Set<string> = new Set();\n\n /**\n * Plugin initialization phase.\n * - 'core': Initialized first (e.g., config plugins)\n * - 'normal': Initialized second (most plugins)\n * - 'deferred': Initialized last (e.g., server plugin)\n */\n static phase: PluginPhase = \"normal\";\n\n /**\n * Plugin name identifier.\n */\n name: string;\n\n constructor(protected config: TConfig) {\n this.name =\n config.name ??\n (this.constructor as { manifest?: { name: string } }).manifest?.name ??\n \"plugin\";\n this.streamManager = new StreamManager(config.streamConfig);\n this.app = new AppManager();\n this.devFileReader = DevFileReader.getInstance();\n this.context = (config as Record<string, unknown>).context as\n | PluginContext\n | undefined;\n\n // Eagerly bind telemetry + cache if the core services have already been\n // initialized (normal createApp path, or tests that mock CacheManager).\n // If they haven't, we leave these undefined and rely on `attachContext`\n // being called later — this lets factories eagerly construct plugin\n // instances at module top-level before `createApp` has run.\n this.tryAttachContext();\n }\n\n private tryAttachContext(): void {\n try {\n this.cache = CacheManager.getInstanceSync();\n } catch {\n return;\n }\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.isReady = true;\n }\n\n /**\n * Binds runtime dependencies (telemetry provider, cache, plugin context) to\n * this plugin. Called by `AppKit._createApp` after construction and before\n * `setup()`. Idempotent: safe to call if the constructor already bound them\n * eagerly. Kept separate so factories can eagerly construct plugin instances\n * without running this before `TelemetryManager.initialize()` /\n * `CacheManager.getInstance()` have run.\n */\n attachContext(\n deps: {\n context?: unknown;\n telemetryConfig?: BasePluginConfig[\"telemetry\"];\n } = {},\n ): void {\n if (!this.cache) {\n this.cache = CacheManager.getInstanceSync();\n }\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n deps.telemetryConfig ?? this.config.telemetry,\n );\n if (deps.context !== undefined) {\n this.context = deps.context as PluginContext;\n }\n this.isReady = true;\n }\n\n injectRoutes(_: express.Router) {\n return;\n }\n\n async setup() {}\n\n getEndpoints(): PluginEndpointMap {\n return this.registeredEndpoints;\n }\n\n getSkipBodyParsingPaths(): ReadonlySet<string> {\n return this.skipBodyParsingPaths;\n }\n\n abortActiveOperations(): void {\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the public exports for this plugin.\n * Override this to define a custom public API.\n * By default, returns an empty object.\n *\n * The returned object becomes the plugin's public API on the AppKit instance\n * (e.g. `appkit.myPlugin.method()`). AppKit automatically binds method context\n * and adds `asUser(req)` for user-scoped execution.\n *\n * @example\n * ```ts\n * class MyPlugin extends Plugin {\n * private getData() { return []; }\n *\n * exports() {\n * return { getData: this.getData };\n * }\n * }\n *\n * // After registration:\n * const appkit = await createApp({ plugins: [myPlugin()] });\n * appkit.myPlugin.getData();\n * ```\n */\n exports(): unknown {\n return {};\n }\n\n /**\n * Returns startup config to expose to the client.\n * Override this to surface server-side values that are safe to publish to the\n * frontend, such as feature flags, resource IDs, or other app boot settings.\n *\n * This runs once when the server starts, so it should not depend on\n * request-scoped or user-specific state.\n *\n * String values that match non-public environment variables are redacted\n * unless you intentionally expose them via a matching `PUBLIC_APPKIT_` env var.\n *\n * Values must be JSON-serializable plain data (no functions, Dates, classes,\n * Maps, Sets, BigInts, or circular references).\n * By default returns an empty object (plugin contributes nothing to client config).\n *\n * On the client, read the config with the `usePluginClientConfig` hook\n * (React) or the `getPluginClientConfig` function (vanilla JS), both\n * from `@databricks/appkit-ui`.\n *\n * @example\n * ```ts\n * // Server — plugin definition\n * class MyPlugin extends Plugin<MyConfig> {\n * clientConfig() {\n * return {\n * warehouseId: this.config.warehouseId,\n * features: { darkMode: true },\n * };\n * }\n * }\n *\n * // Client — React component\n * import { usePluginClientConfig } from \"@databricks/appkit-ui/react\";\n *\n * interface MyPluginConfig { warehouseId: string; features: { darkMode: boolean } }\n *\n * const config = usePluginClientConfig<MyPluginConfig>(\"myPlugin\");\n * config.warehouseId; // \"abc-123\"\n *\n * // Client — vanilla JS\n * import { getPluginClientConfig } from \"@databricks/appkit-ui/js\";\n *\n * const config = getPluginClientConfig<MyPluginConfig>(\"myPlugin\");\n * ```\n */\n clientConfig(): Record<string, unknown> {\n return {};\n }\n\n /**\n * Resolve the effective user ID from a request.\n *\n * Returns the `x-forwarded-user` header when present. In development mode\n * (`NODE_ENV=development`) falls back to the current context user ID so\n * that callers outside an active `runInUserContext` scope still get a\n * consistent value.\n *\n * @throws AuthenticationError in production when no user header is present.\n */\n protected resolveUserId(req: express.Request): string {\n const userId = req.header(\"x-forwarded-user\")?.trim();\n if (userId) return userId;\n if (process.env.NODE_ENV === \"development\") return getCurrentUserId();\n throw AuthenticationError.missingUserId();\n }\n\n /**\n * Execute operations using the user's identity from the request.\n * Returns a proxy of this plugin where all method calls execute\n * with the user's Databricks credentials instead of the service principal.\n *\n * @param req - The Express request containing the user token in headers\n * @returns A proxied plugin instance that executes as the user\n * @throws AuthenticationError if user token is not available in request headers (production only).\n * In development mode (`NODE_ENV=development`), skips user impersonation instead of throwing.\n */\n asUser(req: express.Request): this {\n const token = req.header(\"x-forwarded-access-token\")?.trim();\n const userId = req.header(\"x-forwarded-user\")?.trim();\n const userEmail = req.header(\"x-forwarded-email\");\n const isDev = process.env.NODE_ENV === \"development\";\n\n // In local development, skip user impersonation since there's no user\n // token available. Mark execution as OBO dev fallback via OTel context\n // so telemetry can distinguish intended OBO calls from regular SP calls.\n if (!token && isDev) {\n logger.warn(\n \"asUser() called without user token in development mode. Skipping user impersonation.\",\n );\n\n return this._createAsUserProxy((fn) => (...args) => {\n const ctx = otelContext.active().setValue(DEV_OBO_FALLBACK_KEY, true);\n return otelContext.with(ctx, () => fn(...args));\n });\n }\n\n if (!token) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n\n if (!userId && !isDev) {\n throw AuthenticationError.missingUserId();\n }\n\n const effectiveUserId = userId || \"dev-user\";\n\n const userContext = ServiceContext.createUserContext(\n token,\n effectiveUserId,\n undefined,\n userEmail ?? undefined,\n );\n\n return this._createAsUserProxy(\n (fn) =>\n (...args) =>\n runInUserContext(userContext, () => fn(...args)),\n );\n }\n\n /**\n * Creates a proxy of `this` where every method call — and every function\n * in the result of `exports()` — runs inside `wrapCall`.\n *\n * `wrapCall` decides the per-call scope. Two strategies are used today:\n * - real OBO: fn => (...args) => runInUserContext(userContext, () => fn(...args))\n * - dev fallback: fn => (...args) => otelContext.with(DEV_OBO_FALLBACK_KEY=true, () => fn(...args))\n *\n * `exports` is intercepted because methods captured in the returned\n * exports object never re-enter the proxy's `get` trap. Wrapping them\n * here is the only way to make the user context follow function\n * references back out of the plugin.\n */\n private _createAsUserProxy(\n wrapCall: (\n fn: (...a: unknown[]) => unknown,\n ) => (...a: unknown[]) => unknown,\n ): this {\n return new Proxy(this, {\n get: (target, prop, receiver) => {\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value !== \"function\") return value;\n if (typeof prop === \"string\" && EXCLUDED_FROM_PROXY.has(prop))\n return value;\n\n if (prop === \"exports\") {\n return () => {\n const raw = (value as () => unknown).call(target);\n if (raw == null) return {};\n // Callable exports (e.g. files, jobs) manage per-call asUser\n // themselves; leave them untouched.\n if (typeof raw === \"function\") return raw;\n if (isPlainObject(raw)) {\n return wrapExportFunctions(raw, wrapCall);\n }\n return raw;\n };\n }\n\n const fn = (value as (...a: unknown[]) => unknown).bind(target);\n return wrapCall(fn);\n },\n }) as this;\n }\n\n // streaming execution with interceptors\n protected async executeStream<T>(\n res: IAppResponse,\n fn: StreamExecuteHandler<T>,\n options: StreamExecutionSettings,\n userKey?: string,\n ) {\n // destructure options\n const {\n stream: streamConfig,\n default: defaultConfig,\n user: userConfig,\n } = options;\n\n // build execution options\n const executeConfig = this._buildExecutionConfig({\n default: defaultConfig,\n user: userConfig,\n });\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const self = this;\n // capture the active OTel context (HTTP span) before entering the async generator,\n // where it would otherwise be lost across the async boundary\n const parentOtelContext = otelContext.active();\n\n // wrapper function to ensure it returns a generator\n const asyncWrapperFn = async function* (streamSignal?: AbortSignal) {\n // build execution context\n const context: InterceptorContext = {\n signal: streamSignal,\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n // build interceptors\n const interceptors = self._buildInterceptors(executeConfig);\n\n // wrap the function to ensure it returns a promise\n const wrappedFn = async () => {\n const result = await fn(context.signal);\n return result;\n };\n\n // execute the function with interceptors, restoring the parent OTel context\n // so telemetry spans are linked as children of the HTTP request span\n const result = await otelContext.with(parentOtelContext, () =>\n self._executeWithInterceptors(\n wrappedFn as (signal?: AbortSignal) => Promise<T>,\n interceptors,\n context,\n ),\n );\n\n // check if result is a generator\n if (self._checkIfGenerator(result)) {\n yield* result;\n } else {\n yield result;\n }\n };\n\n // stream the result to the client. The effective user key is forwarded\n // to the stream manager so that reconnections to existing streamIds are\n // bound to the original creator (prevents cross-user stream takeover via\n // guessed/leaked IDs).\n await this.streamManager.stream(\n res,\n asyncWrapperFn,\n streamConfig,\n effectiveUserKey,\n );\n }\n\n /**\n * Execute a function with the plugin's interceptor chain.\n *\n * Returns an {@link ExecutionResult} discriminated union:\n * - `{ ok: true, data: T }` on success\n * - `{ ok: false, status: number, message: string }` on failure\n *\n * Errors are never thrown — the method is production-safe.\n */\n protected async execute<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: PluginExecutionSettings,\n userKey?: string,\n ): Promise<ExecutionResult<T>> {\n const executeConfig = this._buildExecutionConfig(options);\n\n const interceptors = this._buildInterceptors(executeConfig);\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const context: InterceptorContext = {\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n try {\n const data = await this._executeWithInterceptors(\n fn,\n interceptors,\n context,\n );\n return { ok: true, data };\n } catch (error) {\n logger.error(\"Plugin execution failed\", { error, plugin: this.name });\n\n if (error instanceof AppKitError) {\n return {\n ok: false,\n status: error.statusCode,\n message: error.message,\n };\n }\n\n if (hasHttpStatusCode(error)) {\n const isDev = process.env.NODE_ENV !== \"production\";\n const isClientError = error.statusCode >= 400 && error.statusCode < 500;\n return {\n ok: false,\n status: error.statusCode,\n message: isDev || isClientError ? error.message : \"Server error\",\n };\n }\n\n const isDev = process.env.NODE_ENV !== \"production\";\n return {\n ok: false,\n status: 500,\n message:\n isDev && error instanceof Error ? error.message : \"Server error\",\n };\n }\n }\n\n protected registerEndpoint(name: string, path: string): void {\n this.registeredEndpoints[name] = path;\n }\n\n protected route<_TResponse>(\n router: express.Router,\n config: RouteConfig,\n ): void {\n const { name, method, path, handler } = config;\n\n router[method](path, forwardAsyncErrors(handler));\n\n const fullPath = `/api/${camelToKebab(this.name)}${path}`;\n this.registerEndpoint(name, fullPath);\n\n if (config.skipBodyParsing) {\n this.skipBodyParsingPaths.add(fullPath);\n }\n }\n\n // build execution options by merging defaults, plugin config, and user overrides\n private _buildExecutionConfig(\n options: PluginExecutionSettings,\n ): PluginExecuteConfig {\n const { default: methodDefaults, user: userOverride } = options;\n\n // Merge: method defaults <- plugin config <- user override (highest priority)\n return deepMerge(\n deepMerge(methodDefaults, this.config),\n userOverride ?? {},\n ) as PluginExecuteConfig;\n }\n\n // build interceptors based on execute options\n private _buildInterceptors(\n options: PluginExecuteConfig,\n ): ExecutionInterceptor[] {\n const interceptors: ExecutionInterceptor[] = [];\n\n // order matters: telemetry → timeout → retry → cache (innermost to outermost)\n\n const telemetryConfig = normalizeTelemetryOptions(this.config.telemetry);\n if (\n telemetryConfig.traces &&\n (options.telemetryInterceptor?.enabled ?? true)\n ) {\n interceptors.push(\n new TelemetryInterceptor(this.telemetry, options.telemetryInterceptor),\n );\n }\n\n if (options.timeout && options.timeout > 0) {\n interceptors.push(new TimeoutInterceptor(options.timeout));\n }\n\n if (\n options.retry?.enabled &&\n options.retry.attempts &&\n options.retry.attempts > 1\n ) {\n interceptors.push(new RetryInterceptor(options.retry));\n }\n\n if (options.cache?.enabled && options.cache.cacheKey?.length) {\n interceptors.push(new CacheInterceptor(this.cache, options.cache));\n }\n\n return interceptors;\n }\n\n // execute method wrapped with interceptors\n private async _executeWithInterceptors<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n interceptors: ExecutionInterceptor[],\n context: InterceptorContext,\n ): Promise<T> {\n // no interceptors, execute directly\n if (interceptors.length === 0) {\n return fn(context.signal);\n }\n // build nested execution chain from interceptors\n let wrappedFn = () => fn(context.signal);\n\n // wrap each interceptor around the previous function\n for (const interceptor of interceptors) {\n const previousFn = wrappedFn;\n wrappedFn = () => interceptor.intercept(previousFn, context);\n }\n\n return wrappedFn();\n }\n\n private _checkIfGenerator(\n result: any,\n ): result is AsyncGenerator<any, void, unknown> {\n return (\n result && typeof result === \"object\" && Symbol.asyncIterator in result\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin.js","names":["otelContext","context"],"sources":["../../src/plugin/plugin.ts"],"sourcesContent":["import { createContextKey, context as otelContext } from \"@opentelemetry/api\";\nimport type express from \"express\";\nimport type {\n BasePlugin,\n BasePluginConfig,\n IAppResponse,\n PluginEndpointMap,\n PluginExecuteConfig,\n PluginExecutionSettings,\n PluginPhase,\n RouteConfig,\n StreamExecuteHandler,\n StreamExecutionSettings,\n} from \"shared\";\nimport { camelToKebab } from \"shared\";\n\nimport { AppManager } from \"../app\";\nimport { CacheManager } from \"../cache\";\nimport { getCurrentUserId, runInUserContext, ServiceContext } from \"../context\";\nimport type { PluginContext } from \"../core/plugin-context\";\nimport { AppKitError, AuthenticationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { StreamManager } from \"../stream\";\nimport {\n type ITelemetry,\n normalizeTelemetryOptions,\n TelemetryManager,\n} from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { forwardAsyncErrors } from \"../utils/safe-handler\";\nimport { DevFileReader } from \"./dev-reader\";\nimport type { ExecutionResult } from \"./execution-result\";\nimport { CacheInterceptor } from \"./interceptors/cache\";\nimport { RetryInterceptor } from \"./interceptors/retry\";\nimport { TelemetryInterceptor } from \"./interceptors/telemetry\";\nimport { TimeoutInterceptor } from \"./interceptors/timeout\";\nimport type {\n ExecutionInterceptor,\n InterceptorContext,\n} from \"./interceptors/types\";\n\nconst logger = createLogger(\"plugin\");\n\n/**\n * OTel context key for marking OBO dev mode fallback.\n * Set when asUser() is called in development mode without a user token.\n */\nconst DEV_OBO_FALLBACK_KEY = createContextKey(\"appkit.devOboFallback\");\n\n/**\n * Returns true if `value` is a plain object literal (not an array, Date,\n * class instance, etc.). Used to decide whether to recurse into nested\n * export shapes when wrapping functions.\n *\n * @internal exported so the AppKit core can reuse the same predicate for\n * its `bindExportMethods` walk; not part of the public package surface.\n */\nexport function isPlainObject(\n value: unknown,\n): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Returns a deep copy of `exports` where every function has been replaced\n * with `wrap(fn)`, walking into nested plain objects.\n *\n * Used by the asUser proxy to make the user context follow function\n * references that escape the proxy via `exports()`. The original input is\n * not mutated, so plugins that memoize `exports()` are safe — each call\n * through the proxy yields an independent, freshly wrapped view.\n */\nfunction wrapExportFunctions(\n exports: Record<string, unknown>,\n wrap: (fn: (...a: unknown[]) => unknown) => (...a: unknown[]) => unknown,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(exports)) {\n const val = exports[key];\n if (typeof val === \"function\") {\n result[key] = wrap(val as (...a: unknown[]) => unknown);\n } else if (isPlainObject(val)) {\n result[key] = wrapExportFunctions(val, wrap);\n } else {\n result[key] = val;\n }\n }\n return result;\n}\n\n/**\n * Returns true if the current execution is an OBO dev mode fallback\n * (asUser() was called but fell back to service principal due to missing token).\n */\nexport function isDevOboFallback(): boolean {\n return otelContext.active().getValue(DEV_OBO_FALLBACK_KEY) === true;\n}\n\n/**\n * Narrow an unknown thrown value to an Error that carries a numeric\n * `statusCode` property (e.g. `ApiError` from `@databricks/sdk-experimental`).\n */\nfunction hasHttpStatusCode(\n error: unknown,\n): error is Error & { statusCode: number } {\n return (\n error instanceof Error &&\n \"statusCode\" in error &&\n typeof (error as Record<string, unknown>).statusCode === \"number\"\n );\n}\n\n/**\n * Methods that should not be proxied by asUser().\n * These are lifecycle/internal methods that don't make sense\n * to execute in a user context.\n */\nconst EXCLUDED_FROM_PROXY = new Set([\n // Lifecycle methods\n \"setup\",\n \"shutdown\",\n \"attachContext\",\n \"injectRoutes\",\n \"getEndpoints\",\n \"getSkipBodyParsingPaths\",\n \"abortActiveOperations\",\n \"clientConfig\",\n // asUser itself - prevent chaining like .asUser().asUser()\n \"asUser\",\n // Internal methods\n \"constructor\",\n]);\n\n/**\n * Base abstract class for creating AppKit plugins.\n *\n * All plugins must declare a static `manifest` property with their metadata\n * and resource requirements. The manifest defines:\n * - `required` resources: Always needed for the plugin to function\n * - `optional` resources: May be needed depending on plugin configuration\n *\n * ## Static vs Runtime Resource Requirements\n *\n * The manifest is static and doesn't know the plugin's runtime configuration.\n * For resources that become required based on config options, plugins can\n * implement a static `getResourceRequirements(config)` method.\n *\n * At runtime, this method is called with the actual config to determine\n * which \"optional\" resources should be treated as \"required\".\n *\n * @example Basic plugin with static requirements\n * ```typescript\n * import { Plugin, toPlugin, PluginManifest, ResourceType } from '@databricks/appkit';\n *\n * const myManifest: PluginManifest = {\n * name: 'myPlugin',\n * displayName: 'My Plugin',\n * description: 'Does something awesome',\n * resources: {\n * required: [\n * { type: ResourceType.SQL_WAREHOUSE, alias: 'warehouse', ... }\n * ],\n * optional: []\n * }\n * };\n *\n * class MyPlugin extends Plugin<MyConfig> {\n * static manifest = myManifest;\n * }\n * ```\n *\n * @example Plugin with config-dependent resources\n * ```typescript\n * interface MyConfig extends BasePluginConfig {\n * enableCaching?: boolean;\n * }\n *\n * const myManifest: PluginManifest = {\n * name: 'myPlugin',\n * resources: {\n * required: [\n * { type: ResourceType.SQL_WAREHOUSE, alias: 'warehouse', ... }\n * ],\n * optional: [\n * // Database is optional in the static manifest\n * { type: ResourceType.DATABASE, alias: 'cache', description: 'Required if caching enabled', ... }\n * ]\n * }\n * };\n *\n * class MyPlugin extends Plugin<MyConfig> {\n * static manifest = myManifest<\"myPlugin\">;\n *\n * // Runtime method: converts optional resources to required based on config\n * static getResourceRequirements(config: MyConfig) {\n * const resources = [];\n * if (config.enableCaching) {\n * // When caching is enabled, Database becomes required\n * resources.push({\n * type: ResourceType.DATABASE,\n * alias: 'cache',\n * resourceKey: 'database',\n * description: 'Cache storage for query results',\n * permission: 'CAN_CONNECT_AND_CREATE',\n * fields: {\n * instance_name: { env: 'DATABRICKS_CACHE_INSTANCE' },\n * database_name: { env: 'DATABRICKS_CACHE_DB' },\n * },\n * required: true // Mark as required at runtime\n * });\n * }\n * return resources;\n * }\n * }\n * ```\n */\nexport abstract class Plugin<\n TConfig extends BasePluginConfig = BasePluginConfig,\n> implements BasePlugin {\n protected isReady = false;\n protected cache!: CacheManager;\n protected app: AppManager;\n protected devFileReader: DevFileReader;\n protected streamManager: StreamManager;\n protected telemetry!: ITelemetry;\n protected context?: PluginContext;\n\n /** Registered endpoints for this plugin */\n private registeredEndpoints: PluginEndpointMap = {};\n\n /** Paths that opt out of JSON body parsing (e.g. file upload routes) */\n private skipBodyParsingPaths: Set<string> = new Set();\n\n /**\n * Plugin initialization phase.\n * - 'core': Initialized first (e.g., config plugins)\n * - 'normal': Initialized second (most plugins)\n * - 'deferred': Initialized last (e.g., server plugin)\n */\n static phase: PluginPhase = \"normal\";\n\n /**\n * Plugin name identifier.\n */\n name: string;\n\n constructor(protected config: TConfig) {\n this.name =\n config.name ??\n (this.constructor as { manifest?: { name: string } }).manifest?.name ??\n \"plugin\";\n this.streamManager = new StreamManager(config.streamConfig);\n this.app = new AppManager();\n this.devFileReader = DevFileReader.getInstance();\n this.context = (config as Record<string, unknown>).context as\n | PluginContext\n | undefined;\n\n // Eagerly bind telemetry + cache if the core services have already been\n // initialized (normal createApp path, or tests that mock CacheManager).\n // If they haven't, we leave these undefined and rely on `attachContext`\n // being called later — this lets factories eagerly construct plugin\n // instances at module top-level before `createApp` has run.\n this.tryAttachContext();\n }\n\n private tryAttachContext(): void {\n try {\n this.cache = CacheManager.getInstanceSync();\n } catch {\n return;\n }\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.isReady = true;\n }\n\n /**\n * Binds runtime dependencies (telemetry provider, cache, plugin context) to\n * this plugin. Called by `AppKit._createApp` after construction and before\n * `setup()`. Idempotent: safe to call if the constructor already bound them\n * eagerly. Kept separate so factories can eagerly construct plugin instances\n * without running this before `TelemetryManager.initialize()` /\n * `CacheManager.getInstance()` have run.\n */\n attachContext(\n deps: {\n context?: unknown;\n telemetryConfig?: BasePluginConfig[\"telemetry\"];\n } = {},\n ): void {\n if (!this.cache) {\n this.cache = CacheManager.getInstanceSync();\n }\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n deps.telemetryConfig ?? this.config.telemetry,\n );\n if (deps.context !== undefined) {\n this.context = deps.context as PluginContext;\n }\n this.isReady = true;\n }\n\n injectRoutes(_: express.Router) {\n return;\n }\n\n async setup() {}\n\n getEndpoints(): PluginEndpointMap {\n return this.registeredEndpoints;\n }\n\n getSkipBodyParsingPaths(): ReadonlySet<string> {\n return this.skipBodyParsingPaths;\n }\n\n abortActiveOperations(): void {\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the public exports for this plugin.\n * Override this to define a custom public API.\n * By default, returns an empty object.\n *\n * The returned object becomes the plugin's public API on the AppKit instance\n * (e.g. `appkit.myPlugin.method()`). AppKit automatically binds method context\n * and adds `asUser(req)` for user-scoped execution.\n *\n * @example\n * ```ts\n * class MyPlugin extends Plugin {\n * private getData() { return []; }\n *\n * exports() {\n * return { getData: this.getData };\n * }\n * }\n *\n * // After registration:\n * const appkit = await createApp({ plugins: [myPlugin()] });\n * appkit.myPlugin.getData();\n * ```\n */\n exports(): unknown {\n return {};\n }\n\n /**\n * Returns startup config to expose to the client.\n * Override this to surface server-side values that are safe to publish to the\n * frontend, such as feature flags, resource IDs, or other app boot settings.\n *\n * This runs once when the server starts, so it should not depend on\n * request-scoped or user-specific state.\n *\n * String values that match non-public environment variables are redacted\n * unless you intentionally expose them via a matching `PUBLIC_APPKIT_` env var.\n *\n * Values must be JSON-serializable plain data (no functions, Dates, classes,\n * Maps, Sets, BigInts, or circular references).\n * By default returns an empty object (plugin contributes nothing to client config).\n *\n * On the client, read the config with the `usePluginClientConfig` hook\n * (React) or the `getPluginClientConfig` function (vanilla JS), both\n * from `@databricks/appkit-ui`.\n *\n * @example\n * ```ts\n * // Server — plugin definition\n * class MyPlugin extends Plugin<MyConfig> {\n * clientConfig() {\n * return {\n * warehouseId: this.config.warehouseId,\n * features: { darkMode: true },\n * };\n * }\n * }\n *\n * // Client — React component\n * import { usePluginClientConfig } from \"@databricks/appkit-ui/react\";\n *\n * interface MyPluginConfig { warehouseId: string; features: { darkMode: boolean } }\n *\n * const config = usePluginClientConfig<MyPluginConfig>(\"myPlugin\");\n * config.warehouseId; // \"abc-123\"\n *\n * // Client — vanilla JS\n * import { getPluginClientConfig } from \"@databricks/appkit-ui/js\";\n *\n * const config = getPluginClientConfig<MyPluginConfig>(\"myPlugin\");\n * ```\n */\n clientConfig(): Record<string, unknown> {\n return {};\n }\n\n /**\n * Resolve the effective user ID from a request.\n *\n * Returns the `x-forwarded-user` header when present. In development mode\n * (`NODE_ENV=development`) falls back to the current context user ID so\n * that callers outside an active `runInUserContext` scope still get a\n * consistent value.\n *\n * @throws AuthenticationError in production when no user header is present.\n */\n protected resolveUserId(req: express.Request): string {\n const userId = req.header(\"x-forwarded-user\")?.trim();\n if (userId) return userId;\n if (process.env.NODE_ENV === \"development\") return getCurrentUserId();\n throw AuthenticationError.missingUserId();\n }\n\n /**\n * Execute operations using the user's identity from the request.\n * Returns a proxy of this plugin where all method calls execute\n * with the user's Databricks credentials instead of the service principal.\n *\n * @param req - The Express request containing the user token in headers\n * @returns A proxied plugin instance that executes as the user\n * @throws AuthenticationError if user token is not available in request headers (production only).\n * In development mode (`NODE_ENV=development`), skips user impersonation instead of throwing.\n */\n asUser(req: express.Request): this {\n const token = req.header(\"x-forwarded-access-token\")?.trim();\n const userId = req.header(\"x-forwarded-user\")?.trim();\n const userEmail = req.header(\"x-forwarded-email\");\n const isDev = process.env.NODE_ENV === \"development\";\n\n // In local development, skip user impersonation since there's no user\n // token available. Mark execution as OBO dev fallback via OTel context\n // so telemetry can distinguish intended OBO calls from regular SP calls.\n if (!token && isDev) {\n logger.warn(\n \"asUser() called without user token in development mode. Skipping user impersonation.\",\n );\n\n return this._createAsUserProxy((fn) => (...args) => {\n const ctx = otelContext.active().setValue(DEV_OBO_FALLBACK_KEY, true);\n return otelContext.with(ctx, () => fn(...args));\n });\n }\n\n if (!token) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n\n if (!userId && !isDev) {\n throw AuthenticationError.missingUserId();\n }\n\n const effectiveUserId = userId || \"dev-user\";\n\n const userContext = ServiceContext.createUserContext(\n token,\n effectiveUserId,\n undefined,\n userEmail ?? undefined,\n );\n\n return this._createAsUserProxy(\n (fn) =>\n (...args) =>\n runInUserContext(userContext, () => fn(...args)),\n );\n }\n\n /**\n * Creates a proxy of `this` where every method call — and every function\n * in the result of `exports()` — runs inside `wrapCall`.\n *\n * `wrapCall` decides the per-call scope. Two strategies are used today:\n * - real OBO: fn => (...args) => runInUserContext(userContext, () => fn(...args))\n * - dev fallback: fn => (...args) => otelContext.with(DEV_OBO_FALLBACK_KEY=true, () => fn(...args))\n *\n * `exports` is intercepted because methods captured in the returned\n * exports object never re-enter the proxy's `get` trap. Wrapping them\n * here is the only way to make the user context follow function\n * references back out of the plugin.\n */\n private _createAsUserProxy(\n wrapCall: (\n fn: (...a: unknown[]) => unknown,\n ) => (...a: unknown[]) => unknown,\n ): this {\n return new Proxy(this, {\n get: (target, prop, receiver) => {\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value !== \"function\") return value;\n if (typeof prop === \"string\" && EXCLUDED_FROM_PROXY.has(prop))\n return value;\n\n if (prop === \"exports\") {\n return () => {\n const raw = (value as () => unknown).call(target);\n if (raw == null) return {};\n // Callable exports (e.g. files, jobs) manage per-call asUser\n // themselves; leave them untouched.\n if (typeof raw === \"function\") return raw;\n if (isPlainObject(raw)) {\n return wrapExportFunctions(raw, (fn) =>\n wrapCall(fn.bind(target)),\n );\n }\n return raw;\n };\n }\n\n const fn = (value as (...a: unknown[]) => unknown).bind(target);\n return wrapCall(fn);\n },\n }) as this;\n }\n\n // streaming execution with interceptors\n protected async executeStream<T>(\n res: IAppResponse,\n fn: StreamExecuteHandler<T>,\n options: StreamExecutionSettings,\n userKey?: string,\n ) {\n // destructure options\n const {\n stream: streamConfig,\n default: defaultConfig,\n user: userConfig,\n } = options;\n\n // build execution options\n const executeConfig = this._buildExecutionConfig({\n default: defaultConfig,\n user: userConfig,\n });\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const self = this;\n // capture the active OTel context (HTTP span) before entering the async generator,\n // where it would otherwise be lost across the async boundary\n const parentOtelContext = otelContext.active();\n\n // wrapper function to ensure it returns a generator\n const asyncWrapperFn = async function* (streamSignal?: AbortSignal) {\n // build execution context\n const context: InterceptorContext = {\n signal: streamSignal,\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n // build interceptors\n const interceptors = self._buildInterceptors(executeConfig);\n\n // wrap the function to ensure it returns a promise\n const wrappedFn = async () => {\n const result = await fn(context.signal);\n return result;\n };\n\n // execute the function with interceptors, restoring the parent OTel context\n // so telemetry spans are linked as children of the HTTP request span\n const result = await otelContext.with(parentOtelContext, () =>\n self._executeWithInterceptors(\n wrappedFn as (signal?: AbortSignal) => Promise<T>,\n interceptors,\n context,\n ),\n );\n\n // check if result is a generator\n if (self._checkIfGenerator(result)) {\n yield* result;\n } else {\n yield result;\n }\n };\n\n // stream the result to the client. The effective user key is forwarded\n // to the stream manager so that reconnections to existing streamIds are\n // bound to the original creator (prevents cross-user stream takeover via\n // guessed/leaked IDs).\n await this.streamManager.stream(\n res,\n asyncWrapperFn,\n streamConfig,\n effectiveUserKey,\n );\n }\n\n /**\n * Execute a function with the plugin's interceptor chain.\n *\n * Returns an {@link ExecutionResult} discriminated union:\n * - `{ ok: true, data: T }` on success\n * - `{ ok: false, status: number, message: string }` on failure\n *\n * Errors are never thrown — the method is production-safe.\n */\n protected async execute<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: PluginExecutionSettings,\n userKey?: string,\n ): Promise<ExecutionResult<T>> {\n const executeConfig = this._buildExecutionConfig(options);\n\n const interceptors = this._buildInterceptors(executeConfig);\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const context: InterceptorContext = {\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n try {\n const data = await this._executeWithInterceptors(\n fn,\n interceptors,\n context,\n );\n return { ok: true, data };\n } catch (error) {\n logger.error(\"Plugin execution failed\", { error, plugin: this.name });\n\n if (error instanceof AppKitError) {\n return {\n ok: false,\n status: error.statusCode,\n message: error.message,\n };\n }\n\n if (hasHttpStatusCode(error)) {\n const isDev = process.env.NODE_ENV !== \"production\";\n const isClientError = error.statusCode >= 400 && error.statusCode < 500;\n return {\n ok: false,\n status: error.statusCode,\n message: isDev || isClientError ? error.message : \"Server error\",\n };\n }\n\n const isDev = process.env.NODE_ENV !== \"production\";\n return {\n ok: false,\n status: 500,\n message:\n isDev && error instanceof Error ? error.message : \"Server error\",\n };\n }\n }\n\n protected registerEndpoint(name: string, path: string): void {\n this.registeredEndpoints[name] = path;\n }\n\n protected route<_TResponse>(\n router: express.Router,\n config: RouteConfig,\n ): void {\n const { name, method, path, handler } = config;\n\n router[method](path, forwardAsyncErrors(handler));\n\n const fullPath = `/api/${camelToKebab(this.name)}${path}`;\n this.registerEndpoint(name, fullPath);\n\n if (config.skipBodyParsing) {\n this.skipBodyParsingPaths.add(fullPath);\n }\n }\n\n // build execution options by merging defaults, plugin config, and user overrides\n private _buildExecutionConfig(\n options: PluginExecutionSettings,\n ): PluginExecuteConfig {\n const { default: methodDefaults, user: userOverride } = options;\n\n // Merge: method defaults <- plugin config <- user override (highest priority)\n return deepMerge(\n deepMerge(methodDefaults, this.config),\n userOverride ?? {},\n ) as PluginExecuteConfig;\n }\n\n // build interceptors based on execute options\n private _buildInterceptors(\n options: PluginExecuteConfig,\n ): ExecutionInterceptor[] {\n const interceptors: ExecutionInterceptor[] = [];\n\n // order matters: telemetry → timeout → retry → cache (innermost to outermost)\n\n const telemetryConfig = normalizeTelemetryOptions(this.config.telemetry);\n if (\n telemetryConfig.traces &&\n (options.telemetryInterceptor?.enabled ?? true)\n ) {\n interceptors.push(\n new TelemetryInterceptor(this.telemetry, options.telemetryInterceptor),\n );\n }\n\n if (options.timeout && options.timeout > 0) {\n interceptors.push(new TimeoutInterceptor(options.timeout));\n }\n\n if (\n options.retry?.enabled &&\n options.retry.attempts &&\n options.retry.attempts > 1\n ) {\n interceptors.push(new RetryInterceptor(options.retry));\n }\n\n if (options.cache?.enabled && options.cache.cacheKey?.length) {\n interceptors.push(new CacheInterceptor(this.cache, options.cache));\n }\n\n return interceptors;\n }\n\n // execute method wrapped with interceptors\n private async _executeWithInterceptors<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n interceptors: ExecutionInterceptor[],\n context: InterceptorContext,\n ): Promise<T> {\n // no interceptors, execute directly\n if (interceptors.length === 0) {\n return fn(context.signal);\n }\n // build nested execution chain from interceptors\n let wrappedFn = () => fn(context.signal);\n\n // wrap each interceptor around the previous function\n for (const interceptor of interceptors) {\n const previousFn = wrappedFn;\n wrappedFn = () => interceptor.intercept(previousFn, context);\n }\n\n return wrappedFn();\n }\n\n private _checkIfGenerator(\n result: any,\n ): result is AsyncGenerator<any, void, unknown> {\n return (\n result && typeof result === \"object\" && Symbol.asyncIterator in result\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { filterOperatorsForKind } from "../../../database/schema-builder/types.js";
|
|
2
|
-
import { DatabasePluginError
|
|
2
|
+
import { DatabasePluginError } from "../../../database/errors.js";
|
|
3
3
|
import { MAX_SERIALIZED_DEPTH, MAX_SERIALIZED_NODES } from "../defaults.js";
|
|
4
4
|
import { compileColumn } from "./codecs.js";
|
|
5
5
|
|
|
@@ -70,6 +70,13 @@ function sanitizeRow(table, row, depth, state) {
|
|
|
70
70
|
return out;
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
+
/** The budget a caller's own JSON has to fit, same as the one going back out. */
|
|
74
|
+
function boundedJson(value) {
|
|
75
|
+
return sanitizeJson(value, 0, {
|
|
76
|
+
nodes: 0,
|
|
77
|
+
ancestors: /* @__PURE__ */ new Set()
|
|
78
|
+
});
|
|
79
|
+
}
|
|
73
80
|
/** Project an included row through its own table; absent to-one reads null. */
|
|
74
81
|
function projectRelation(target, value) {
|
|
75
82
|
if (value === null || value === void 0) return null;
|
|
@@ -94,6 +101,8 @@ function compileTable(table) {
|
|
|
94
101
|
const columns = /* @__PURE__ */ new Map();
|
|
95
102
|
const selectable = /* @__PURE__ */ new Set();
|
|
96
103
|
const queryable = /* @__PURE__ */ new Set();
|
|
104
|
+
const creatable = /* @__PURE__ */ new Set();
|
|
105
|
+
const updatable = /* @__PURE__ */ new Set();
|
|
97
106
|
let primaryKey;
|
|
98
107
|
for (const meta of Object.values(table.$columns)) {
|
|
99
108
|
const column = compileColumn(meta);
|
|
@@ -102,6 +111,10 @@ function compileTable(table) {
|
|
|
102
111
|
if (meta.isPrivate) continue;
|
|
103
112
|
selectable.add(meta.columnName);
|
|
104
113
|
if (filterOperatorsForKind(meta.kind).length > 0) queryable.add(meta.columnName);
|
|
114
|
+
if (meta.serverGenerated || meta.primaryKey && meta.defaultRandom) continue;
|
|
115
|
+
creatable.add(meta.columnName);
|
|
116
|
+
if (meta.primaryKey || meta.defaultNow || meta.defaultRandom) continue;
|
|
117
|
+
updatable.add(meta.columnName);
|
|
105
118
|
}
|
|
106
119
|
const compiled = {
|
|
107
120
|
name: table.$name,
|
|
@@ -109,13 +122,9 @@ function compileTable(table) {
|
|
|
109
122
|
columns,
|
|
110
123
|
selectable,
|
|
111
124
|
queryable,
|
|
125
|
+
creatable,
|
|
126
|
+
updatable,
|
|
112
127
|
relations: /* @__PURE__ */ new Map(),
|
|
113
|
-
decodeId: (raw) => {
|
|
114
|
-
if (!primaryKey) throw new DatabasePluginError("INTERNAL", "read");
|
|
115
|
-
const value = primaryKey.decode(raw);
|
|
116
|
-
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint") throw invalidDatabaseInput(["id"], "Not a valid identifier");
|
|
117
|
-
return value;
|
|
118
|
-
},
|
|
119
128
|
projectPublicRow: (row) => projectRow(compiled, row),
|
|
120
129
|
sanitizeSerializedRow: (row) => sanitizeRow(compiled, row, 0, {
|
|
121
130
|
nodes: 0,
|
|
@@ -147,5 +156,5 @@ function compileCrudTables(tables) {
|
|
|
147
156
|
}
|
|
148
157
|
|
|
149
158
|
//#endregion
|
|
150
|
-
export { compileCrudTables };
|
|
159
|
+
export { boundedJson, compileCrudTables, isPlainObject };
|
|
151
160
|
//# sourceMappingURL=contract.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.js","names":[],"sources":["../../../../src/plugins/database/crud/contract.ts"],"sourcesContent":["import {\n DatabasePluginError,\n invalidDatabaseInput,\n} from \"../../../database/errors\";\nimport type { IdValue, Row } from \"../../../database/runtime\";\nimport type { AppKitTable } from \"../../../database/schema-builder\";\nimport { filterOperatorsForKind } from \"../../../database/schema-builder/types\";\nimport { MAX_SERIALIZED_DEPTH, MAX_SERIALIZED_NODES } from \"../defaults\";\nimport { type CompiledColumn, compileColumn, type JsonValue } from \"./codecs\";\n\n/** One relation edge wired to the contract of its target table. */\nexport interface CrudRelation {\n readonly cardinality: \"toOne\" | \"toMany\";\n readonly target: CrudTable;\n}\n\n/** Private HTTP contract compiled once for one explicitly exposed table. */\nexport interface CrudTable {\n readonly name: string;\n readonly primaryKey?: CompiledColumn;\n readonly columns: ReadonlyMap<string, CompiledColumn>;\n /** Public columns a request may project. */\n readonly selectable: ReadonlySet<string>;\n /** Public columns a request may filter or order by. */\n readonly queryable: ReadonlySet<string>;\n readonly relations: ReadonlyMap<string, CrudRelation>;\n decodeId(raw: string): IdValue;\n projectPublicRow(row: Row): JsonValue;\n sanitizeSerializedRow(row: unknown): JsonValue;\n}\n\ntype MutableCrudTable = Omit<CrudTable, \"relations\"> & {\n readonly relations: Map<string, CrudRelation>;\n};\n\ninterface SanitizeState {\n nodes: number;\n readonly ancestors: Set<object>;\n}\n\n/** A bare object literal; a `Date`, class instance, or `Map` is not JSON. */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** A serializer that breaks its contract is trusted code failing, not input. */\nfunction serializerFault(): never {\n throw new DatabasePluginError(\"INTERNAL\", \"read\");\n}\n\n/** Charge one value against the output budget before descending into it. */\nfunction countNode(depth: number, state: SanitizeState): void {\n state.nodes += 1;\n if (state.nodes > MAX_SERIALIZED_NODES || depth > MAX_SERIALIZED_DEPTH) {\n serializerFault();\n }\n}\n\n/** Walk a container while its ancestors are tracked, so a cycle cannot pass. */\nfunction enterObject<T>(\n value: object,\n state: SanitizeState,\n visit: () => T,\n): T {\n if (state.ancestors.has(value)) serializerFault();\n state.ancestors.add(value);\n try {\n return visit();\n } finally {\n state.ancestors.delete(value);\n }\n}\n\n/** Accept a serializer's own added value only where it is already JSON. */\nfunction sanitizeJson(\n value: unknown,\n depth: number,\n state: SanitizeState,\n): JsonValue {\n countNode(depth, state);\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) serializerFault();\n return value;\n }\n if (Array.isArray(value)) {\n return enterObject(value, state, () =>\n value.map((item) => sanitizeJson(item, depth + 1, state)),\n );\n }\n if (!isPlainObject(value)) serializerFault();\n return enterObject(value, state, () => {\n // A null prototype keeps `__proto__` an ordinary key instead of a setter.\n const out: Record<string, JsonValue> = Object.create(null);\n for (const [key, child] of Object.entries(value)) {\n if (child === undefined) continue;\n out[key] = sanitizeJson(child, depth + 1, state);\n }\n return out;\n });\n}\n\n/** Keep an included row under its own table's policy, one row or many. */\nfunction sanitizeRelation(\n target: CrudTable,\n value: unknown,\n depth: number,\n state: SanitizeState,\n): JsonValue {\n if (value === null) return null;\n if (!Array.isArray(value)) return sanitizeRow(target, value, depth, state);\n countNode(depth, state);\n return enterObject(value, state, () =>\n value.map((row) => sanitizeRow(target, row, depth + 1, state)),\n );\n}\n\n/** Re-apply the private-column policy wherever the output stays contracted. */\nfunction sanitizeRow(\n table: CrudTable,\n row: unknown,\n depth: number,\n state: SanitizeState,\n): JsonValue {\n countNode(depth, state);\n if (!isPlainObject(row)) serializerFault();\n return enterObject(row, state, () => {\n const out: Record<string, JsonValue> = {};\n for (const [key, child] of Object.entries(row)) {\n if (child === undefined) continue;\n if (table.columns.get(key)?.meta.isPrivate) continue;\n const relation = table.relations.get(key);\n out[key] = relation\n ? sanitizeRelation(relation.target, child, depth + 1, state)\n : sanitizeJson(child, depth + 1, state);\n }\n return out;\n });\n}\n\n/** Project an included row through its own table; absent to-one reads null. */\nfunction projectRelation(target: CrudTable, value: unknown): JsonValue {\n if (value === null || value === undefined) return null;\n return Array.isArray(value)\n ? value.map((row) => target.projectPublicRow(row as Row))\n : target.projectPublicRow(value as Row);\n}\n\n/** Build the public JSON for one driver row, dropping anything uncontracted. */\nfunction projectRow(table: CrudTable, row: Row): JsonValue {\n const out: Record<string, JsonValue> = {};\n for (const [key, value] of Object.entries(row)) {\n const column = table.columns.get(key);\n if (column) {\n // Whatever the driver returned, only public contracted columns ship.\n if (!column.meta.isPrivate) out[key] = column.encode(value);\n continue;\n }\n const relation = table.relations.get(key);\n if (relation) out[key] = projectRelation(relation.target, value);\n }\n return out;\n}\n\n/** Compile one table's allowlists and codecs from its finalized metadata. */\nfunction compileTable(table: AppKitTable): MutableCrudTable {\n const columns = new Map<string, CompiledColumn>();\n const selectable = new Set<string>();\n const queryable = new Set<string>();\n let primaryKey: CompiledColumn | undefined;\n\n for (const meta of Object.values(table.$columns)) {\n const column = compileColumn(meta);\n columns.set(meta.columnName, column);\n // A private key must not power `GET /:table/:id`: per-id probing would\n // answer 200 or 404 on an identifier the schema hides, so over HTTP the\n // table is keyless — no detail route, and lists must name their own order.\n if (meta.primaryKey && !meta.isPrivate) primaryKey = column;\n if (meta.isPrivate) continue;\n selectable.add(meta.columnName);\n if (filterOperatorsForKind(meta.kind).length > 0) {\n queryable.add(meta.columnName);\n }\n }\n\n const compiled: MutableCrudTable = {\n name: table.$name,\n primaryKey,\n columns,\n selectable,\n queryable,\n relations: new Map(),\n decodeId: (raw) => {\n if (!primaryKey) throw new DatabasePluginError(\"INTERNAL\", \"read\");\n const value = primaryKey.decode(raw);\n if (\n typeof value !== \"string\" &&\n typeof value !== \"number\" &&\n typeof value !== \"bigint\"\n ) {\n throw invalidDatabaseInput([\"id\"], \"Not a valid identifier\");\n }\n return value;\n },\n projectPublicRow: (row) => projectRow(compiled, row),\n sanitizeSerializedRow: (row) =>\n sanitizeRow(compiled, row, 0, { nodes: 0, ancestors: new Set() }),\n };\n return compiled;\n}\n\n/**\n * Compile the HTTP contract for every exposed table and wire the relations\n * they share. A relation whose target is not exposed stays unreachable, so\n * enabling one table never widens another table's public surface.\n */\nexport function compileCrudTables(\n tables: Record<string, AppKitTable>,\n): Map<string, CrudTable> {\n const compiled = new Map<string, MutableCrudTable>();\n for (const table of Object.values(tables)) {\n compiled.set(table.$name, compileTable(table));\n }\n for (const table of Object.values(tables)) {\n const entry = compiled.get(table.$name);\n for (const relation of table.$relations) {\n const target = compiled.get(relation.targetTable);\n if (!entry || !target) continue;\n entry.relations.set(relation.name, {\n cardinality: relation.cardinality,\n target,\n });\n }\n }\n return compiled as Map<string, CrudTable>;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"contract.js","names":[],"sources":["../../../../src/plugins/database/crud/contract.ts"],"sourcesContent":["import { DatabasePluginError } from \"../../../database/errors\";\nimport type { Row } from \"../../../database/runtime\";\nimport type { AppKitTable } from \"../../../database/schema-builder\";\nimport { filterOperatorsForKind } from \"../../../database/schema-builder/types\";\nimport { MAX_SERIALIZED_DEPTH, MAX_SERIALIZED_NODES } from \"../defaults\";\nimport { type CompiledColumn, compileColumn, type JsonValue } from \"./codecs\";\n\n/** One relation edge wired to the contract of its target table. */\nexport interface CrudRelation {\n readonly cardinality: \"toOne\" | \"toMany\";\n readonly target: CrudTable;\n}\n\n/** Private HTTP contract compiled once for one exposed table. */\nexport interface CrudTable {\n readonly name: string;\n readonly primaryKey?: CompiledColumn;\n readonly columns: ReadonlyMap<string, CompiledColumn>;\n /** Public columns a request may project. */\n readonly selectable: ReadonlySet<string>;\n /** Public columns a request may filter or order by. */\n readonly queryable: ReadonlySet<string>;\n /** Public columns a create body may set, including a caller-chosen key. */\n readonly creatable: ReadonlySet<string>;\n /** Public columns an update body may set; a key or a stamp is never one. */\n readonly updatable: ReadonlySet<string>;\n readonly relations: ReadonlyMap<string, CrudRelation>;\n projectPublicRow(row: Row): JsonValue;\n sanitizeSerializedRow(row: unknown): JsonValue;\n}\n\ntype MutableCrudTable = Omit<CrudTable, \"relations\"> & {\n readonly relations: Map<string, CrudRelation>;\n};\n\ninterface SanitizeState {\n nodes: number;\n readonly ancestors: Set<object>;\n}\n\n/** A bare object literal; a `Date`, class instance, or `Map` is not JSON. */\nexport function isPlainObject(\n value: unknown,\n): value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** A serializer that breaks its contract is trusted code failing, not input. */\nfunction serializerFault(): never {\n throw new DatabasePluginError(\"INTERNAL\", \"read\");\n}\n\n/** Charge one value against the output budget before descending into it. */\nfunction countNode(depth: number, state: SanitizeState): void {\n state.nodes += 1;\n if (state.nodes > MAX_SERIALIZED_NODES || depth > MAX_SERIALIZED_DEPTH) {\n serializerFault();\n }\n}\n\n/** Walk a container while its ancestors are tracked, so a cycle cannot pass. */\nfunction enterObject<T>(\n value: object,\n state: SanitizeState,\n visit: () => T,\n): T {\n if (state.ancestors.has(value)) serializerFault();\n state.ancestors.add(value);\n try {\n return visit();\n } finally {\n state.ancestors.delete(value);\n }\n}\n\n/** Accept a serializer's own added value only where it is already JSON. */\nfunction sanitizeJson(\n value: unknown,\n depth: number,\n state: SanitizeState,\n): JsonValue {\n countNode(depth, state);\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) serializerFault();\n return value;\n }\n if (Array.isArray(value)) {\n return enterObject(value, state, () =>\n value.map((item) => sanitizeJson(item, depth + 1, state)),\n );\n }\n if (!isPlainObject(value)) serializerFault();\n return enterObject(value, state, () => {\n // A null prototype keeps `__proto__` an ordinary key instead of a setter.\n const out: Record<string, JsonValue> = Object.create(null);\n for (const [key, child] of Object.entries(value)) {\n if (child === undefined) continue;\n out[key] = sanitizeJson(child, depth + 1, state);\n }\n return out;\n });\n}\n\n/** Keep an included row under its own table's policy, one row or many. */\nfunction sanitizeRelation(\n target: CrudTable,\n value: unknown,\n depth: number,\n state: SanitizeState,\n): JsonValue {\n if (value === null) return null;\n if (!Array.isArray(value)) return sanitizeRow(target, value, depth, state);\n countNode(depth, state);\n return enterObject(value, state, () =>\n value.map((row) => sanitizeRow(target, row, depth + 1, state)),\n );\n}\n\n/** Re-apply the private-column policy wherever the output stays contracted. */\nfunction sanitizeRow(\n table: CrudTable,\n row: unknown,\n depth: number,\n state: SanitizeState,\n): JsonValue {\n countNode(depth, state);\n if (!isPlainObject(row)) serializerFault();\n return enterObject(row, state, () => {\n const out: Record<string, JsonValue> = {};\n for (const [key, child] of Object.entries(row)) {\n if (child === undefined) continue;\n if (table.columns.get(key)?.meta.isPrivate) continue;\n const relation = table.relations.get(key);\n out[key] = relation\n ? sanitizeRelation(relation.target, child, depth + 1, state)\n : sanitizeJson(child, depth + 1, state);\n }\n return out;\n });\n}\n\n/** The budget a caller's own JSON has to fit, same as the one going back out. */\nexport function boundedJson(value: unknown): JsonValue {\n return sanitizeJson(value, 0, { nodes: 0, ancestors: new Set() });\n}\n\n/** Project an included row through its own table; absent to-one reads null. */\nfunction projectRelation(target: CrudTable, value: unknown): JsonValue {\n if (value === null || value === undefined) return null;\n return Array.isArray(value)\n ? value.map((row) => target.projectPublicRow(row as Row))\n : target.projectPublicRow(value as Row);\n}\n\n/** Build the public JSON for one driver row, dropping anything uncontracted. */\nfunction projectRow(table: CrudTable, row: Row): JsonValue {\n const out: Record<string, JsonValue> = {};\n for (const [key, value] of Object.entries(row)) {\n const column = table.columns.get(key);\n if (column) {\n // Whatever the driver returned, only public contracted columns ship.\n if (!column.meta.isPrivate) out[key] = column.encode(value);\n continue;\n }\n const relation = table.relations.get(key);\n if (relation) out[key] = projectRelation(relation.target, value);\n }\n return out;\n}\n\n/** Compile one table's allowlists and codecs from its finalized metadata. */\nfunction compileTable(table: AppKitTable): MutableCrudTable {\n const columns = new Map<string, CompiledColumn>();\n const selectable = new Set<string>();\n const queryable = new Set<string>();\n const creatable = new Set<string>();\n const updatable = new Set<string>();\n let primaryKey: CompiledColumn | undefined;\n\n for (const meta of Object.values(table.$columns)) {\n const column = compileColumn(meta);\n columns.set(meta.columnName, column);\n // A private key must not power `GET /:table/:id`: per-id probing would\n // answer 200 or 404 on an identifier the schema hides, so over HTTP the\n // table is keyless — no detail route, and lists must name their own order.\n if (meta.primaryKey && !meta.isPrivate) primaryKey = column;\n if (meta.isPrivate) continue;\n selectable.add(meta.columnName);\n if (filterOperatorsForKind(meta.kind).length > 0) {\n queryable.add(meta.columnName);\n }\n // Database-generated identities belong to the server, never the caller.\n if (meta.serverGenerated || (meta.primaryKey && meta.defaultRandom))\n continue;\n creatable.add(meta.columnName);\n // Rewriting a key would move a row out from under every existing reference,\n // and rewriting a database-materialized stamp would rewrite history.\n if (meta.primaryKey || meta.defaultNow || meta.defaultRandom) continue;\n updatable.add(meta.columnName);\n }\n\n const compiled: MutableCrudTable = {\n name: table.$name,\n primaryKey,\n columns,\n selectable,\n queryable,\n creatable,\n updatable,\n relations: new Map(),\n projectPublicRow: (row) => projectRow(compiled, row),\n sanitizeSerializedRow: (row) =>\n sanitizeRow(compiled, row, 0, { nodes: 0, ancestors: new Set() }),\n };\n return compiled;\n}\n\n/**\n * Compile the HTTP contract for every exposed table and wire the relations\n * they share. A relation whose target is not exposed stays unreachable, so\n * enabling one table never widens another table's public surface.\n */\nexport function compileCrudTables(\n tables: Record<string, AppKitTable>,\n): Map<string, CrudTable> {\n const compiled = new Map<string, MutableCrudTable>();\n for (const table of Object.values(tables)) {\n compiled.set(table.$name, compileTable(table));\n }\n for (const table of Object.values(tables)) {\n const entry = compiled.get(table.$name);\n for (const relation of table.$relations) {\n const target = compiled.get(relation.targetTable);\n if (!entry || !target) continue;\n entry.relations.set(relation.name, {\n cardinality: relation.cardinality,\n target,\n });\n }\n }\n return compiled as Map<string, CrudTable>;\n}\n"],"mappings":"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"}
|