@databricks/appkit 0.82.0 → 0.83.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 +6 -0
- package/dist/appkit/package.js +1 -1
- package/dist/connectors/lakebase/index.js +1 -1
- package/dist/connectors/lakebase/routing-pool.d.ts +1 -1
- package/dist/connectors/lakebase/routing-pool.js +3 -3
- package/dist/connectors/lakebase/routing-pool.js.map +1 -1
- package/dist/context/caller-context.d.ts +22 -0
- package/dist/context/caller-context.d.ts.map +1 -0
- package/dist/context/caller-context.js +21 -0
- package/dist/context/caller-context.js.map +1 -0
- package/dist/context/deprecation.js +14 -0
- package/dist/context/deprecation.js.map +1 -0
- package/dist/context/execution-context.d.ts +12 -4
- package/dist/context/execution-context.d.ts.map +1 -1
- package/dist/context/execution-context.js +36 -34
- package/dist/context/execution-context.js.map +1 -1
- package/dist/context/index.d.ts +3 -2
- package/dist/context/index.js +2 -1
- package/dist/context/service-context.d.ts +18 -12
- package/dist/context/service-context.d.ts.map +1 -1
- package/dist/context/service-context.js +44 -54
- package/dist/context/service-context.js.map +1 -1
- package/dist/context/user-context.d.ts +5 -8
- package/dist/context/user-context.d.ts.map +1 -1
- package/dist/context/user-context.js +33 -6
- package/dist/context/user-context.js.map +1 -1
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +4 -2
- package/dist/core/appkit.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +4 -2
- package/dist/plugin/plugin.d.ts +2 -2
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +6 -6
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugins/agents/agents.d.ts +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts +1 -1
- package/dist/plugins/analytics/analytics.d.ts +1 -1
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js +3 -1
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/database/database.d.ts +1 -1
- package/dist/plugins/files/plugin.d.ts +11 -11
- package/dist/plugins/files/plugin.js +17 -17
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/genie/genie.d.ts +1 -1
- package/dist/plugins/jobs/plugin.d.ts +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js +4 -4
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/plugins/server/index.d.ts +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/plugins/serving/serving.d.ts +1 -1
- package/dist/resources/index.d.ts +1 -0
- package/dist/resources/index.js +3 -0
- package/dist/resources/warehouse.d.ts +16 -0
- package/dist/resources/warehouse.d.ts.map +1 -0
- package/dist/resources/warehouse.js +93 -0
- package/dist/resources/warehouse.js.map +1 -0
- package/dist/schemas/manifest.d.ts +9 -0
- package/dist/schemas/manifest.d.ts.map +1 -1
- package/dist/schemas/manifest.js +11 -0
- package/dist/schemas/manifest.js.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts +42 -33
- package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
- package/dist/shared/src/schemas/manifest.js +11 -0
- package/dist/shared/src/schemas/manifest.js.map +1 -1
- package/dist/testing/create-test-app.js +2 -2
- package/dist/testing/create-test-app.js.map +1 -1
- package/dist/testing/fixtures.d.ts +3 -2
- package/dist/testing/fixtures.d.ts.map +1 -1
- package/dist/testing/fixtures.js +21 -14
- package/dist/testing/fixtures.js.map +1 -1
- package/dist/testing/reset-singletons.js +1 -1
- 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.Plugin.md +1 -1
- package/docs/api/appkit/Function.getCurrentActorId.md +12 -0
- package/docs/api/appkit/Function.getCurrentPrincipalKey.md +12 -0
- package/docs/api/appkit/Function.getExecutionContext.md +5 -3
- package/docs/api/appkit/Function.getWarehouseId.md +20 -0
- package/docs/api/appkit/Interface.CallerContext.md +41 -0
- package/docs/api/appkit/Interface.PluginManifest.md +25 -2
- package/docs/api/appkit/TypeAlias.CallerPrincipal.md +13 -0
- package/docs/api/appkit/TypeAlias.ExecutionContext.md +6 -0
- package/docs/api/appkit.md +6 -0
- package/docs/plugins/analytics.md +8 -0
- package/llms.txt +6 -0
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixtures.js","names":[],"sources":["../../src/testing/fixtures.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\n\nimport type { Span, SpanOptions } from \"@opentelemetry/api\";\nimport type { IAppRouter } from \"shared\";\nimport { afterEach, beforeEach, vi } from \"vitest\";\n\nimport { CacheManager } from \"../cache\";\nimport type { ServiceContextState } from \"../context/service-context\";\nimport { ServiceContext } from \"../context/service-context\";\nimport { AuthenticationError } from \"../errors\";\nimport type { InstrumentConfig, ITelemetry } from \"../telemetry/types\";\nimport { ApiError } from \"../workspace-client\";\nimport { createMockWorkspaceClient } from \"./mock-workspace-client\";\n\n// Test fixtures intentionally use loose shapes; `noExplicitAny` is disabled\n// repo-wide (see .oxlintrc.json), so a local alias keeps the intent readable.\ntype Any = any;\n\n/**\n * Creates a mock telemetry provider for testing. Every span/meter/logger is a\n * `vi.fn()` no-op, so plugins that trace, count, or log run without a live\n * OpenTelemetry pipeline. Passed into {@link createTestPluginContext} as the one\n * injectable production seam.\n */\nexport function createMockTelemetry(): ITelemetry {\n const mockSpan: Span = {\n addLink: vi.fn(),\n addLinks: vi.fn(),\n end: vi.fn(),\n setAttribute: vi.fn(),\n setAttributes: vi.fn(),\n setStatus: vi.fn(),\n recordException: vi.fn(),\n updateName: vi.fn(),\n addEvent: vi.fn(),\n isRecording: vi.fn().mockReturnValue(false),\n spanContext: vi.fn(),\n };\n\n return {\n getTracer: vi.fn().mockReturnValue({\n startActiveSpan: vi.fn().mockImplementation((...args: Any[]) => {\n const fn = args[args.length - 1];\n if (typeof fn === \"function\") {\n return fn(mockSpan);\n }\n return undefined;\n }),\n }),\n getMeter: vi.fn().mockReturnValue({\n createCounter: vi.fn().mockReturnValue({ add: vi.fn() }),\n createHistogram: vi.fn().mockReturnValue({ record: vi.fn() }),\n }),\n getLogger: vi.fn().mockReturnValue({\n emit: vi.fn(),\n }),\n emit: vi.fn(),\n startActiveSpan: vi\n .fn()\n .mockImplementation(\n async (\n _name: string,\n _options: SpanOptions,\n fn: (span: Span) => Promise<Any>,\n _tracerOptions?: InstrumentConfig,\n ) => {\n return await fn(mockSpan);\n },\n ),\n registerInstrumentations: vi.fn(),\n };\n}\n\n/**\n * Creates a mock Express router that captures registered handlers so a test\n * can pull a handler back out by method + path and invoke it directly.\n */\nexport function createMockRouter(): {\n router: IAppRouter;\n handlers: Record<string, Any>;\n getHandler: (method: string, path: string) => Any;\n} {\n const handlers: Record<string, Any> = {};\n\n const mockRouter = {\n get: vi.fn((path: string, handler: Any) => {\n handlers[`GET:${path}`] = handler;\n }),\n post: vi.fn((path: string, handler: Any) => {\n handlers[`POST:${path}`] = handler;\n }),\n put: vi.fn((path: string, handler: Any) => {\n handlers[`PUT:${path}`] = handler;\n }),\n delete: vi.fn((path: string, handler: Any) => {\n handlers[`DELETE:${path}`] = handler;\n }),\n patch: vi.fn((path: string, handler: Any) => {\n handlers[`PATCH:${path}`] = handler;\n }),\n } as unknown as IAppRouter;\n\n return {\n router: mockRouter,\n handlers,\n getHandler: (method: string, path: string) =>\n handlers[`${method.toUpperCase()}:${path}`],\n };\n}\n\n/**\n * On-behalf-of shorthand for {@link createMockRequest}. `true` uses the default\n * test user; an object picks the identity. Sets the forwarded headers the real\n * `Plugin.asUser` reads (`x-forwarded-access-token`, `x-forwarded-user`, and —\n * when given — `x-forwarded-email`), so an OBO test is one flag instead of\n * hand-rolled headers.\n */\nexport type OboOption =\n | boolean\n | {\n /** `x-forwarded-user` — defaults to `\"test-user\"`. */\n userId?: string;\n /** `x-forwarded-access-token` — defaults to `\"test-user-token\"`. */\n token?: string;\n /** `x-forwarded-email` — omitted unless provided. */\n email?: string;\n };\n\n/**\n * The one fake of `ServiceContext.createUserContext` this kit uses, shared by\n * `mockServiceContext` and `createTestApp`.\n *\n * Shared rather than duplicated because the two used to disagree, and neither\n * matched production: a missing token went unrejected and `tokenFingerprint`\n * was absent, which silently disables Lakebase pool rotation — `pool-manager`\n * treats a missing fingerprint as \"not stale\", so the drain-and-recreate branch\n * could never run under a fake.\n *\n * @internal\n */\nexport function fakeUserContext(\n client: Any,\n ids: { warehouseId?: Any; workspaceId: Any },\n) {\n return (\n token: string,\n userId: string,\n userName?: string,\n userEmail?: string,\n ): Any => {\n // Same rejection as production, so a path that forgets to forward the token\n // fails here instead of only in a deployed app.\n if (!token) throw AuthenticationError.missingToken(\"user token\");\n return {\n client,\n userId,\n userName,\n userEmail,\n // Derived from the token exactly as production does. Keyed on the user it\n // would be constant across tokens, and rotation compares this value.\n tokenFingerprint: createHash(\"sha256\")\n .update(token)\n .digest(\"hex\")\n .slice(0, 16),\n warehouseId: ids.warehouseId,\n workspaceId: ids.workspaceId,\n isUserContext: true,\n };\n };\n}\n\n/**\n * Build the forwarded identity headers an `obo` option implies.\n *\n * Exported so `createTestApp`'s request methods use the same convention as\n * `createMockRequest` rather than a second one.\n *\n * @internal\n */\nexport function oboHeaders(\n obo: Exclude<OboOption, false>,\n): Record<string, string> {\n const opts = obo === true ? {} : obo;\n const headers: Record<string, string> = {\n \"x-forwarded-access-token\": opts.token ?? \"test-user-token\",\n \"x-forwarded-user\": opts.userId ?? \"test-user\",\n };\n if (opts.email) headers[\"x-forwarded-email\"] = opts.email;\n return headers;\n}\n\n/**\n * Creates a mock Express request. Pass `overrides` to set `params`, `query`,\n * `body`, `headers`, etc.\n *\n * For on-behalf-of tests, pass `obo` instead of hand-adding forwarded headers —\n * `createMockRequest({ obo: true })` sets the identity headers the real\n * `asUser` requires. Any explicit `headers` you also pass win over the ones\n * `obo` generates, so you can override a single field.\n *\n * @example\n * ```ts\n * createMockRequest({ obo: true }); // default test user + token\n * createMockRequest({ obo: { userId: \"alice\" } }); // pick the user\n * ```\n */\nexport function createMockRequest(overrides: Any = {}) {\n const { obo, headers: headerOverrides, ...rest } = overrides;\n\n // `obo` seeds the forwarded identity headers; an explicit `headers` override\n // still wins (merged last) so a test can tweak or drop a single field.\n //\n // Keys are lowercased on the way in, the way Node's parser gives them to\n // Express. `header()` looks up lowercased, so storing as-given would file a\n // mixed-case override under a second key and leave the obo one winning.\n const headers: Record<string, unknown> = Object.fromEntries(\n Object.entries({\n ...(obo ? oboHeaders(obo) : {}),\n ...headerOverrides,\n }).map(([name, value]) => [name.toLowerCase(), value]),\n );\n\n const req = {\n params: {},\n query: {},\n body: {},\n header: function (name: string) {\n return this.headers[name.toLowerCase()];\n },\n // `...rest` keeps the original override power over every default above;\n // `headers` is applied last as the one managed field (obo + overrides).\n ...rest,\n headers,\n };\n return req;\n}\n\n/**\n * Creates a mock Express response object. `write`/`send`/`setHeader` flip\n * `headersSent`, `end` flips `writableEnded` and fires any `close` listener —\n * enough for streaming handlers that branch on those flags.\n *\n * Every chunk passed to `write` (and a final chunk to `end`) is captured, so a\n * streaming route's real SSE output can be replayed: pass the response straight\n * to {@link expectStream}, or call `sseResponse()` for a real `Response`.\n *\n * @example Assert what a streaming route emitted\n * ```ts\n * const res = createMockResponse();\n * await plugin._handleStream(req, res);\n * await expectStream(res).toEmit(\"status\", \"result\");\n * ```\n */\nexport function createMockResponse() {\n const eventListeners: Record<string, Array<(...args: Any[]) => void>> = {};\n const chunks: string[] = [];\n\n const res = {\n // Flips to true once headers/body have gone out — mirrors Express so\n // streaming handlers can branch between a JSON error (pre-headers) and\n // aborting the socket (mid-stream).\n headersSent: false,\n status: vi.fn().mockReturnThis(),\n json: vi.fn().mockReturnThis(),\n send: vi.fn(function (this: Any) {\n this.headersSent = true;\n return this;\n }),\n sendStatus: vi.fn().mockReturnThis(),\n end: vi.fn(function (this: Any, chunk?: unknown) {\n // Express allows `end(chunk)` and `end(callback)`; capture only a data\n // chunk, never the completion callback.\n if (chunk != null && typeof chunk !== \"function\") {\n chunks.push(String(chunk));\n }\n this.writableEnded = true;\n if (eventListeners.close) {\n for (const handler of eventListeners.close) {\n handler();\n }\n }\n return this;\n }),\n write: vi.fn(function (this: Any, chunk?: unknown) {\n this.headersSent = true;\n if (chunk != null) chunks.push(String(chunk));\n // Return `this` (truthy) rather than a boolean: handlers that gate on\n // backpressure (`if (res.write(buf)) …`) then take the no-wait path.\n return this;\n }),\n setHeader: vi.fn(function (this: Any) {\n this.headersSent = true;\n return this;\n }),\n flushHeaders: vi.fn().mockReturnThis(),\n destroy: vi.fn().mockReturnThis(),\n on: vi.fn(function (\n this: Any,\n event: string,\n handler: (...args: Any[]) => void,\n ) {\n if (!eventListeners[event]) {\n eventListeners[event] = [];\n }\n eventListeners[event].push(handler);\n return this;\n }),\n off: vi.fn(function (\n this: Any,\n event: string,\n handler: (...args: Any[]) => void,\n ) {\n if (eventListeners[event]) {\n eventListeners[event] = eventListeners[event].filter(\n (h) => h !== handler,\n );\n }\n return this;\n }),\n writableEnded: false,\n /**\n * The SSE body captured so far, as a real `Response` — the bridge from a\n * `res.write`-based handler into {@link expectStream}. `expectStream`\n * detects this method and calls it for you, so `expectStream(res)` and\n * `expectStream(res.sseResponse())` are equivalent.\n */\n sseResponse(): Response {\n return new Response(chunks.join(\"\"));\n },\n };\n return res;\n}\n\n/**\n * Sets up common environment variables for Databricks testing so code that\n * reads `DATABRICKS_HOST` / `DATABRICKS_WAREHOUSE_ID` finds test values.\n */\nexport function setupDatabricksEnv(overrides: Record<string, string> = {}) {\n process.env.DATABRICKS_HOST = \"https://test.databricks.com\";\n process.env.DATABRICKS_WAREHOUSE_ID = \"test-warehouse-id\";\n Object.assign(process.env, overrides);\n}\n\n/**\n * Set environment variables and return a function that restores each key to its\n * prior state — the prior value, or a delete when the key was previously unset.\n * Shared capture/restore behind {@link withEnv} and the\n * {@link createTestPluginContext} options path; not part of the public surface.\n */\nexport function applyEnv(vars: Record<string, string>): () => void {\n const prior = new Map<string, string | undefined>();\n for (const key of Object.keys(vars)) {\n prior.set(key, process.env[key]);\n }\n Object.assign(process.env, vars);\n return () => {\n for (const [key, value] of prior) {\n if (value === undefined) {\n delete process.env[key];\n } else {\n process.env[key] = value;\n }\n }\n };\n}\n\n/**\n * Run a restore on an error path, suppressing any failure it throws so it\n * cannot replace the caller's original error.\n */\nfunction restoreQuietly(restore: () => void): void {\n try {\n restore();\n } catch (restoreError) {\n // A failed env restore must not mask the caller's original error.\n void restoreError;\n }\n}\n\n/**\n * Sets environment variables for the duration of `fn`, then restores them to\n * their prior state. Each key's prior value (or \"was absent\") is captured on\n * entry; on exit, the prior value is restored, or the key is deleted only if\n * it was previously unset.\n *\n * Supports both sync and async `fn`. If `fn` returns a thenable, `withEnv`\n * returns that promise and restores in `.finally()`. Otherwise, it restores\n * in a synchronous `finally` and returns the callback's return value.\n * Restoration runs even if `fn` throws. Nested calls restore in LIFO order.\n *\n * @example\n * ```ts\n * // Sync: restores synchronously after fn\n * withEnv({ MY_VAR: \"test\" }, () => {\n * console.log(process.env.MY_VAR); // \"test\"\n * });\n * console.log(process.env.MY_VAR); // prior value (or undefined)\n *\n * // Async: restores after promise settles\n * await withEnv({ MY_VAR: \"test\" }, async () => {\n * await fetch(...);\n * });\n * ```\n */\nexport function withEnv<T>(\n vars: Record<string, string>,\n fn: () => T | Promise<T>,\n): T | Promise<T> {\n const restore = applyEnv(vars);\n\n let result: T | Promise<T>;\n try {\n result = fn();\n } catch (err) {\n // Sync throw: restore, but never let a restore failure mask `err`.\n restoreQuietly(restore);\n throw err;\n }\n\n // Async: restore after the promise settles. On rejection, guard the restore\n // so it cannot replace the caller's error; on success, let a genuine restore\n // failure surface.\n if (result != null && typeof (result as Any).then === \"function\") {\n return (result as Promise<T>).then(\n (value) => {\n restore();\n return value;\n },\n (err: unknown) => {\n restoreQuietly(restore);\n throw err;\n },\n );\n }\n\n restore();\n return result;\n}\n\n/**\n * Clears AppKit's process-wide cache singleton so cached values don't leak\n * between tests in the same file.\n *\n * The cache `attach()` seeds is shared by every test in a file (Vitest isolates\n * files, not tests within a file). Call this in `beforeEach` when one test's\n * cached value must not be seen by the next, or mid-test to force a cache miss\n * before asserting a subsequent hit.\n *\n * No-ops when the cache has not been initialized yet, so it is safe to call\n * before any `attach()`.\n *\n * @example\n * ```ts\n * beforeEach(async () => {\n * await resetTestCache();\n * });\n * ```\n */\nexport async function resetTestCache(): Promise<void> {\n let cache: ReturnType<typeof CacheManager.getInstanceSync>;\n try {\n cache = CacheManager.getInstanceSync();\n } catch {\n // Not initialized yet — nothing to clear.\n return;\n }\n await cache.clear();\n}\n\n/**\n * Context options for running tests with mocked service/user context\n */\nexport interface TestContextOptions {\n /** Mock WorkspaceClient for service principal operations */\n serviceDatabricksClient?: Any;\n /** Mock WorkspaceClient for user operations */\n userDatabricksClient?: Any;\n /** User ID for user context */\n userId?: string;\n /** Service user ID */\n serviceUserId?: string;\n /** Warehouse ID */\n warehouseId?: string;\n /** Workspace ID */\n workspaceId?: string;\n}\n\n/**\n * Builds a {@link ServiceContextState} value for testing without touching the\n * singleton. Internal building block for {@link mockServiceContext}, which\n * installs the state as spies — that installer is the public entry point.\n */\nfunction buildServiceContextState(\n options: TestContextOptions = {},\n): ServiceContextState {\n return {\n client: (options.serviceDatabricksClient ||\n createMockWorkspaceClient()) as Any,\n serviceUserId: options.serviceUserId || \"test-service-user\",\n warehouseId: Promise.resolve(options.warehouseId || \"test-warehouse-id\"),\n workspaceId: Promise.resolve(options.workspaceId || \"test-workspace-id\"),\n };\n}\n\n/**\n * Mocks the `ServiceContext` singleton for testing — spies `get`,\n * `initialize`, `isInitialized`, and `createUserContext` so code that resolves\n * the service principal or an on-behalf-of user context gets test doubles.\n * Call in `beforeEach`; call the returned `restore()` in `afterEach`.\n *\n * @returns The mock context plus the spies and a `restore()` helper.\n */\nexport function mockServiceContext(options: TestContextOptions = {}) {\n const serviceContext = buildServiceContextState(options);\n\n const getSpy = vi\n .spyOn(ServiceContext, \"get\")\n .mockReturnValue(serviceContext);\n\n const initSpy = vi\n .spyOn(ServiceContext, \"initialize\")\n .mockResolvedValue(serviceContext);\n\n const isInitializedSpy = vi\n .spyOn(ServiceContext, \"isInitialized\")\n .mockReturnValue(true);\n\n const createUserContextSpy = vi\n .spyOn(ServiceContext, \"createUserContext\")\n .mockImplementation(\n fakeUserContext(\n options.userDatabricksClient || createMockWorkspaceClient(),\n serviceContext,\n ),\n );\n\n return {\n serviceContext,\n getSpy,\n initSpy,\n isInitializedSpy,\n createUserContextSpy,\n restore: () => {\n getSpy.mockRestore();\n initSpy.mockRestore();\n isInitializedSpy.mockRestore();\n createUserContextSpy.mockRestore();\n },\n };\n}\n\n/** The handle {@link mockServiceContext} returns (spies + `restore`). */\nexport type ServiceContextMock = ReturnType<typeof mockServiceContext>;\n\n/**\n * Registers a fresh {@link mockServiceContext} before each test and restores it\n * after — the whole `beforeEach`/`afterEach` dance in one line.\n *\n * Call it at the top of a `describe` block (or module top-level), NOT inside a\n * test: Vitest's `beforeEach`/`afterEach` only register during collection, so a\n * call from within a test body registers nothing for that test.\n *\n * Returns a **live** accessor, not the handle: each `beforeEach` builds fresh\n * spies, so reading `.current` inside a test always sees that test's mock. A\n * handle captured once would go stale after the first hook runs.\n *\n * @example\n * ```ts\n * describe(\"my plugin\", () => {\n * const ctx = useServiceContextMock({ warehouseId: \"wh-1\" });\n *\n * test(\"resolves the warehouse\", async () => {\n * await myHandler(req, res);\n * expect(ctx.current.getSpy).toHaveBeenCalled();\n * });\n * });\n * ```\n *\n * @returns `{ current }` — the active {@link ServiceContextMock} for the test.\n */\nexport function useServiceContextMock(options: TestContextOptions = {}): {\n readonly current: ServiceContextMock;\n} {\n let handle: ServiceContextMock | undefined;\n\n beforeEach(() => {\n handle = mockServiceContext(options);\n });\n\n afterEach(() => {\n handle?.restore();\n handle = undefined;\n });\n\n return {\n get current(): ServiceContextMock {\n if (!handle) {\n throw new Error(\n \"useServiceContextMock: no active mock. Call useServiceContextMock() \" +\n \"at the top of a describe block (not inside a test), and read \" +\n \"`.current` from within a test.\",\n );\n }\n return handle;\n },\n };\n}\n\n/**\n * Runs a test function within a mocked service context: installs the mock,\n * runs `fn`, and restores the singleton afterward.\n */\nexport async function runWithRequestContext<T>(\n fn: () => T | Promise<T>,\n context?: TestContextOptions,\n): Promise<T> {\n const mocks = mockServiceContext(context);\n\n try {\n return await fn();\n } finally {\n mocks.restore();\n }\n}\n\n/**\n * Builds a SUCCEEDED SQL statement response with a synthetic statement id,\n * `data_array` rows, and a manifest schema derived from `columns`.\n */\nexport function createSuccessfulSQLResponse(\n data: Any[][],\n columns: Array<{ name: string; type_name?: string }>,\n) {\n return {\n status: { state: \"SUCCEEDED\" },\n statement_id: `stmt-${Date.now()}`,\n result: {\n data_array: data,\n },\n manifest: {\n schema: {\n columns: columns.map((col) => ({\n name: col.name,\n type_name: col.type_name ?? \"STRING\",\n })),\n },\n },\n };\n}\n\n/** Builds a FAILED SQL statement response carrying `errorMessage`. */\nexport function createFailedSQLResponse(errorMessage: string) {\n return {\n status: {\n state: \"FAILED\",\n error: {\n message: errorMessage,\n },\n },\n statement_id: `stmt-${Date.now()}`,\n };\n}\n\n/**\n * Creates a genuine `ApiError` instance for testing error paths. Returns a real\n * instance (where `error instanceof ApiError` holds), suitable for testing\n * `instanceof` checks and `.statusCode` / `.errorCode` / `.message` accessors.\n *\n * @param options Error details: `statusCode`, `message`, and `errorCode`. All required.\n * @returns A genuine `ApiError` instance.\n *\n * @example\n * ```ts\n * const error = createApiError({\n * statusCode: 404,\n * message: \"File not found\",\n * errorCode: \"NOT_FOUND\",\n * });\n * expect(error).toBeInstanceOf(ApiError);\n * expect(error.statusCode).toBe(404);\n * ```\n */\nexport function createApiError(options: {\n statusCode: number;\n message: string;\n errorCode: string;\n}): ApiError {\n return new ApiError(\n options.message,\n options.errorCode,\n options.statusCode,\n undefined, // response: sensible default for testing\n [], // details: empty array\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fixtures.js","names":[],"sources":["../../src/testing/fixtures.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\n\nimport type { Span, SpanOptions } from \"@opentelemetry/api\";\nimport type { IAppRouter } from \"shared\";\nimport { afterEach, beforeEach, vi } from \"vitest\";\n\nimport { CacheManager } from \"../cache\";\nimport { snapshotCallerContext } from \"../context/caller-context\";\nimport type { ServiceContextState } from \"../context/service-context\";\nimport { ServiceContext } from \"../context/service-context\";\nimport { AuthenticationError } from \"../errors\";\nimport {\n WarehouseResource,\n type WarehouseBinding,\n} from \"../resources/warehouse\";\nimport type { InstrumentConfig, ITelemetry } from \"../telemetry/types\";\nimport { ApiError } from \"../workspace-client\";\nimport { createMockWorkspaceClient } from \"./mock-workspace-client\";\n\n// Test fixtures intentionally use loose shapes; `noExplicitAny` is disabled\n// repo-wide (see .oxlintrc.json), so a local alias keeps the intent readable.\ntype Any = any;\n\n/**\n * Creates a mock telemetry provider for testing. Every span/meter/logger is a\n * `vi.fn()` no-op, so plugins that trace, count, or log run without a live\n * OpenTelemetry pipeline. Passed into {@link createTestPluginContext} as the one\n * injectable production seam.\n */\nexport function createMockTelemetry(): ITelemetry {\n const mockSpan: Span = {\n addLink: vi.fn(),\n addLinks: vi.fn(),\n end: vi.fn(),\n setAttribute: vi.fn(),\n setAttributes: vi.fn(),\n setStatus: vi.fn(),\n recordException: vi.fn(),\n updateName: vi.fn(),\n addEvent: vi.fn(),\n isRecording: vi.fn().mockReturnValue(false),\n spanContext: vi.fn(),\n };\n\n return {\n getTracer: vi.fn().mockReturnValue({\n startActiveSpan: vi.fn().mockImplementation((...args: Any[]) => {\n const fn = args[args.length - 1];\n if (typeof fn === \"function\") {\n return fn(mockSpan);\n }\n return undefined;\n }),\n }),\n getMeter: vi.fn().mockReturnValue({\n createCounter: vi.fn().mockReturnValue({ add: vi.fn() }),\n createHistogram: vi.fn().mockReturnValue({ record: vi.fn() }),\n }),\n getLogger: vi.fn().mockReturnValue({\n emit: vi.fn(),\n }),\n emit: vi.fn(),\n startActiveSpan: vi\n .fn()\n .mockImplementation(\n async (\n _name: string,\n _options: SpanOptions,\n fn: (span: Span) => Promise<Any>,\n _tracerOptions?: InstrumentConfig,\n ) => {\n return await fn(mockSpan);\n },\n ),\n registerInstrumentations: vi.fn(),\n };\n}\n\n/**\n * Creates a mock Express router that captures registered handlers so a test\n * can pull a handler back out by method + path and invoke it directly.\n */\nexport function createMockRouter(): {\n router: IAppRouter;\n handlers: Record<string, Any>;\n getHandler: (method: string, path: string) => Any;\n} {\n const handlers: Record<string, Any> = {};\n\n const mockRouter = {\n get: vi.fn((path: string, handler: Any) => {\n handlers[`GET:${path}`] = handler;\n }),\n post: vi.fn((path: string, handler: Any) => {\n handlers[`POST:${path}`] = handler;\n }),\n put: vi.fn((path: string, handler: Any) => {\n handlers[`PUT:${path}`] = handler;\n }),\n delete: vi.fn((path: string, handler: Any) => {\n handlers[`DELETE:${path}`] = handler;\n }),\n patch: vi.fn((path: string, handler: Any) => {\n handlers[`PATCH:${path}`] = handler;\n }),\n } as unknown as IAppRouter;\n\n return {\n router: mockRouter,\n handlers,\n getHandler: (method: string, path: string) =>\n handlers[`${method.toUpperCase()}:${path}`],\n };\n}\n\n/**\n * On-behalf-of shorthand for {@link createMockRequest}. `true` uses the default\n * test user; an object picks the identity. Sets the forwarded headers the real\n * `Plugin.asUser` reads (`x-forwarded-access-token`, `x-forwarded-user`, and —\n * when given — `x-forwarded-email`), so an OBO test is one flag instead of\n * hand-rolled headers.\n */\nexport type OboOption =\n | boolean\n | {\n /** `x-forwarded-user` — defaults to `\"test-user\"`. */\n userId?: string;\n /** `x-forwarded-access-token` — defaults to `\"test-user-token\"`. */\n token?: string;\n /** `x-forwarded-email` — omitted unless provided. */\n email?: string;\n };\n\n/**\n * The one fake of `ServiceContext.createCallerContext` this kit uses, shared by\n * `mockServiceContext` and `createTestApp`.\n *\n * Shared rather than duplicated because the two used to disagree, and neither\n * matched production: a missing token went unrejected and `tokenFingerprint`\n * was absent, which silently disables Lakebase pool rotation — `pool-manager`\n * treats a missing fingerprint as \"not stale\", so the drain-and-recreate branch\n * could never run under a fake.\n *\n * @internal\n */\nexport function fakeUserContext(client: Any, ids: { workspaceId: Any }) {\n return (\n token: string,\n userId: string,\n userName?: string,\n userEmail?: string,\n ): Any => {\n // Same rejection as production, so a path that forgets to forward the token\n // fails here instead of only in a deployed app.\n if (!token) throw AuthenticationError.missingToken(\"user token\");\n return snapshotCallerContext({\n client,\n principal: { type: \"user\", userId, userName, userEmail },\n // Derived from the token exactly as production does. Keyed on the user it\n // would be constant across tokens, and rotation compares this value.\n tokenFingerprint: createHash(\"sha256\")\n .update(token)\n .digest(\"hex\")\n .slice(0, 16),\n workspaceId: ids.workspaceId,\n });\n };\n}\n\n/**\n * Build the forwarded identity headers an `obo` option implies.\n *\n * Exported so `createTestApp`'s request methods use the same convention as\n * `createMockRequest` rather than a second one.\n *\n * @internal\n */\nexport function oboHeaders(\n obo: Exclude<OboOption, false>,\n): Record<string, string> {\n const opts = obo === true ? {} : obo;\n const headers: Record<string, string> = {\n \"x-forwarded-access-token\": opts.token ?? \"test-user-token\",\n \"x-forwarded-user\": opts.userId ?? \"test-user\",\n };\n if (opts.email) headers[\"x-forwarded-email\"] = opts.email;\n return headers;\n}\n\n/**\n * Creates a mock Express request. Pass `overrides` to set `params`, `query`,\n * `body`, `headers`, etc.\n *\n * For on-behalf-of tests, pass `obo` instead of hand-adding forwarded headers —\n * `createMockRequest({ obo: true })` sets the identity headers the real\n * `asUser` requires. Any explicit `headers` you also pass win over the ones\n * `obo` generates, so you can override a single field.\n *\n * @example\n * ```ts\n * createMockRequest({ obo: true }); // default test user + token\n * createMockRequest({ obo: { userId: \"alice\" } }); // pick the user\n * ```\n */\nexport function createMockRequest(overrides: Any = {}) {\n const { obo, headers: headerOverrides, ...rest } = overrides;\n\n // `obo` seeds the forwarded identity headers; an explicit `headers` override\n // still wins (merged last) so a test can tweak or drop a single field.\n //\n // Keys are lowercased on the way in, the way Node's parser gives them to\n // Express. `header()` looks up lowercased, so storing as-given would file a\n // mixed-case override under a second key and leave the obo one winning.\n const headers: Record<string, unknown> = Object.fromEntries(\n Object.entries({\n ...(obo ? oboHeaders(obo) : {}),\n ...headerOverrides,\n }).map(([name, value]) => [name.toLowerCase(), value]),\n );\n\n const req = {\n params: {},\n query: {},\n body: {},\n header: function (name: string) {\n return this.headers[name.toLowerCase()];\n },\n // `...rest` keeps the original override power over every default above;\n // `headers` is applied last as the one managed field (obo + overrides).\n ...rest,\n headers,\n };\n return req;\n}\n\n/**\n * Creates a mock Express response object. `write`/`send`/`setHeader` flip\n * `headersSent`, `end` flips `writableEnded` and fires any `close` listener —\n * enough for streaming handlers that branch on those flags.\n *\n * Every chunk passed to `write` (and a final chunk to `end`) is captured, so a\n * streaming route's real SSE output can be replayed: pass the response straight\n * to {@link expectStream}, or call `sseResponse()` for a real `Response`.\n *\n * @example Assert what a streaming route emitted\n * ```ts\n * const res = createMockResponse();\n * await plugin._handleStream(req, res);\n * await expectStream(res).toEmit(\"status\", \"result\");\n * ```\n */\nexport function createMockResponse() {\n const eventListeners: Record<string, Array<(...args: Any[]) => void>> = {};\n const chunks: string[] = [];\n\n const res = {\n // Flips to true once headers/body have gone out — mirrors Express so\n // streaming handlers can branch between a JSON error (pre-headers) and\n // aborting the socket (mid-stream).\n headersSent: false,\n status: vi.fn().mockReturnThis(),\n json: vi.fn().mockReturnThis(),\n send: vi.fn(function (this: Any) {\n this.headersSent = true;\n return this;\n }),\n sendStatus: vi.fn().mockReturnThis(),\n end: vi.fn(function (this: Any, chunk?: unknown) {\n // Express allows `end(chunk)` and `end(callback)`; capture only a data\n // chunk, never the completion callback.\n if (chunk != null && typeof chunk !== \"function\") {\n chunks.push(String(chunk));\n }\n this.writableEnded = true;\n if (eventListeners.close) {\n for (const handler of eventListeners.close) {\n handler();\n }\n }\n return this;\n }),\n write: vi.fn(function (this: Any, chunk?: unknown) {\n this.headersSent = true;\n if (chunk != null) chunks.push(String(chunk));\n // Return `this` (truthy) rather than a boolean: handlers that gate on\n // backpressure (`if (res.write(buf)) …`) then take the no-wait path.\n return this;\n }),\n setHeader: vi.fn(function (this: Any) {\n this.headersSent = true;\n return this;\n }),\n flushHeaders: vi.fn().mockReturnThis(),\n destroy: vi.fn().mockReturnThis(),\n on: vi.fn(function (\n this: Any,\n event: string,\n handler: (...args: Any[]) => void,\n ) {\n if (!eventListeners[event]) {\n eventListeners[event] = [];\n }\n eventListeners[event].push(handler);\n return this;\n }),\n off: vi.fn(function (\n this: Any,\n event: string,\n handler: (...args: Any[]) => void,\n ) {\n if (eventListeners[event]) {\n eventListeners[event] = eventListeners[event].filter(\n (h) => h !== handler,\n );\n }\n return this;\n }),\n writableEnded: false,\n /**\n * The SSE body captured so far, as a real `Response` — the bridge from a\n * `res.write`-based handler into {@link expectStream}. `expectStream`\n * detects this method and calls it for you, so `expectStream(res)` and\n * `expectStream(res.sseResponse())` are equivalent.\n */\n sseResponse(): Response {\n return new Response(chunks.join(\"\"));\n },\n };\n return res;\n}\n\n/**\n * Sets up common environment variables for Databricks testing so code that\n * reads `DATABRICKS_HOST` / `DATABRICKS_WAREHOUSE_ID` finds test values.\n */\nexport function setupDatabricksEnv(overrides: Record<string, string> = {}) {\n process.env.DATABRICKS_HOST = \"https://test.databricks.com\";\n process.env.DATABRICKS_WAREHOUSE_ID = \"test-warehouse-id\";\n Object.assign(process.env, overrides);\n}\n\n/**\n * Set environment variables and return a function that restores each key to its\n * prior state — the prior value, or a delete when the key was previously unset.\n * Shared capture/restore behind {@link withEnv} and the\n * {@link createTestPluginContext} options path; not part of the public surface.\n */\nexport function applyEnv(vars: Record<string, string>): () => void {\n const prior = new Map<string, string | undefined>();\n for (const key of Object.keys(vars)) {\n prior.set(key, process.env[key]);\n }\n Object.assign(process.env, vars);\n return () => {\n for (const [key, value] of prior) {\n if (value === undefined) {\n delete process.env[key];\n } else {\n process.env[key] = value;\n }\n }\n };\n}\n\n/**\n * Run a restore on an error path, suppressing any failure it throws so it\n * cannot replace the caller's original error.\n */\nfunction restoreQuietly(restore: () => void): void {\n try {\n restore();\n } catch (restoreError) {\n // A failed env restore must not mask the caller's original error.\n void restoreError;\n }\n}\n\n/**\n * Sets environment variables for the duration of `fn`, then restores them to\n * their prior state. Each key's prior value (or \"was absent\") is captured on\n * entry; on exit, the prior value is restored, or the key is deleted only if\n * it was previously unset.\n *\n * Supports both sync and async `fn`. If `fn` returns a thenable, `withEnv`\n * returns that promise and restores in `.finally()`. Otherwise, it restores\n * in a synchronous `finally` and returns the callback's return value.\n * Restoration runs even if `fn` throws. Nested calls restore in LIFO order.\n *\n * @example\n * ```ts\n * // Sync: restores synchronously after fn\n * withEnv({ MY_VAR: \"test\" }, () => {\n * console.log(process.env.MY_VAR); // \"test\"\n * });\n * console.log(process.env.MY_VAR); // prior value (or undefined)\n *\n * // Async: restores after promise settles\n * await withEnv({ MY_VAR: \"test\" }, async () => {\n * await fetch(...);\n * });\n * ```\n */\nexport function withEnv<T>(\n vars: Record<string, string>,\n fn: () => T | Promise<T>,\n): T | Promise<T> {\n const restore = applyEnv(vars);\n\n let result: T | Promise<T>;\n try {\n result = fn();\n } catch (err) {\n // Sync throw: restore, but never let a restore failure mask `err`.\n restoreQuietly(restore);\n throw err;\n }\n\n // Async: restore after the promise settles. On rejection, guard the restore\n // so it cannot replace the caller's error; on success, let a genuine restore\n // failure surface.\n if (result != null && typeof (result as Any).then === \"function\") {\n return (result as Promise<T>).then(\n (value) => {\n restore();\n return value;\n },\n (err: unknown) => {\n restoreQuietly(restore);\n throw err;\n },\n );\n }\n\n restore();\n return result;\n}\n\n/**\n * Clears AppKit's process-wide cache singleton so cached values don't leak\n * between tests in the same file.\n *\n * The cache `attach()` seeds is shared by every test in a file (Vitest isolates\n * files, not tests within a file). Call this in `beforeEach` when one test's\n * cached value must not be seen by the next, or mid-test to force a cache miss\n * before asserting a subsequent hit.\n *\n * No-ops when the cache has not been initialized yet, so it is safe to call\n * before any `attach()`.\n *\n * @example\n * ```ts\n * beforeEach(async () => {\n * await resetTestCache();\n * });\n * ```\n */\nexport async function resetTestCache(): Promise<void> {\n let cache: ReturnType<typeof CacheManager.getInstanceSync>;\n try {\n cache = CacheManager.getInstanceSync();\n } catch {\n // Not initialized yet — nothing to clear.\n return;\n }\n await cache.clear();\n}\n\n/**\n * Context options for running tests with mocked service/user context\n */\nexport interface TestContextOptions {\n /** Mock WorkspaceClient for service principal operations */\n serviceDatabricksClient?: Any;\n /** Mock WorkspaceClient for user operations */\n userDatabricksClient?: Any;\n /** User ID for user context */\n userId?: string;\n /** Service user ID */\n serviceUserId?: string;\n /** Warehouse ID */\n warehouseId?: string;\n /** Workspace ID */\n workspaceId?: string;\n}\n\n/**\n * Builds a {@link ServiceContextState} value for testing without touching the\n * singleton. Internal building block for {@link mockServiceContext}, which\n * installs the state as spies — that installer is the public entry point.\n */\nfunction buildServiceContextState(\n options: TestContextOptions,\n resources: WarehouseBinding,\n): ServiceContextState {\n return {\n client: (options.serviceDatabricksClient ||\n createMockWorkspaceClient()) as Any,\n serviceUserId: options.serviceUserId || \"test-service-user\",\n // Preserve the deprecated field for external test callers during migration.\n warehouseId: resources.warehouseId,\n workspaceId: Promise.resolve(options.workspaceId || \"test-workspace-id\"),\n };\n}\n\n/**\n * Mocks the `ServiceContext` singleton for testing — spies `get`,\n * `initialize`, `isInitialized`, and `createCallerContext` so code that resolves\n * the service principal or an on-behalf-of user context gets test doubles.\n * Also supplies the app-level warehouse binding through WarehouseResource.\n * Call in `beforeEach`; call the returned `restore()` in `afterEach`.\n *\n * @returns The mock context plus the spies and a `restore()` helper.\n */\nexport function mockServiceContext(options: TestContextOptions = {}) {\n const resources = Object.freeze({\n warehouseId: Promise.resolve(options.warehouseId || \"test-warehouse-id\"),\n });\n const serviceContext = buildServiceContextState(options, resources);\n\n const resourcesSpy = vi\n .spyOn(WarehouseResource, \"get\")\n .mockReturnValue(resources);\n\n const getSpy = vi\n .spyOn(ServiceContext, \"get\")\n .mockReturnValue(serviceContext);\n\n const initSpy = vi\n .spyOn(ServiceContext, \"initialize\")\n .mockResolvedValue(serviceContext);\n\n const isInitializedSpy = vi\n .spyOn(ServiceContext, \"isInitialized\")\n .mockReturnValue(true);\n\n // Keep the public spy handle name for existing tests. The deprecated factory\n // delegates to createCallerContext, so this spy covers both entry points.\n const createUserContextSpy = vi\n .spyOn(ServiceContext, \"createCallerContext\")\n .mockImplementation(\n fakeUserContext(\n options.userDatabricksClient || createMockWorkspaceClient(),\n serviceContext,\n ),\n );\n\n return {\n serviceContext,\n getSpy,\n initSpy,\n isInitializedSpy,\n createUserContextSpy,\n restore: () => {\n getSpy.mockRestore();\n initSpy.mockRestore();\n isInitializedSpy.mockRestore();\n createUserContextSpy.mockRestore();\n resourcesSpy.mockRestore();\n },\n };\n}\n\n/** The handle {@link mockServiceContext} returns (spies + `restore`). */\nexport type ServiceContextMock = ReturnType<typeof mockServiceContext>;\n\n/**\n * Registers a fresh {@link mockServiceContext} before each test and restores it\n * after — the whole `beforeEach`/`afterEach` dance in one line.\n *\n * Call it at the top of a `describe` block (or module top-level), NOT inside a\n * test: Vitest's `beforeEach`/`afterEach` only register during collection, so a\n * call from within a test body registers nothing for that test.\n *\n * Returns a **live** accessor, not the handle: each `beforeEach` builds fresh\n * spies, so reading `.current` inside a test always sees that test's mock. A\n * handle captured once would go stale after the first hook runs.\n *\n * @example\n * ```ts\n * describe(\"my plugin\", () => {\n * const ctx = useServiceContextMock({ warehouseId: \"wh-1\" });\n *\n * test(\"resolves the warehouse\", async () => {\n * await myHandler(req, res);\n * expect(ctx.current.getSpy).toHaveBeenCalled();\n * });\n * });\n * ```\n *\n * @returns `{ current }` — the active {@link ServiceContextMock} for the test.\n */\nexport function useServiceContextMock(options: TestContextOptions = {}): {\n readonly current: ServiceContextMock;\n} {\n let handle: ServiceContextMock | undefined;\n\n beforeEach(() => {\n handle = mockServiceContext(options);\n });\n\n afterEach(() => {\n handle?.restore();\n handle = undefined;\n });\n\n return {\n get current(): ServiceContextMock {\n if (!handle) {\n throw new Error(\n \"useServiceContextMock: no active mock. Call useServiceContextMock() \" +\n \"at the top of a describe block (not inside a test), and read \" +\n \"`.current` from within a test.\",\n );\n }\n return handle;\n },\n };\n}\n\n/**\n * Runs a test function within a mocked service context: installs the mock,\n * runs `fn`, and restores the singleton afterward.\n */\nexport async function runWithRequestContext<T>(\n fn: () => T | Promise<T>,\n context?: TestContextOptions,\n): Promise<T> {\n const mocks = mockServiceContext(context);\n\n try {\n return await fn();\n } finally {\n mocks.restore();\n }\n}\n\n/**\n * Builds a SUCCEEDED SQL statement response with a synthetic statement id,\n * `data_array` rows, and a manifest schema derived from `columns`.\n */\nexport function createSuccessfulSQLResponse(\n data: Any[][],\n columns: Array<{ name: string; type_name?: string }>,\n) {\n return {\n status: { state: \"SUCCEEDED\" },\n statement_id: `stmt-${Date.now()}`,\n result: {\n data_array: data,\n },\n manifest: {\n schema: {\n columns: columns.map((col) => ({\n name: col.name,\n type_name: col.type_name ?? \"STRING\",\n })),\n },\n },\n };\n}\n\n/** Builds a FAILED SQL statement response carrying `errorMessage`. */\nexport function createFailedSQLResponse(errorMessage: string) {\n return {\n status: {\n state: \"FAILED\",\n error: {\n message: errorMessage,\n },\n },\n statement_id: `stmt-${Date.now()}`,\n };\n}\n\n/**\n * Creates a genuine `ApiError` instance for testing error paths. Returns a real\n * instance (where `error instanceof ApiError` holds), suitable for testing\n * `instanceof` checks and `.statusCode` / `.errorCode` / `.message` accessors.\n *\n * @param options Error details: `statusCode`, `message`, and `errorCode`. All required.\n * @returns A genuine `ApiError` instance.\n *\n * @example\n * ```ts\n * const error = createApiError({\n * statusCode: 404,\n * message: \"File not found\",\n * errorCode: \"NOT_FOUND\",\n * });\n * expect(error).toBeInstanceOf(ApiError);\n * expect(error.statusCode).toBe(404);\n * ```\n */\nexport function createApiError(options: {\n statusCode: number;\n message: string;\n errorCode: string;\n}): ApiError {\n return new ApiError(\n options.message,\n options.errorCode,\n options.statusCode,\n undefined, // response: sensible default for testing\n [], // details: empty array\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ServiceContext } from "../context/service-context.js";
|
|
2
1
|
import { createLogger } from "../logging/logger.js";
|
|
2
|
+
import { ServiceContext } from "../context/service-context.js";
|
|
3
3
|
import { TelemetryManager } from "../telemetry/telemetry-manager.js";
|
|
4
4
|
import "../telemetry/index.js";
|
|
5
5
|
import { CacheManager } from "../cache/index.js";
|
|
@@ -3,7 +3,7 @@ import { walkSchema } from "./walk-schema.js";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import
|
|
6
|
+
import fsSync from "node:fs";
|
|
7
7
|
|
|
8
8
|
//#region src/type-generator/database/generate.ts
|
|
9
9
|
/** Safe diagnostic raised when database declarations cannot be generated. */
|
|
@@ -63,7 +63,7 @@ ${walkSchema(schema).map((entry) => ` ${JSON.stringify(entry.name)}: {
|
|
|
63
63
|
}
|
|
64
64
|
/** Atomically replace generated output only when its content changes. */
|
|
65
65
|
async function writeIfChanged(outFile, content) {
|
|
66
|
-
if (
|
|
66
|
+
if (fsSync.existsSync(outFile) && await fs.readFile(outFile, "utf8") === content) return;
|
|
67
67
|
await fs.mkdir(path.dirname(outFile), { recursive: true });
|
|
68
68
|
const temporary = path.join(path.dirname(outFile), `.${path.basename(outFile)}.${randomUUID()}.tmp`);
|
|
69
69
|
try {
|
|
@@ -80,7 +80,7 @@ async function importSchema(schemaFile) {
|
|
|
80
80
|
}
|
|
81
81
|
/** Generate current registry types or neutralize them when loading fails. */
|
|
82
82
|
async function generateDatabaseTypes(options) {
|
|
83
|
-
if (!
|
|
83
|
+
if (!fsSync.existsSync(options.schemaFile)) {
|
|
84
84
|
await writeIfChanged(options.outFile, NEUTRAL_DATABASE_TYPES);
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate.js","names":[
|
|
1
|
+
{"version":3,"file":"generate.js","names":[],"sources":["../../../src/type-generator/database/generate.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport fsSync from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { Schema } from \"../../database/schema-builder\";\nimport { assertFinalizedSchema } from \"../../database/schema-builder/define-schema\";\nimport { walkSchema } from \"./walk-schema\";\n\n/** Safe diagnostic raised when database declarations cannot be generated. */\nexport class DatabaseTypegenError extends Error {\n constructor(message = \"Database schema generation failed\") {\n super(message);\n this.name = \"DatabaseTypegenError\";\n }\n}\n\ninterface GenerateDatabaseTypesOptions {\n readonly schemaFile: string;\n readonly outFile: string;\n}\n\nexport const DATABASE_TYPES_FILE = \"database.d.ts\";\n\nfunction schemaLabel(schemaFile: string): string {\n return path.relative(process.cwd(), schemaFile) || schemaFile;\n}\n\nfunction importFailureReason(error: unknown): string {\n if (error instanceof SyntaxError) return \"could not be parsed\";\n let code: unknown;\n try {\n code =\n error && typeof error === \"object\"\n ? Reflect.get(error, \"code\")\n : undefined;\n } catch {\n code = undefined;\n }\n if (code === \"MODULE_NOT_FOUND\" || code === \"ERR_MODULE_NOT_FOUND\") {\n return \"contains an unresolved import\";\n }\n return \"threw while loading\";\n}\n\n/** Empty augmentation used when no valid database schema is available. */\nexport const NEUTRAL_DATABASE_TYPES = `// Auto-generated by AppKit - DO NOT EDIT\nimport \"@databricks/appkit\";\n\ndeclare module \"@databricks/appkit\" {\n interface DatabaseRegistry {}\n}\n`;\n\n/** Render one `DatabaseRegistry` augmentation from a finalized schema. */\nfunction render(schema: Schema): string {\n const entries = walkSchema(schema)\n .map(\n (entry) => ` ${JSON.stringify(entry.name)}: {\n row: ${entry.row};\n publicRow: ${entry.publicRow};\n insert: ${entry.insert};\n update: ${entry.update};\n filters: ${entry.filters};\n includes: ${entry.includes};\n hasPrimaryKey: ${entry.hasPrimaryKey};\n };`,\n )\n .join(\"\\n\");\n return `// Auto-generated by AppKit - DO NOT EDIT\nimport \"@databricks/appkit\";\n\ndeclare module \"@databricks/appkit\" {\n type DatabaseLogicalFilter<T> = T & {\n and?: readonly DatabaseLogicalFilter<T>[];\n or?: readonly DatabaseLogicalFilter<T>[];\n };\n\n interface DatabaseRegistry {\n${entries}\n }\n}\n`;\n}\n\n/** Atomically replace generated output only when its content changes. */\nasync function writeIfChanged(outFile: string, content: string): Promise<void> {\n if (\n fsSync.existsSync(outFile) &&\n (await fs.readFile(outFile, \"utf8\")) === content\n )\n return;\n await fs.mkdir(path.dirname(outFile), { recursive: true });\n const temporary = path.join(\n path.dirname(outFile),\n `.${path.basename(outFile)}.${randomUUID()}.tmp`,\n );\n try {\n await fs.writeFile(temporary, content, \"utf8\");\n await fs.rename(temporary, outFile);\n } finally {\n await fs.rm(temporary, { force: true });\n }\n}\n\n/** Disable Jiti's module cache so watch-mode runs observe schema edits. */\nasync function importSchema(schemaFile: string): Promise<unknown> {\n const { createJiti } = await import(\"jiti\");\n return createJiti(import.meta.url, { moduleCache: false }).import(schemaFile);\n}\n\n/** Generate current registry types or neutralize them when loading fails. */\nexport async function generateDatabaseTypes(\n options: GenerateDatabaseTypesOptions,\n): Promise<void> {\n if (!fsSync.existsSync(options.schemaFile)) {\n await writeIfChanged(options.outFile, NEUTRAL_DATABASE_TYPES);\n return;\n }\n try {\n const module = (await importSchema(options.schemaFile)) as {\n schema?: Schema;\n };\n if (!(\"schema\" in module)) {\n throw new DatabaseTypegenError(\n `Database schema module \"${schemaLabel(options.schemaFile)}\" must export a named \"schema\"`,\n );\n }\n try {\n assertFinalizedSchema(module.schema);\n } catch {\n throw new DatabaseTypegenError(\n `Database schema \"${schemaLabel(options.schemaFile)}\" is not a finalized AppKit schema`,\n );\n }\n await writeIfChanged(options.outFile, render(module.schema));\n } catch (error) {\n // A failed run must not leave stale entities visible to TypeScript.\n await writeIfChanged(options.outFile, NEUTRAL_DATABASE_TYPES);\n throw error instanceof DatabaseTypegenError\n ? error\n : new DatabaseTypegenError(\n `Database schema \"${schemaLabel(options.schemaFile)}\" ${importFailureReason(error)}`,\n );\n }\n}\n"],"mappings":";;;;;;;;;AAUA,IAAa,uBAAb,cAA0C,MAAM;CAC9C,YAAY,UAAU,qCAAqC;AACzD,QAAM,QAAQ;AACd,OAAK,OAAO;;;AAShB,MAAa,sBAAsB;AAEnC,SAAS,YAAY,YAA4B;AAC/C,QAAO,KAAK,SAAS,QAAQ,KAAK,EAAE,WAAW,IAAI;;AAGrD,SAAS,oBAAoB,OAAwB;AACnD,KAAI,iBAAiB,YAAa,QAAO;CACzC,IAAI;AACJ,KAAI;AACF,SACE,SAAS,OAAO,UAAU,WACtB,QAAQ,IAAI,OAAO,OAAO,GAC1B;SACA;AACN,SAAO;;AAET,KAAI,SAAS,sBAAsB,SAAS,uBAC1C,QAAO;AAET,QAAO;;;AAIT,MAAa,yBAAyB;;;;;;;;AAStC,SAAS,OAAO,QAAwB;AActC,QAAO;;;;;;;;;;EAbS,WAAW,OAAO,CAC/B,KACE,UAAU,OAAO,KAAK,UAAU,MAAM,KAAK,CAAC;aACtC,MAAM,IAAI;mBACJ,MAAM,UAAU;gBACnB,MAAM,OAAO;gBACb,MAAM,OAAO;iBACZ,MAAM,QAAQ;kBACb,MAAM,SAAS;uBACV,MAAM,cAAc;QAEtC,CACA,KAAK,KAAK,CAWL;;;;;;AAOV,eAAe,eAAe,SAAiB,SAAgC;AAC7E,KACE,OAAO,WAAW,QAAQ,IACzB,MAAM,GAAG,SAAS,SAAS,OAAO,KAAM,QAEzC;AACF,OAAM,GAAG,MAAM,KAAK,QAAQ,QAAQ,EAAE,EAAE,WAAW,MAAM,CAAC;CAC1D,MAAM,YAAY,KAAK,KACrB,KAAK,QAAQ,QAAQ,EACrB,IAAI,KAAK,SAAS,QAAQ,CAAC,GAAG,YAAY,CAAC,MAC5C;AACD,KAAI;AACF,QAAM,GAAG,UAAU,WAAW,SAAS,OAAO;AAC9C,QAAM,GAAG,OAAO,WAAW,QAAQ;WAC3B;AACR,QAAM,GAAG,GAAG,WAAW,EAAE,OAAO,MAAM,CAAC;;;;AAK3C,eAAe,aAAa,YAAsC;CAChE,MAAM,EAAE,eAAe,MAAM,OAAO;AACpC,QAAO,WAAW,OAAO,KAAK,KAAK,EAAE,aAAa,OAAO,CAAC,CAAC,OAAO,WAAW;;;AAI/E,eAAsB,sBACpB,SACe;AACf,KAAI,CAAC,OAAO,WAAW,QAAQ,WAAW,EAAE;AAC1C,QAAM,eAAe,QAAQ,SAAS,uBAAuB;AAC7D;;AAEF,KAAI;EACF,MAAM,SAAU,MAAM,aAAa,QAAQ,WAAW;AAGtD,MAAI,EAAE,YAAY,QAChB,OAAM,IAAI,qBACR,2BAA2B,YAAY,QAAQ,WAAW,CAAC,gCAC5D;AAEH,MAAI;AACF,yBAAsB,OAAO,OAAO;UAC9B;AACN,SAAM,IAAI,qBACR,oBAAoB,YAAY,QAAQ,WAAW,CAAC,oCACrD;;AAEH,QAAM,eAAe,QAAQ,SAAS,OAAO,OAAO,OAAO,CAAC;UACrD,OAAO;AAEd,QAAM,eAAe,QAAQ,SAAS,uBAAuB;AAC7D,QAAM,iBAAiB,uBACnB,QACA,IAAI,qBACF,oBAAoB,YAAY,QAAQ,WAAW,CAAC,IAAI,oBAAoB,MAAM,GACnF"}
|
|
@@ -2,7 +2,7 @@ import { createLogger } from "../logging/logger.js";
|
|
|
2
2
|
import pc from "picocolors";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import
|
|
5
|
+
import fsSync from "node:fs";
|
|
6
6
|
|
|
7
7
|
//#region src/type-generator/migration.ts
|
|
8
8
|
const logger = createLogger("type-generator:migration");
|
|
@@ -16,7 +16,7 @@ const logger = createLogger("type-generator:migration");
|
|
|
16
16
|
*/
|
|
17
17
|
function resolveProjectRoot(outFile) {
|
|
18
18
|
const root = path.resolve(path.dirname(outFile), "..", "..");
|
|
19
|
-
if (!
|
|
19
|
+
if (!fsSync.existsSync(path.join(root, "package.json"))) logger.warn("Resolved project root %s has no package.json — migration may target the wrong directory. Check your outFile path: %s", root, outFile);
|
|
20
20
|
return root;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration.js","names":["fsSync"],"sources":["../../src/type-generator/migration.ts"],"sourcesContent":["import fsSync from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport pc from \"picocolors\";\n\nimport { createLogger } from \"../logging/logger\";\n\nconst logger = createLogger(\"type-generator:migration\");\n\n/**\n * Derive project root from an outFile path.\n * outFile is always `<projectRoot>/shared/appkit-types/<file>` — both the Vite plugins\n * and the CLI construct it this way, so going up two levels is safe.\n *\n * Validates that the resolved root contains a package.json — if not, logs a warning\n * so custom outFile paths don't silently operate on the wrong directory.\n */\nexport function resolveProjectRoot(outFile: string): string {\n const root = path.resolve(path.dirname(outFile), \"..\", \"..\");\n if (!fsSync.existsSync(path.join(root, \"package.json\"))) {\n logger.warn(\n \"Resolved project root %s has no package.json — migration may target the wrong directory. \" +\n \"Check your outFile path: %s\",\n root,\n outFile,\n );\n }\n return root;\n}\n\n/**\n * Remove old generated types from client/src/ (pre-shared/ location).\n * Best-effort: silently ignores missing files.\n */\nexport async function removeOldGeneratedTypes(\n projectRoot: string,\n filename: string,\n): Promise<void> {\n const oldFile = path.join(projectRoot, \"client\", \"src\", filename);\n try {\n await fs.unlink(oldFile);\n logger.debug(\"Removed old types at %s\", oldFile);\n } catch {\n // File doesn't exist — nothing to clean up\n }\n}\n\n// ── Project config migration ────────────────────────────────────────────\n\nconst migratedProjects = new Set<string>();\n\n/**\n * One-time config migration: update tsconfig and package.json for shared/ types output.\n * Idempotent — each sub-migration checks current file state and skips if already migrated.\n * Deduplicates per project root so monorepo builds migrate each app independently.\n * Opt-out: set `\"appkit\": { \"autoMigrate\": false }` in package.json.\n */\nexport async function migrateProjectConfig(projectRoot: string): Promise<void> {\n const resolved = path.resolve(projectRoot);\n if (migratedProjects.has(resolved)) return;\n migratedProjects.add(resolved);\n\n if (await isAutoMigrateDisabled(projectRoot)) {\n logger.debug(\"Auto-migration disabled via package.json appkit.autoMigrate\");\n return;\n }\n\n const results: Array<{ file: string; action: string }> = [];\n\n results.push(...(await migrateTsconfigClient(projectRoot)));\n results.push(...(await migrateTsconfigServer(projectRoot)));\n results.push(...(await migratePackageJsonScripts(projectRoot)));\n\n if (results.length > 0) {\n printMigrationSummary(results);\n }\n}\n\n/** Exported for testing only. */\nexport function _resetMigrationState(): void {\n migratedProjects.clear();\n}\n\n// ── Helpers ─────────────────────────────────────────────────────────────\n\nasync function isAutoMigrateDisabled(projectRoot: string): Promise<boolean> {\n try {\n const raw = await fs.readFile(\n path.join(projectRoot, \"package.json\"),\n \"utf-8\",\n );\n const parsed = JSON.parse(raw);\n return parsed.appkit?.autoMigrate === false;\n } catch {\n return false;\n }\n}\n\n/** Strip JSONC comments (block and line) so JSON.parse can handle tsconfig files. */\nfunction stripJsonComments(text: string): string {\n // Match strings (to skip them) or comments (to remove them).\n // Strings must be matched first to avoid stripping comment-like patterns inside string values\n // (e.g. \"server/**/*\" contains /* which looks like a block comment start).\n return text.replace(/\"(?:[^\"\\\\]|\\\\.)*\"|\\/\\*[\\s\\S]*?\\*\\/|\\/\\/.*/g, (match) =>\n match.startsWith('\"') ? match : \"\",\n );\n}\n\ntype MigrationResult = Array<{ file: string; action: string }>;\n\n// ── tsconfig.client.json ────────────────────────────────────────────────\n\nasync function migrateTsconfigClient(\n projectRoot: string,\n): Promise<MigrationResult> {\n const results: MigrationResult = [];\n const filePath = path.join(projectRoot, \"tsconfig.client.json\");\n\n try {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const parsed = JSON.parse(stripJsonComments(raw));\n\n if (!Array.isArray(parsed.include)) return results;\n if (parsed.include.includes(\"shared/appkit-types\")) return results;\n\n parsed.include.push(\"shared/appkit-types\");\n await fs.writeFile(\n filePath,\n `${JSON.stringify(parsed, null, 2)}\\n`,\n \"utf-8\",\n );\n results.push({\n file: \"tsconfig.client.json\",\n action: 'added \"shared/appkit-types\" to include',\n });\n } catch (err) {\n logger.warn(\n \"Failed to migrate tsconfig.client.json: %s\",\n (err as Error).message,\n );\n }\n\n return results;\n}\n\n// ── tsconfig.server.json ────────────────────────────────────────────────\n\nasync function migrateTsconfigServer(\n projectRoot: string,\n): Promise<MigrationResult> {\n const results: MigrationResult = [];\n const filePath = path.join(projectRoot, \"tsconfig.server.json\");\n\n try {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const parsed = JSON.parse(stripJsonComments(raw));\n const opts = parsed.compilerOptions;\n\n if (!opts || !opts.outDir) return results; // already migrated or non-standard\n\n delete opts.outDir;\n delete opts.declaration;\n delete opts.declarationMap;\n delete opts.sourceMap;\n opts.noEmit = true;\n\n await fs.writeFile(\n filePath,\n `${JSON.stringify(parsed, null, 2)}\\n`,\n \"utf-8\",\n );\n results.push({\n file: \"tsconfig.server.json\",\n action: \"switched to noEmit mode\",\n });\n } catch (err) {\n logger.warn(\n \"Failed to migrate tsconfig.server.json: %s\",\n (err as Error).message,\n );\n }\n\n return results;\n}\n\n// ── package.json ────────────────────────────────────────────────────────\n\nconst SCRIPT_MIGRATIONS: Record<string, { old: string; new: string }> = {\n \"build:server\": {\n old: \"tsdown -c tsdown.server.config.ts\",\n new: \"tsc -b tsconfig.server.json && tsdown -c tsdown.server.config.ts\",\n },\n typecheck: {\n old: \"tsc -p ./tsconfig.server.json --noEmit && tsc -p ./tsconfig.client.json --noEmit\",\n new: \"tsc -b tsconfig.server.json && tsc -b tsconfig.client.json\",\n },\n};\n\nasync function migratePackageJsonScripts(\n projectRoot: string,\n): Promise<MigrationResult> {\n const results: MigrationResult = [];\n const filePath = path.join(projectRoot, \"package.json\");\n\n try {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const parsed = JSON.parse(raw);\n const scripts = parsed.scripts;\n if (!scripts) return results;\n\n const updated: string[] = [];\n\n for (const [name, { old, new: replacement }] of Object.entries(\n SCRIPT_MIGRATIONS,\n )) {\n if (scripts[name] === old) {\n scripts[name] = replacement;\n updated.push(name);\n }\n }\n\n if (updated.length === 0) return results;\n\n const indent = raw.match(/^\\s+/m)?.[0]?.length === 4 ? 4 : 2;\n await fs.writeFile(\n filePath,\n `${JSON.stringify(parsed, null, indent)}\\n`,\n \"utf-8\",\n );\n results.push({\n file: \"package.json\",\n action: `updated ${updated.join(\" and \")} scripts`,\n });\n } catch (err) {\n logger.warn(\n \"Failed to migrate package.json scripts: %s\",\n (err as Error).message,\n );\n }\n\n return results;\n}\n\n// ── Summary ─────────────────────────────────────────────────────────────\n\nfunction printMigrationSummary(\n results: Array<{ file: string; action: string }>,\n) {\n const separator = pc.dim(\"─\".repeat(50));\n console.log(\"\");\n console.log(` ${pc.bold(\"Typegen Migration\")}`);\n console.log(` ${separator}`);\n for (const { file, action } of results) {\n console.log(` ${pc.green(\"✓\")} ${file.padEnd(24)} ${pc.dim(action)}`);\n }\n console.log(` ${separator}`);\n console.log(\"\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"migration.js","names":[],"sources":["../../src/type-generator/migration.ts"],"sourcesContent":["import fsSync from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport pc from \"picocolors\";\n\nimport { createLogger } from \"../logging/logger\";\n\nconst logger = createLogger(\"type-generator:migration\");\n\n/**\n * Derive project root from an outFile path.\n * outFile is always `<projectRoot>/shared/appkit-types/<file>` — both the Vite plugins\n * and the CLI construct it this way, so going up two levels is safe.\n *\n * Validates that the resolved root contains a package.json — if not, logs a warning\n * so custom outFile paths don't silently operate on the wrong directory.\n */\nexport function resolveProjectRoot(outFile: string): string {\n const root = path.resolve(path.dirname(outFile), \"..\", \"..\");\n if (!fsSync.existsSync(path.join(root, \"package.json\"))) {\n logger.warn(\n \"Resolved project root %s has no package.json — migration may target the wrong directory. \" +\n \"Check your outFile path: %s\",\n root,\n outFile,\n );\n }\n return root;\n}\n\n/**\n * Remove old generated types from client/src/ (pre-shared/ location).\n * Best-effort: silently ignores missing files.\n */\nexport async function removeOldGeneratedTypes(\n projectRoot: string,\n filename: string,\n): Promise<void> {\n const oldFile = path.join(projectRoot, \"client\", \"src\", filename);\n try {\n await fs.unlink(oldFile);\n logger.debug(\"Removed old types at %s\", oldFile);\n } catch {\n // File doesn't exist — nothing to clean up\n }\n}\n\n// ── Project config migration ────────────────────────────────────────────\n\nconst migratedProjects = new Set<string>();\n\n/**\n * One-time config migration: update tsconfig and package.json for shared/ types output.\n * Idempotent — each sub-migration checks current file state and skips if already migrated.\n * Deduplicates per project root so monorepo builds migrate each app independently.\n * Opt-out: set `\"appkit\": { \"autoMigrate\": false }` in package.json.\n */\nexport async function migrateProjectConfig(projectRoot: string): Promise<void> {\n const resolved = path.resolve(projectRoot);\n if (migratedProjects.has(resolved)) return;\n migratedProjects.add(resolved);\n\n if (await isAutoMigrateDisabled(projectRoot)) {\n logger.debug(\"Auto-migration disabled via package.json appkit.autoMigrate\");\n return;\n }\n\n const results: Array<{ file: string; action: string }> = [];\n\n results.push(...(await migrateTsconfigClient(projectRoot)));\n results.push(...(await migrateTsconfigServer(projectRoot)));\n results.push(...(await migratePackageJsonScripts(projectRoot)));\n\n if (results.length > 0) {\n printMigrationSummary(results);\n }\n}\n\n/** Exported for testing only. */\nexport function _resetMigrationState(): void {\n migratedProjects.clear();\n}\n\n// ── Helpers ─────────────────────────────────────────────────────────────\n\nasync function isAutoMigrateDisabled(projectRoot: string): Promise<boolean> {\n try {\n const raw = await fs.readFile(\n path.join(projectRoot, \"package.json\"),\n \"utf-8\",\n );\n const parsed = JSON.parse(raw);\n return parsed.appkit?.autoMigrate === false;\n } catch {\n return false;\n }\n}\n\n/** Strip JSONC comments (block and line) so JSON.parse can handle tsconfig files. */\nfunction stripJsonComments(text: string): string {\n // Match strings (to skip them) or comments (to remove them).\n // Strings must be matched first to avoid stripping comment-like patterns inside string values\n // (e.g. \"server/**/*\" contains /* which looks like a block comment start).\n return text.replace(/\"(?:[^\"\\\\]|\\\\.)*\"|\\/\\*[\\s\\S]*?\\*\\/|\\/\\/.*/g, (match) =>\n match.startsWith('\"') ? match : \"\",\n );\n}\n\ntype MigrationResult = Array<{ file: string; action: string }>;\n\n// ── tsconfig.client.json ────────────────────────────────────────────────\n\nasync function migrateTsconfigClient(\n projectRoot: string,\n): Promise<MigrationResult> {\n const results: MigrationResult = [];\n const filePath = path.join(projectRoot, \"tsconfig.client.json\");\n\n try {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const parsed = JSON.parse(stripJsonComments(raw));\n\n if (!Array.isArray(parsed.include)) return results;\n if (parsed.include.includes(\"shared/appkit-types\")) return results;\n\n parsed.include.push(\"shared/appkit-types\");\n await fs.writeFile(\n filePath,\n `${JSON.stringify(parsed, null, 2)}\\n`,\n \"utf-8\",\n );\n results.push({\n file: \"tsconfig.client.json\",\n action: 'added \"shared/appkit-types\" to include',\n });\n } catch (err) {\n logger.warn(\n \"Failed to migrate tsconfig.client.json: %s\",\n (err as Error).message,\n );\n }\n\n return results;\n}\n\n// ── tsconfig.server.json ────────────────────────────────────────────────\n\nasync function migrateTsconfigServer(\n projectRoot: string,\n): Promise<MigrationResult> {\n const results: MigrationResult = [];\n const filePath = path.join(projectRoot, \"tsconfig.server.json\");\n\n try {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const parsed = JSON.parse(stripJsonComments(raw));\n const opts = parsed.compilerOptions;\n\n if (!opts || !opts.outDir) return results; // already migrated or non-standard\n\n delete opts.outDir;\n delete opts.declaration;\n delete opts.declarationMap;\n delete opts.sourceMap;\n opts.noEmit = true;\n\n await fs.writeFile(\n filePath,\n `${JSON.stringify(parsed, null, 2)}\\n`,\n \"utf-8\",\n );\n results.push({\n file: \"tsconfig.server.json\",\n action: \"switched to noEmit mode\",\n });\n } catch (err) {\n logger.warn(\n \"Failed to migrate tsconfig.server.json: %s\",\n (err as Error).message,\n );\n }\n\n return results;\n}\n\n// ── package.json ────────────────────────────────────────────────────────\n\nconst SCRIPT_MIGRATIONS: Record<string, { old: string; new: string }> = {\n \"build:server\": {\n old: \"tsdown -c tsdown.server.config.ts\",\n new: \"tsc -b tsconfig.server.json && tsdown -c tsdown.server.config.ts\",\n },\n typecheck: {\n old: \"tsc -p ./tsconfig.server.json --noEmit && tsc -p ./tsconfig.client.json --noEmit\",\n new: \"tsc -b tsconfig.server.json && tsc -b tsconfig.client.json\",\n },\n};\n\nasync function migratePackageJsonScripts(\n projectRoot: string,\n): Promise<MigrationResult> {\n const results: MigrationResult = [];\n const filePath = path.join(projectRoot, \"package.json\");\n\n try {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const parsed = JSON.parse(raw);\n const scripts = parsed.scripts;\n if (!scripts) return results;\n\n const updated: string[] = [];\n\n for (const [name, { old, new: replacement }] of Object.entries(\n SCRIPT_MIGRATIONS,\n )) {\n if (scripts[name] === old) {\n scripts[name] = replacement;\n updated.push(name);\n }\n }\n\n if (updated.length === 0) return results;\n\n const indent = raw.match(/^\\s+/m)?.[0]?.length === 4 ? 4 : 2;\n await fs.writeFile(\n filePath,\n `${JSON.stringify(parsed, null, indent)}\\n`,\n \"utf-8\",\n );\n results.push({\n file: \"package.json\",\n action: `updated ${updated.join(\" and \")} scripts`,\n });\n } catch (err) {\n logger.warn(\n \"Failed to migrate package.json scripts: %s\",\n (err as Error).message,\n );\n }\n\n return results;\n}\n\n// ── Summary ─────────────────────────────────────────────────────────────\n\nfunction printMigrationSummary(\n results: Array<{ file: string; action: string }>,\n) {\n const separator = pc.dim(\"─\".repeat(50));\n console.log(\"\");\n console.log(` ${pc.bold(\"Typegen Migration\")}`);\n console.log(` ${separator}`);\n for (const { file, action } of results) {\n console.log(` ${pc.green(\"✓\")} ${file.padEnd(24)} ${pc.dim(action)}`);\n }\n console.log(` ${separator}`);\n console.log(\"\");\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createLogger } from "../../logging/logger.js";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import
|
|
3
|
+
import fsSync from "node:fs";
|
|
4
4
|
import { Lang, parse } from "@ast-grep/napi";
|
|
5
5
|
|
|
6
6
|
//#region src/type-generator/serving/server-file-extractor.ts
|
|
@@ -20,7 +20,7 @@ const SERVER_FILE_CANDIDATES = ["server/index.ts", "server/server.ts"];
|
|
|
20
20
|
function findServerFile(basePath) {
|
|
21
21
|
for (const candidate of SERVER_FILE_CANDIDATES) {
|
|
22
22
|
const fullPath = path.join(basePath, candidate);
|
|
23
|
-
if (
|
|
23
|
+
if (fsSync.existsSync(fullPath)) return fullPath;
|
|
24
24
|
}
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
@@ -35,7 +35,7 @@ function findServerFile(basePath) {
|
|
|
35
35
|
function extractServingEndpoints(serverFilePath) {
|
|
36
36
|
let content;
|
|
37
37
|
try {
|
|
38
|
-
content =
|
|
38
|
+
content = fsSync.readFileSync(serverFilePath, "utf-8");
|
|
39
39
|
} catch {
|
|
40
40
|
logger.debug("Could not read server file: %s", serverFilePath);
|
|
41
41
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-file-extractor.js","names":[],"sources":["../../../src/type-generator/serving/server-file-extractor.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { Lang, parse, type SgNode } from \"@ast-grep/napi\";\n\nimport { createLogger } from \"../../logging/logger\";\nimport type { EndpointConfig } from \"../../plugins/serving/types\";\n\nconst logger = createLogger(\"type-generator:serving:extractor\");\n\n/**\n * Candidate paths for the server entry file, relative to the project root.\n * Checked in order; the first that exists is used.\n * Same convention as plugin sync (sync.ts SERVER_FILE_CANDIDATES).\n */\nconst SERVER_FILE_CANDIDATES = [\"server/index.ts\", \"server/server.ts\"];\n\n/**\n * Find the server entry file by checking candidate paths in order.\n *\n * @param basePath - Project root directory to search from\n * @returns Absolute path to the server file, or null if none found\n */\nexport function findServerFile(basePath: string): string | null {\n for (const candidate of SERVER_FILE_CANDIDATES) {\n const fullPath = path.join(basePath, candidate);\n if (fs.existsSync(fullPath)) {\n return fullPath;\n }\n }\n return null;\n}\n\n/**\n * Extract serving endpoint config from a server file by AST-parsing it.\n * Looks for `serving({ endpoints: { alias: { env: \"...\" }, ... } })` calls\n * and extracts the endpoint alias names and their environment variable mappings.\n *\n * @param serverFilePath - Absolute path to the server entry file\n * @returns Extracted endpoint config, or null if not found or not extractable\n */\nexport function extractServingEndpoints(\n serverFilePath: string,\n): Record<string, EndpointConfig> | null {\n let content: string;\n try {\n content = fs.readFileSync(serverFilePath, \"utf-8\");\n } catch {\n logger.debug(\"Could not read server file: %s\", serverFilePath);\n return null;\n }\n\n const lang = serverFilePath.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const ast = parse(lang, content);\n const root = ast.root();\n\n // Find serving(...) call expressions\n const servingCall = findServingCall(root);\n if (!servingCall) {\n logger.debug(\"No serving() call found in %s\", serverFilePath);\n return null;\n }\n\n // Get the first argument (the config object)\n const args = servingCall.field(\"arguments\");\n if (!args) {\n return null;\n }\n\n const configArg = args.children().find((child) => child.kind() === \"object\");\n if (!configArg) {\n // serving() called with no args or non-object arg\n return null;\n }\n\n // Find the \"endpoints\" property in the config object\n const endpointsPair = findProperty(configArg, \"endpoints\");\n if (!endpointsPair) {\n // Config object has no \"endpoints\" property (e.g. serving({ timeout: 5000 }))\n return null;\n }\n\n // Get the value of the endpoints property\n const endpointsValue = getPropertyValue(endpointsPair);\n if (!endpointsValue || endpointsValue.kind() !== \"object\") {\n // endpoints is a variable reference, not an inline object\n logger.debug(\n \"serving() endpoints is not an inline object literal in %s. \" +\n \"Pass endpoints explicitly via appKitServingTypesPlugin({ endpoints }) in vite.config.ts.\",\n serverFilePath,\n );\n return null;\n }\n\n // Extract each endpoint entry\n const endpoints: Record<string, EndpointConfig> = {};\n const pairs = endpointsValue\n .children()\n .filter((child) => child.kind() === \"pair\");\n\n for (const pair of pairs) {\n const entry = extractEndpointEntry(pair);\n if (entry) {\n endpoints[entry.alias] = entry.config;\n }\n }\n\n if (Object.keys(endpoints).length === 0) {\n return null;\n }\n\n logger.debug(\n \"Extracted %d endpoint(s) from %s: %s\",\n Object.keys(endpoints).length,\n serverFilePath,\n Object.keys(endpoints).join(\", \"),\n );\n\n return endpoints;\n}\n\n/**\n * Find the serving() call expression in the AST.\n * Looks for call expressions where the callee identifier is \"serving\".\n */\nfunction findServingCall(root: SgNode): SgNode | null {\n const callExpressions = root.findAll({\n rule: { kind: \"call_expression\" },\n });\n\n for (const call of callExpressions) {\n const callee = call.children()[0];\n if (callee?.kind() === \"identifier\" && callee.text() === \"serving\") {\n return call;\n }\n }\n\n return null;\n}\n\n/**\n * Find a property (pair node) with the given key name in an object expression.\n */\nfunction findProperty(objectNode: SgNode, propertyName: string): SgNode | null {\n const pairs = objectNode\n .children()\n .filter((child) => child.kind() === \"pair\");\n\n for (const pair of pairs) {\n const key = pair.children()[0];\n if (!key) continue;\n\n const keyText =\n key.kind() === \"property_identifier\"\n ? key.text()\n : key.kind() === \"string\"\n ? key.text().replace(/^['\"]|['\"]$/g, \"\")\n : null;\n\n if (keyText === propertyName) {\n return pair;\n }\n }\n\n return null;\n}\n\n/**\n * Get the value node from a pair (property: value).\n * The value is typically the last meaningful child after the colon.\n */\nfunction getPropertyValue(pairNode: SgNode): SgNode | null {\n const children = pairNode.children();\n // pair children: [key, \":\", value]\n return children.length >= 3 ? children[children.length - 1] : null;\n}\n\n/**\n * Extract a single endpoint entry from a pair node like:\n * `demo: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\", servedModel: \"my-model\" }`\n */\nfunction extractEndpointEntry(\n pair: SgNode,\n): { alias: string; config: EndpointConfig } | null {\n const children = pair.children();\n if (children.length < 3) return null;\n\n // Get alias name (the key)\n const keyNode = children[0];\n const alias =\n keyNode.kind() === \"property_identifier\"\n ? keyNode.text()\n : keyNode.kind() === \"string\"\n ? keyNode.text().replace(/^['\"]|['\"]$/g, \"\")\n : null;\n\n if (!alias) return null;\n\n // Get the value (should be an object like { env: \"...\" })\n const valueNode = children[children.length - 1];\n if (valueNode.kind() !== \"object\") return null;\n\n // Extract env field\n const envPair = findProperty(valueNode, \"env\");\n if (!envPair) return null;\n\n const envValue = getPropertyValue(envPair);\n if (!envValue || envValue.kind() !== \"string\") return null;\n\n const env = envValue.text().replace(/^['\"]|['\"]$/g, \"\");\n\n // Extract optional servedModel field\n const config: EndpointConfig = { env };\n const servedModelPair = findProperty(valueNode, \"servedModel\");\n if (servedModelPair) {\n const servedModelValue = getPropertyValue(servedModelPair);\n if (servedModelValue?.kind() === \"string\") {\n config.servedModel = servedModelValue.text().replace(/^['\"]|['\"]$/g, \"\");\n }\n }\n\n return { alias, config };\n}\n"],"mappings":";;;;;;AAQA,MAAM,SAAS,aAAa,mCAAmC;;;;;;AAO/D,MAAM,yBAAyB,CAAC,mBAAmB,mBAAmB;;;;;;;AAQtE,SAAgB,eAAe,UAAiC;AAC9D,MAAK,MAAM,aAAa,wBAAwB;EAC9C,MAAM,WAAW,KAAK,KAAK,UAAU,UAAU;AAC/C,
|
|
1
|
+
{"version":3,"file":"server-file-extractor.js","names":["fs"],"sources":["../../../src/type-generator/serving/server-file-extractor.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { Lang, parse, type SgNode } from \"@ast-grep/napi\";\n\nimport { createLogger } from \"../../logging/logger\";\nimport type { EndpointConfig } from \"../../plugins/serving/types\";\n\nconst logger = createLogger(\"type-generator:serving:extractor\");\n\n/**\n * Candidate paths for the server entry file, relative to the project root.\n * Checked in order; the first that exists is used.\n * Same convention as plugin sync (sync.ts SERVER_FILE_CANDIDATES).\n */\nconst SERVER_FILE_CANDIDATES = [\"server/index.ts\", \"server/server.ts\"];\n\n/**\n * Find the server entry file by checking candidate paths in order.\n *\n * @param basePath - Project root directory to search from\n * @returns Absolute path to the server file, or null if none found\n */\nexport function findServerFile(basePath: string): string | null {\n for (const candidate of SERVER_FILE_CANDIDATES) {\n const fullPath = path.join(basePath, candidate);\n if (fs.existsSync(fullPath)) {\n return fullPath;\n }\n }\n return null;\n}\n\n/**\n * Extract serving endpoint config from a server file by AST-parsing it.\n * Looks for `serving({ endpoints: { alias: { env: \"...\" }, ... } })` calls\n * and extracts the endpoint alias names and their environment variable mappings.\n *\n * @param serverFilePath - Absolute path to the server entry file\n * @returns Extracted endpoint config, or null if not found or not extractable\n */\nexport function extractServingEndpoints(\n serverFilePath: string,\n): Record<string, EndpointConfig> | null {\n let content: string;\n try {\n content = fs.readFileSync(serverFilePath, \"utf-8\");\n } catch {\n logger.debug(\"Could not read server file: %s\", serverFilePath);\n return null;\n }\n\n const lang = serverFilePath.endsWith(\".tsx\") ? Lang.Tsx : Lang.TypeScript;\n const ast = parse(lang, content);\n const root = ast.root();\n\n // Find serving(...) call expressions\n const servingCall = findServingCall(root);\n if (!servingCall) {\n logger.debug(\"No serving() call found in %s\", serverFilePath);\n return null;\n }\n\n // Get the first argument (the config object)\n const args = servingCall.field(\"arguments\");\n if (!args) {\n return null;\n }\n\n const configArg = args.children().find((child) => child.kind() === \"object\");\n if (!configArg) {\n // serving() called with no args or non-object arg\n return null;\n }\n\n // Find the \"endpoints\" property in the config object\n const endpointsPair = findProperty(configArg, \"endpoints\");\n if (!endpointsPair) {\n // Config object has no \"endpoints\" property (e.g. serving({ timeout: 5000 }))\n return null;\n }\n\n // Get the value of the endpoints property\n const endpointsValue = getPropertyValue(endpointsPair);\n if (!endpointsValue || endpointsValue.kind() !== \"object\") {\n // endpoints is a variable reference, not an inline object\n logger.debug(\n \"serving() endpoints is not an inline object literal in %s. \" +\n \"Pass endpoints explicitly via appKitServingTypesPlugin({ endpoints }) in vite.config.ts.\",\n serverFilePath,\n );\n return null;\n }\n\n // Extract each endpoint entry\n const endpoints: Record<string, EndpointConfig> = {};\n const pairs = endpointsValue\n .children()\n .filter((child) => child.kind() === \"pair\");\n\n for (const pair of pairs) {\n const entry = extractEndpointEntry(pair);\n if (entry) {\n endpoints[entry.alias] = entry.config;\n }\n }\n\n if (Object.keys(endpoints).length === 0) {\n return null;\n }\n\n logger.debug(\n \"Extracted %d endpoint(s) from %s: %s\",\n Object.keys(endpoints).length,\n serverFilePath,\n Object.keys(endpoints).join(\", \"),\n );\n\n return endpoints;\n}\n\n/**\n * Find the serving() call expression in the AST.\n * Looks for call expressions where the callee identifier is \"serving\".\n */\nfunction findServingCall(root: SgNode): SgNode | null {\n const callExpressions = root.findAll({\n rule: { kind: \"call_expression\" },\n });\n\n for (const call of callExpressions) {\n const callee = call.children()[0];\n if (callee?.kind() === \"identifier\" && callee.text() === \"serving\") {\n return call;\n }\n }\n\n return null;\n}\n\n/**\n * Find a property (pair node) with the given key name in an object expression.\n */\nfunction findProperty(objectNode: SgNode, propertyName: string): SgNode | null {\n const pairs = objectNode\n .children()\n .filter((child) => child.kind() === \"pair\");\n\n for (const pair of pairs) {\n const key = pair.children()[0];\n if (!key) continue;\n\n const keyText =\n key.kind() === \"property_identifier\"\n ? key.text()\n : key.kind() === \"string\"\n ? key.text().replace(/^['\"]|['\"]$/g, \"\")\n : null;\n\n if (keyText === propertyName) {\n return pair;\n }\n }\n\n return null;\n}\n\n/**\n * Get the value node from a pair (property: value).\n * The value is typically the last meaningful child after the colon.\n */\nfunction getPropertyValue(pairNode: SgNode): SgNode | null {\n const children = pairNode.children();\n // pair children: [key, \":\", value]\n return children.length >= 3 ? children[children.length - 1] : null;\n}\n\n/**\n * Extract a single endpoint entry from a pair node like:\n * `demo: { env: \"DATABRICKS_SERVING_ENDPOINT_NAME\", servedModel: \"my-model\" }`\n */\nfunction extractEndpointEntry(\n pair: SgNode,\n): { alias: string; config: EndpointConfig } | null {\n const children = pair.children();\n if (children.length < 3) return null;\n\n // Get alias name (the key)\n const keyNode = children[0];\n const alias =\n keyNode.kind() === \"property_identifier\"\n ? keyNode.text()\n : keyNode.kind() === \"string\"\n ? keyNode.text().replace(/^['\"]|['\"]$/g, \"\")\n : null;\n\n if (!alias) return null;\n\n // Get the value (should be an object like { env: \"...\" })\n const valueNode = children[children.length - 1];\n if (valueNode.kind() !== \"object\") return null;\n\n // Extract env field\n const envPair = findProperty(valueNode, \"env\");\n if (!envPair) return null;\n\n const envValue = getPropertyValue(envPair);\n if (!envValue || envValue.kind() !== \"string\") return null;\n\n const env = envValue.text().replace(/^['\"]|['\"]$/g, \"\");\n\n // Extract optional servedModel field\n const config: EndpointConfig = { env };\n const servedModelPair = findProperty(valueNode, \"servedModel\");\n if (servedModelPair) {\n const servedModelValue = getPropertyValue(servedModelPair);\n if (servedModelValue?.kind() === \"string\") {\n config.servedModel = servedModelValue.text().replace(/^['\"]|['\"]$/g, \"\");\n }\n }\n\n return { alias, config };\n}\n"],"mappings":"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"}
|
|
@@ -564,7 +564,7 @@ protected resolveUserId(req: Request): string;
|
|
|
564
564
|
|
|
565
565
|
Resolve the effective user ID from a request.
|
|
566
566
|
|
|
567
|
-
Returns the `x-forwarded-user` header when present. In development mode (`NODE_ENV=development`) falls back to the current context user ID so that callers outside an active `
|
|
567
|
+
Returns the `x-forwarded-user` header when present. In development mode (`NODE_ENV=development`) falls back to the current context user ID so that callers outside an active `runInCallerContext` scope still get a consistent value.
|
|
568
568
|
|
|
569
569
|
#### Parameters[](#parameters-7 "Direct link to Parameters")
|
|
570
570
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Function: getCurrentActorId()
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
function getCurrentActorId(): string | undefined;
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
The initiating user in a caller scope; no user actor exists in service scope.
|
|
9
|
+
|
|
10
|
+
## Returns[](#returns "Direct link to Returns")
|
|
11
|
+
|
|
12
|
+
`string` | `undefined`
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
# Function: getExecutionContext()
|
|
2
2
|
|
|
3
3
|
```ts
|
|
4
|
-
function getExecutionContext():
|
|
4
|
+
function getExecutionContext():
|
|
5
|
+
| ServiceContextState
|
|
6
|
+
| CallerContext & UserContext;
|
|
5
7
|
|
|
6
8
|
```
|
|
7
9
|
|
|
8
10
|
Get the current execution context.
|
|
9
11
|
|
|
10
|
-
* If running inside a
|
|
12
|
+
* If running inside a caller context (via asUser), returns the caller context
|
|
11
13
|
* Otherwise, returns the service context
|
|
12
14
|
|
|
13
15
|
## Returns[](#returns "Direct link to Returns")
|
|
14
16
|
|
|
15
|
-
`
|
|
17
|
+
\| `ServiceContextState` | [`CallerContext`](./docs/api/appkit/Interface.CallerContext.md) & `UserContext`
|
|
16
18
|
|
|
17
19
|
## Throws[](#throws "Direct link to Throws")
|
|
18
20
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Function: getWarehouseId()
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
function getWarehouseId(): Promise<string>;
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
Get the configured SQL warehouse ID after app initialization. The warehouse is an app resource; SP and caller executions use the same binding. Deprecated user-context scopes retain support for explicit warehouse overrides.
|
|
9
|
+
|
|
10
|
+
## Returns[](#returns "Direct link to Returns")
|
|
11
|
+
|
|
12
|
+
`Promise`<`string`>
|
|
13
|
+
|
|
14
|
+
## Throws[](#throws "Direct link to Throws")
|
|
15
|
+
|
|
16
|
+
ConfigurationError if no SQL warehouse was required at startup.
|
|
17
|
+
|
|
18
|
+
## Throws[](#throws-1 "Direct link to Throws")
|
|
19
|
+
|
|
20
|
+
InitializationError if the app resources are not initialized.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Interface: CallerContext
|
|
2
|
+
|
|
3
|
+
Caller identity and workspace for one immutable execution scope.
|
|
4
|
+
|
|
5
|
+
## Properties[](#properties "Direct link to Properties")
|
|
6
|
+
|
|
7
|
+
### client[](#client "Direct link to client")
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
readonly client: WorkspaceClient;
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
***
|
|
15
|
+
|
|
16
|
+
### principal[](#principal "Direct link to principal")
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
readonly principal: CallerPrincipal;
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
***
|
|
24
|
+
|
|
25
|
+
### tokenFingerprint?[](#tokenfingerprint "Direct link to tokenFingerprint?")
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
readonly optional tokenFingerprint: string;
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Truncated SHA-256 hash of the caller token, used to detect rotation.
|
|
33
|
+
|
|
34
|
+
***
|
|
35
|
+
|
|
36
|
+
### workspaceId[](#workspaceid "Direct link to workspaceId")
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
readonly workspaceId: Promise<string>;
|
|
40
|
+
|
|
41
|
+
```
|
|
@@ -323,6 +323,29 @@ Omit.scaffolding
|
|
|
323
323
|
|
|
324
324
|
***
|
|
325
325
|
|
|
326
|
+
### scopes?[](#scopes "Direct link to scopes?")
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
optional scopes: (
|
|
330
|
+
| "ai-gateway"
|
|
331
|
+
| "mcp.external"
|
|
332
|
+
| "mcp.functions"
|
|
333
|
+
| "workspace.workspace"
|
|
334
|
+
| "catalog.catalogs:read"
|
|
335
|
+
| "catalog.schemas:read"
|
|
336
|
+
| "catalog.tables:read")[];
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
#### Inherited from[](#inherited-from-11 "Direct link to Inherited from")
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
Omit.scopes
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
***
|
|
348
|
+
|
|
326
349
|
### stability?[](#stability "Direct link to stability?")
|
|
327
350
|
|
|
328
351
|
```ts
|
|
@@ -332,7 +355,7 @@ optional stability: "beta" | "ga";
|
|
|
332
355
|
|
|
333
356
|
Plugin stability level. Beta plugins may have breaking API changes between minor releases but are on a path to GA. GA (general availability) plugins follow semver strictly.
|
|
334
357
|
|
|
335
|
-
#### Inherited from[](#inherited-from-
|
|
358
|
+
#### Inherited from[](#inherited-from-12 "Direct link to Inherited from")
|
|
336
359
|
|
|
337
360
|
```ts
|
|
338
361
|
Omit.stability
|
|
@@ -350,7 +373,7 @@ optional version: string;
|
|
|
350
373
|
|
|
351
374
|
Plugin version (semver format)
|
|
352
375
|
|
|
353
|
-
#### Inherited from[](#inherited-from-
|
|
376
|
+
#### Inherited from[](#inherited-from-13 "Direct link to Inherited from")
|
|
354
377
|
|
|
355
378
|
```ts
|
|
356
379
|
Omit.version
|