@databricks/appkit 0.75.0 → 0.76.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 +2 -2
- package/dist/appkit/package.js +1 -1
- package/dist/beta.d.ts +1 -1
- package/dist/beta.js +2 -1
- package/dist/cache/index.d.ts +10 -0
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +15 -2
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/storage/persistent.js +1 -1
- package/dist/connectors/index.js +1 -1
- package/dist/connectors/jobs/client.js +1 -1
- package/dist/connectors/lakebase/index.d.ts +2 -2
- package/dist/connectors/lakebase/index.d.ts.map +1 -1
- package/dist/connectors/lakebase/index.js +25 -2
- package/dist/connectors/lakebase/index.js.map +1 -1
- package/dist/connectors/mcp/client.js +1 -1
- package/dist/connectors/sql-warehouse/client.js +14 -2
- package/dist/connectors/sql-warehouse/client.js.map +1 -1
- package/dist/context/service-context.js +1 -1
- package/dist/core/appkit.d.ts +1 -1
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +27 -6
- package/dist/core/appkit.js.map +1 -1
- package/dist/core/lifecycle-manager.js +47 -27
- package/dist/core/lifecycle-manager.js.map +1 -1
- package/dist/database/errors.js +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin/dev-reader.js +1 -1
- package/dist/plugin/interceptors/retry.js +1 -1
- package/dist/plugin/plugin.js +1 -1
- package/dist/plugins/analytics/analytics.js +1 -1
- package/dist/plugins/analytics/result-delivery.js +1 -1
- package/dist/plugins/beta-exports.generated.d.ts +1 -3
- package/dist/plugins/beta-exports.generated.js +0 -2
- package/dist/plugins/database/config.js +14 -0
- package/dist/plugins/database/config.js.map +1 -0
- package/dist/plugins/database/database.d.ts +22 -5
- package/dist/plugins/database/database.d.ts.map +1 -1
- package/dist/plugins/database/database.js +23 -8
- package/dist/plugins/database/database.js.map +1 -1
- package/dist/plugins/database/lifecycle.js +3 -3
- package/dist/plugins/database/lifecycle.js.map +1 -1
- package/dist/plugins/database/load-schema.js +40 -0
- package/dist/plugins/database/load-schema.js.map +1 -0
- package/dist/plugins/database/manifest.js +1 -1
- package/dist/plugins/database/types.d.ts +12 -3
- package/dist/plugins/database/types.d.ts.map +1 -1
- package/dist/plugins/files/plugin.js +1 -1
- package/dist/plugins/jobs/plugin.js +1 -1
- package/dist/plugins/lakebase/lakebase.js +1 -1
- package/dist/plugins/server/index.js +3 -3
- 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 +5 -5
- package/dist/plugins/server/vite-dev-server.js.map +1 -1
- package/dist/registry/manifest-loader.js +1 -1
- package/dist/registry/resource-registry.js +1 -1
- package/dist/shared/src/schemas/manifest.d.ts +33 -33
- package/dist/stream/arrow-stream-processor.js +1 -1
- package/dist/stream/stream-manager.js +1 -1
- package/dist/telemetry/telemetry-manager.js +17 -0
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/testing/create-test-app.d.ts +111 -0
- package/dist/testing/create-test-app.d.ts.map +1 -0
- package/dist/testing/create-test-app.js +187 -0
- package/dist/testing/create-test-app.js.map +1 -0
- package/dist/testing/create-test-plugin.d.ts +17 -0
- package/dist/testing/create-test-plugin.d.ts.map +1 -0
- package/dist/testing/create-test-plugin.js +22 -0
- package/dist/testing/create-test-plugin.js.map +1 -0
- package/dist/testing/fixtures.d.ts +51 -35
- package/dist/testing/fixtures.d.ts.map +1 -1
- package/dist/testing/fixtures.js +129 -54
- package/dist/testing/fixtures.js.map +1 -1
- package/dist/testing/index.d.ts +8 -3
- package/dist/testing/index.js +7 -2
- package/dist/testing/mock-workspace-client.d.ts +47 -0
- package/dist/testing/mock-workspace-client.d.ts.map +1 -0
- package/dist/testing/mock-workspace-client.js +192 -0
- package/dist/testing/mock-workspace-client.js.map +1 -0
- package/dist/testing/reset-singletons.js +39 -0
- package/dist/testing/reset-singletons.js.map +1 -0
- package/dist/testing/test-app.d.ts +54 -0
- package/dist/testing/test-app.d.ts.map +1 -0
- package/dist/testing/test-app.js +55 -0
- package/dist/testing/test-app.js.map +1 -0
- package/dist/testing/test-cache.d.ts +60 -0
- package/dist/testing/test-cache.d.ts.map +1 -0
- package/dist/testing/test-cache.js +65 -0
- package/dist/testing/test-cache.js.map +1 -0
- package/dist/testing/test-plugin-context.d.ts +43 -2
- package/dist/testing/test-plugin-context.d.ts.map +1 -1
- package/dist/testing/test-plugin-context.js +47 -2
- package/dist/testing/test-plugin-context.js.map +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/Function.createApp.md +9 -9
- package/docs/api/appkit/Function.database.md +30 -29
- package/docs/api/appkit/TypeAlias.IDatabaseConfig.md +8 -6
- package/docs/api/appkit.md +1 -1
- package/docs/plugins/database.md +46 -18
- package/docs/plugins/testing.md +440 -14
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle-manager.js","names":[],"sources":["../../src/core/lifecycle-manager.ts"],"sourcesContent":["import type { BasePlugin } from \"shared\";\n\nimport { CacheManager } from \"../cache\";\nimport { TelemetryReporter } from \"../internal-telemetry\";\nimport { createLogger } from \"../logging/logger\";\nimport { TelemetryManager } from \"../telemetry\";\nimport type { PluginContext } from \"./plugin-context\";\n\nconst logger = createLogger(\"lifecycle\");\n\n/**\n * Owns the process's graceful-shutdown sequence.\n *\n * Created by AppKit core once every plugin has started. It is the single\n * owner of the SIGTERM/SIGINT handlers and of `process.exit`, mirroring the\n * core-owned startup in `AppKit._createApp`: core initializes telemetry,\n * cache, and the internal-telemetry reporter, and core tears them all down\n * here. Plugins participate through the generic hooks\n * (`abortActiveOperations()`, `shutdown()`, and `onLifecycle(\"shutdown\")`) —\n * they do not touch process signals or the core singletons themselves.\n */\nexport class LifecycleManager {\n /**\n * Overall graceful-shutdown budget before the process is force-exited.\n *\n * Budget arithmetic: plugin `shutdown()` hooks run concurrently and are\n * bounded by {@link PLUGIN_SHUTDOWN_TIMEOUT_MS} (10s); the lifecycle emit\n * is bounded by {@link PHASE_SHUTDOWN_TIMEOUT_MS} (2s); the cache storage\n * close and the telemetry flush run concurrently, each bounded by\n * {@link PHASE_SHUTDOWN_TIMEOUT_MS} (2s). Worst case is\n * 10s + 2s + max(2s, 2s) = 14s, leaving ~1s of margin for the remaining\n * steps (aborts) before this timer force-exits.\n */\n private static readonly SHUTDOWN_TIMEOUT_MS = 15_000;\n /**\n * Per-plugin budget for `shutdown()` hooks. Sized to cover the longest\n * built-in drain (the files plugin waits up to 10s for in-flight writes).\n */\n private static readonly PLUGIN_SHUTDOWN_TIMEOUT_MS = 10_000;\n /**\n * Budget for each non-plugin shutdown phase (the `\"shutdown\"` lifecycle\n * emit, the cache storage close, and the telemetry flush). Keeps the\n * worst-case total under {@link SHUTDOWN_TIMEOUT_MS} — see the arithmetic\n * there.\n */\n private static readonly PHASE_SHUTDOWN_TIMEOUT_MS = 2_000;\n\n /**\n * Guards against re-entrant shutdown (e.g. SIGTERM followed by SIGINT).\n * The flag set in `shutdown` must remain synchronous and first — any\n * `await` before it would open a window for a second signal to re-enter\n * the sequence.\n */\n private isShuttingDown = false;\n /**\n * Name of the shutdown phase currently in flight, so the force-exit log\n * can say where shutdown got stuck without extra bookkeeping.\n */\n private shutdownPhase = \"not started\";\n\n constructor(private readonly context: PluginContext) {}\n\n /**\n * Install the SIGTERM/SIGINT handlers that trigger {@link shutdown}.\n *\n * Uses `process.once` (not `on`) so a repeated signal cannot register the\n * handler twice; re-entrancy from a *different* signal is guarded by\n * `isShuttingDown` inside {@link shutdown}.\n */\n installSignalHandlers(): void {\n process.once(\"SIGTERM\", () => this.shutdown());\n process.once(\"SIGINT\", () => this.shutdown());\n }\n\n /**\n * Run the graceful-shutdown sequence and exit the process.\n *\n * Phases:\n * 1. stop the internal-telemetry reporter\n * 2. abort in-flight work on every plugin (cancellation only — teardown of\n * shared resources belongs in `shutdown()` so peers can still drain)\n * 3. run every plugin's `shutdown()` hook concurrently, each bounded\n * 4. emit the `\"shutdown\"` lifecycle event, bounded\n * 5. close the cache storage and flush telemetry concurrently, each bounded\n *\n * Exits 0 on completion (and on the force-exit backstop): a deliberate\n * shutdown is not a crash. Exit 1 is reserved for an unexpected error\n * thrown by the sequence itself.\n */\n async shutdown(): Promise<void> {\n // Must stay synchronous and first: any await before the flag is set\n // would let a second signal re-enter the shutdown sequence.\n if (this.isShuttingDown) return;\n this.isShuttingDown = true;\n\n logger.info(\"Starting graceful shutdown...\");\n\n let exitCode = 0;\n\n // Force exit once the overall budget is spent. Exit 0 is deliberate:\n // a force-timeout still happens on a routine deploy (deliberate\n // shutdown, not a crash), and orchestrators record nonzero exits on\n // deploys as crashes. The error log below is the stuck-shutdown\n // signal instead of the exit code.\n const forceExitTimer = setTimeout(() => {\n logger.error(\n \"Graceful shutdown did NOT complete within the %dms budget (phase in flight: %s); force-exiting with code 0.\",\n LifecycleManager.SHUTDOWN_TIMEOUT_MS,\n this.shutdownPhase,\n );\n process.exit(0);\n }, LifecycleManager.SHUTDOWN_TIMEOUT_MS);\n // unref so this backstop timer never by itself keeps the process alive.\n // Any real pending teardown (OTEL export timer, DB pool sockets, the\n // still-open HTTP listener) is a ref'd handle that holds the loop open\n // until this fires; if nothing is ref'd, there is nothing left to tear\n // down and exiting early is correct.\n forceExitTimer.unref();\n\n try {\n const plugins = Array.from(this.context.getPlugins().values());\n\n // 1. stop the internal-telemetry reporter (no-op if never started).\n this.shutdownPhase = \"stopping internal telemetry reporter\";\n TelemetryReporter.getInstance()?.stop();\n\n // 2. abort active operations from plugins (in-flight executions, SSE\n // streams). Cancellation only — resource teardown (e.g. the\n // lakebase pools, the server's socket close) belongs in plugin\n // shutdown() hooks / lifecycle subscribers so other plugins can\n // still drain state through them.\n this.shutdownPhase = \"aborting active operations\";\n for (const plugin of plugins) {\n if (plugin.abortActiveOperations) {\n try {\n plugin.abortActiveOperations();\n } catch (err) {\n logger.error(\n \"Error aborting operations for plugin %s: %O\",\n plugin.name,\n err,\n );\n }\n }\n }\n\n // 3. run every plugin's shutdown() hook concurrently, each bounded\n // by a per-plugin timeout so one hung plugin cannot stall exit.\n this.shutdownPhase = \"plugin shutdown() hooks\";\n await Promise.all(\n plugins\n .filter((plugin) => typeof plugin.shutdown === \"function\")\n .map((plugin) => this.runPluginShutdown(plugin)),\n );\n\n // 4. notify lifecycle subscribers, bounded so a slow subscriber\n // cannot eat the remaining budget. The server plugin closes its\n // remaining sockets here, after other plugins have drained.\n this.shutdownPhase = \"shutdown lifecycle emit\";\n try {\n await this.raceWithTimeout(\n this.context.emitLifecycle(\"shutdown\"),\n LifecycleManager.PHASE_SHUTDOWN_TIMEOUT_MS,\n \"shutdown lifecycle emit\",\n );\n } catch (err) {\n logger.error(\"Error emitting shutdown lifecycle event: %O\", err);\n }\n\n // 5. close the cache manager's storage (drains the persistent\n // Lakebase pool; no-op for in-memory storage) and flush telemetry.\n // Runs after the lifecycle emit so subscribers can still read the\n // cache. The two are independent (the flush never touches the\n // cache), so they run concurrently — each bounded so a stuck pool\n // drain or stalled OTLP export cannot eat the remaining budget.\n this.shutdownPhase = \"cache storage close + telemetry flush\";\n await Promise.all([this.closeCacheStorage(), this.flushTelemetry()]);\n\n logger.info(\"Graceful shutdown complete\");\n } catch (err) {\n // Exit 1 is reserved for an unexpected error thrown by the sequence\n // itself; every per-phase failure above is already caught and logged.\n logger.error(\"Error during graceful shutdown: %O\", err);\n exitCode = 1;\n }\n\n clearTimeout(forceExitTimer);\n process.exit(exitCode);\n }\n\n /** Close the cache storage, bounded and error-isolated. */\n private async closeCacheStorage(): Promise<void> {\n let cache: CacheManager;\n try {\n cache = CacheManager.getInstanceSync();\n } catch {\n // Cache was never initialized — nothing to close.\n return;\n }\n try {\n await this.raceWithTimeout(\n cache.close(),\n LifecycleManager.PHASE_SHUTDOWN_TIMEOUT_MS,\n \"cache storage close\",\n );\n } catch (err) {\n logger.error(\"Error closing cache storage during shutdown: %O\", err);\n }\n }\n\n /** Flush and shut down the telemetry SDK, bounded and error-isolated. */\n private async flushTelemetry(): Promise<void> {\n try {\n await this.raceWithTimeout(\n TelemetryManager.getInstance().shutdown(),\n LifecycleManager.PHASE_SHUTDOWN_TIMEOUT_MS,\n \"telemetry flush\",\n );\n } catch (err) {\n logger.error(\"Error flushing telemetry during shutdown: %O\", err);\n }\n }\n\n /**\n * Run a single plugin's `shutdown()` hook bounded by\n * {@link LifecycleManager.PLUGIN_SHUTDOWN_TIMEOUT_MS}. Errors and timeouts\n * are logged but never thrown so one misbehaving plugin cannot block\n * the rest of the shutdown sequence.\n */\n private async runPluginShutdown(plugin: BasePlugin): Promise<void> {\n try {\n await this.raceWithTimeout(\n plugin.shutdown?.(),\n LifecycleManager.PLUGIN_SHUTDOWN_TIMEOUT_MS,\n \"shutdown()\",\n );\n } catch (err) {\n logger.error(\"Error shutting down plugin %s: %O\", plugin.name, err);\n }\n }\n\n /**\n * Race `work` against a timeout. Rejects with a labeled error when the\n * timeout wins. A no-op rejection handler is attached to the work promise\n * before racing so a branch that rejects after the timeout already won\n * does not surface as an unhandledRejection.\n */\n private async raceWithTimeout<T>(\n work: Promise<T> | T,\n timeoutMs: number,\n label: string,\n ): Promise<T> {\n const promise = Promise.resolve(work);\n promise.catch(() => {});\n let timer: NodeJS.Timeout | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timer = setTimeout(\n () => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),\n timeoutMs,\n );\n timer.unref();\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecycle-manager.js","names":[],"sources":["../../src/core/lifecycle-manager.ts"],"sourcesContent":["import type { BasePlugin } from \"shared\";\n\nimport { CacheManager } from \"../cache\";\nimport { TelemetryReporter } from \"../internal-telemetry\";\nimport { createLogger } from \"../logging/logger\";\nimport { TelemetryManager } from \"../telemetry\";\nimport type { PluginContext } from \"./plugin-context\";\n\nconst logger = createLogger(\"lifecycle\");\n\n/**\n * Owns the process's graceful-shutdown sequence.\n *\n * Created by AppKit core once every plugin has started. It is the single\n * owner of the SIGTERM/SIGINT handlers and of `process.exit`, mirroring the\n * core-owned startup in `AppKit._createApp`: core initializes telemetry,\n * cache, and the internal-telemetry reporter, and core tears them all down\n * here. Plugins participate through the generic hooks\n * (`abortActiveOperations()`, `shutdown()`, and `onLifecycle(\"shutdown\")`) —\n * they do not touch process signals or the core singletons themselves.\n */\nexport class LifecycleManager {\n /**\n * Overall graceful-shutdown budget before the process is force-exited.\n *\n * Budget arithmetic: plugin `shutdown()` hooks run concurrently and are\n * bounded by {@link PLUGIN_SHUTDOWN_TIMEOUT_MS} (10s); the lifecycle emit\n * is bounded by {@link PHASE_SHUTDOWN_TIMEOUT_MS} (2s); the cache storage\n * close and the telemetry flush run concurrently, each bounded by\n * {@link PHASE_SHUTDOWN_TIMEOUT_MS} (2s). Worst case is\n * 10s + 2s + max(2s, 2s) = 14s, leaving ~1s of margin for the remaining\n * steps (aborts) before this timer force-exits.\n */\n private static readonly SHUTDOWN_TIMEOUT_MS = 15_000;\n /**\n * Per-plugin budget for `shutdown()` hooks. Sized to cover the longest\n * built-in drain (the files plugin waits up to 10s for in-flight writes).\n */\n private static readonly PLUGIN_SHUTDOWN_TIMEOUT_MS = 10_000;\n /**\n * Budget for each non-plugin shutdown phase (the `\"shutdown\"` lifecycle\n * emit, the cache storage close, and the telemetry flush). Keeps the\n * worst-case total under {@link SHUTDOWN_TIMEOUT_MS} — see the arithmetic\n * there.\n */\n private static readonly PHASE_SHUTDOWN_TIMEOUT_MS = 2_000;\n\n /**\n * The in-flight teardown, memoized. A boolean guard would let a second caller\n * return while teardown was still running — fine for a signal, wrong for the\n * harness path (`{ exit: false }`), which must not resolve before resources\n * are released.\n */\n private teardown: Promise<number> | undefined;\n /** Reported by the force-exit log so a stuck shutdown names its phase. */\n private shutdownPhase = \"not started\";\n\n constructor(private readonly context: PluginContext) {}\n\n /**\n * Install the SIGTERM/SIGINT handlers that trigger {@link shutdown}. Never\n * removed: the signal path exits the process, and the harness opts out of\n * installing them, so nothing accumulates across boots.\n */\n installSignalHandlers(): void {\n process.once(\"SIGTERM\", () => void this.shutdown());\n process.once(\"SIGINT\", () => void this.shutdown());\n }\n\n /**\n * Run the graceful-shutdown sequence. Exits the process unless\n * `exit: false` — the flag the test harness passes so it can tear a booted\n * app down between tests without killing the vitest process.\n *\n * Phases:\n * 1. stop the internal-telemetry reporter\n * 2. abort in-flight work on every plugin (cancellation only — teardown of\n * shared resources belongs in `shutdown()` so peers can still drain)\n * 3. run every plugin's `shutdown()` hook concurrently, each bounded\n * 4. emit the `\"shutdown\"` lifecycle event, bounded\n * 5. close the cache storage and flush telemetry concurrently, each bounded\n *\n * Every phase is individually bounded, so the sequence always completes —\n * `{ exit: false }` therefore needs no outer timeout and an `afterEach`\n * cannot hang on it. A second call joins the first teardown.\n *\n * Exits 0 on completion (and on the force-exit backstop): a deliberate\n * shutdown is not a crash. Exit 1 is reserved for an unexpected error\n * thrown by the sequence itself.\n */\n async shutdown(options: { exit?: boolean } = {}): Promise<void> {\n const exit = options.exit ?? true;\n\n if (!exit) {\n // Harness path: no backstop, no process.exit. The phases are internally\n // bounded, and this is fully awaited before the harness drops the\n // singletons — so phase 5 always acts on this app's own cache/telemetry.\n await this.runPhasesOnce();\n return;\n }\n\n // Exit 0 on force-timeout: a stuck deploy shutdown is not a crash, and\n // orchestrators read nonzero deploy exits as one. The error log is the\n // signal instead. Belt-and-suspenders over the per-phase budgets.\n const forceExitTimer = setTimeout(() => {\n logger.error(\n \"Graceful shutdown did NOT complete within the %dms budget (phase in flight: %s); force-exiting with code 0.\",\n LifecycleManager.SHUTDOWN_TIMEOUT_MS,\n this.shutdownPhase,\n );\n process.exit(0);\n }, LifecycleManager.SHUTDOWN_TIMEOUT_MS);\n // unref'd so the backstop alone never holds the process open; real pending\n // teardown is ref'd and keeps the loop alive until this fires.\n forceExitTimer.unref();\n\n const exitCode = await this.runPhasesOnce();\n\n clearTimeout(forceExitTimer);\n process.exit(exitCode);\n }\n\n /** No `await` between read and assign — that gap is the re-entrancy window. */\n private runPhasesOnce(): Promise<number> {\n this.teardown ??= this.runPhases();\n return this.teardown;\n }\n\n /** Run the phases and report an exit code; no process-termination concerns. */\n private async runPhases(): Promise<number> {\n logger.info(\"Starting graceful shutdown...\");\n\n let exitCode = 0;\n\n try {\n const plugins = Array.from(this.context.getPlugins().values());\n\n // 1. stop the internal-telemetry reporter (no-op if never started).\n this.shutdownPhase = \"stopping internal telemetry reporter\";\n TelemetryReporter.getInstance()?.stop();\n\n // 2. abort active operations from plugins (in-flight executions, SSE\n // streams). Cancellation only — resource teardown (e.g. the\n // lakebase pools, the server's socket close) belongs in plugin\n // shutdown() hooks / lifecycle subscribers so other plugins can\n // still drain state through them.\n this.shutdownPhase = \"aborting active operations\";\n for (const plugin of plugins) {\n if (plugin.abortActiveOperations) {\n try {\n plugin.abortActiveOperations();\n } catch (err) {\n logger.error(\n \"Error aborting operations for plugin %s: %O\",\n plugin.name,\n err,\n );\n }\n }\n }\n\n // 3. run every plugin's shutdown() hook concurrently, each bounded\n // by a per-plugin timeout so one hung plugin cannot stall exit.\n this.shutdownPhase = \"plugin shutdown() hooks\";\n await Promise.all(\n plugins\n .filter((plugin) => typeof plugin.shutdown === \"function\")\n .map((plugin) => this.runPluginShutdown(plugin)),\n );\n\n // 4. notify lifecycle subscribers, bounded so a slow subscriber\n // cannot eat the remaining budget. The server plugin closes its\n // remaining sockets here, after other plugins have drained.\n this.shutdownPhase = \"shutdown lifecycle emit\";\n try {\n await this.raceWithTimeout(\n this.context.emitLifecycle(\"shutdown\"),\n LifecycleManager.PHASE_SHUTDOWN_TIMEOUT_MS,\n \"shutdown lifecycle emit\",\n );\n } catch (err) {\n logger.error(\"Error emitting shutdown lifecycle event: %O\", err);\n }\n\n // 5. close the cache manager's storage (drains the persistent\n // Lakebase pool; no-op for in-memory storage) and flush telemetry.\n // Runs after the lifecycle emit so subscribers can still read the\n // cache. The two are independent (the flush never touches the\n // cache), so they run concurrently — each bounded so a stuck pool\n // drain or stalled OTLP export cannot eat the remaining budget.\n this.shutdownPhase = \"cache storage close + telemetry flush\";\n await Promise.all([this.closeCacheStorage(), this.flushTelemetry()]);\n\n logger.info(\"Graceful shutdown complete\");\n } catch (err) {\n // Exit 1 is reserved for an unexpected error thrown by the sequence\n // itself; every per-phase failure above is already caught and logged.\n logger.error(\"Error during graceful shutdown: %O\", err);\n exitCode = 1;\n }\n\n return exitCode;\n }\n\n /** Bounded and error-isolated. Reads the cache manager at phase-5 time. */\n private async closeCacheStorage(): Promise<void> {\n let cache: CacheManager | undefined;\n try {\n cache = CacheManager.getInstanceSync();\n } catch {\n // Never initialized — nothing to close.\n return;\n }\n try {\n await this.raceWithTimeout(\n cache.close(),\n LifecycleManager.PHASE_SHUTDOWN_TIMEOUT_MS,\n \"cache storage close\",\n );\n } catch (err) {\n logger.error(\"Error closing cache storage during shutdown: %O\", err);\n }\n }\n\n /** Bounded and error-isolated. Reads the telemetry manager at phase-5 time. */\n private async flushTelemetry(): Promise<void> {\n let telemetry: TelemetryManager | undefined;\n try {\n telemetry = TelemetryManager.getInstance();\n } catch {\n // Unavailable or mocked away — nothing to flush.\n return;\n }\n if (!telemetry) return;\n try {\n await this.raceWithTimeout(\n telemetry.shutdown(),\n LifecycleManager.PHASE_SHUTDOWN_TIMEOUT_MS,\n \"telemetry flush\",\n );\n } catch (err) {\n logger.error(\"Error flushing telemetry during shutdown: %O\", err);\n }\n }\n\n /**\n * Run a single plugin's `shutdown()` hook bounded by\n * {@link LifecycleManager.PLUGIN_SHUTDOWN_TIMEOUT_MS}. Errors and timeouts\n * are logged but never thrown so one misbehaving plugin cannot block\n * the rest of the shutdown sequence.\n */\n private async runPluginShutdown(plugin: BasePlugin): Promise<void> {\n try {\n await this.raceWithTimeout(\n plugin.shutdown?.(),\n LifecycleManager.PLUGIN_SHUTDOWN_TIMEOUT_MS,\n \"shutdown()\",\n );\n } catch (err) {\n logger.error(\"Error shutting down plugin %s: %O\", plugin.name, err);\n }\n }\n\n /**\n * Race `work` against a timeout. Rejects with a labeled error when the\n * timeout wins. A no-op rejection handler is attached to the work promise\n * before racing so a branch that rejects after the timeout already won\n * does not surface as an unhandledRejection.\n */\n private async raceWithTimeout<T>(\n work: Promise<T> | T,\n timeoutMs: number,\n label: string,\n ): Promise<T> {\n const promise = Promise.resolve(work);\n promise.catch(() => {});\n let timer: NodeJS.Timeout | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timer = setTimeout(\n () => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),\n timeoutMs,\n );\n timer.unref();\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/database/errors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createLogger } from "../logging/logger.js";
|
|
2
1
|
import { AppKitError } from "../errors/base.js";
|
|
3
2
|
import "../errors/index.js";
|
|
3
|
+
import { createLogger } from "../logging/logger.js";
|
|
4
4
|
|
|
5
5
|
//#region src/database/errors.ts
|
|
6
6
|
const logger = createLogger("database");
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { isSQLTypeMarker, sql } from "./shared/src/sql/helpers.js";
|
|
2
2
|
import { ApiError } from "./shared/src/workspace-client/errors.js";
|
|
3
3
|
import { createWorkspaceClient } from "./shared/src/workspace-client/factory.js";
|
|
4
|
-
import { createLakebasePoolManager } from "./connectors/lakebase/pool-manager.js";
|
|
5
4
|
import { AppKitError } from "./errors/base.js";
|
|
6
5
|
import { AuthenticationError } from "./errors/authentication.js";
|
|
7
6
|
import { ConfigurationError } from "./errors/configuration.js";
|
|
@@ -13,6 +12,7 @@ import { ServerError } from "./errors/server.js";
|
|
|
13
12
|
import { TunnelError } from "./errors/tunnel.js";
|
|
14
13
|
import { ValidationError } from "./errors/validation.js";
|
|
15
14
|
import "./errors/index.js";
|
|
15
|
+
import { createLakebasePoolManager } from "./connectors/lakebase/pool-manager.js";
|
|
16
16
|
import { getExecutionContext } from "./context/execution-context.js";
|
|
17
17
|
import { RequestedClaimsPermissionSet, createLakebasePool, generateDatabaseCredential, getLakebaseOrmConfig, getLakebasePgConfig, getUsernameWithApiLookup, getWorkspaceClient } from "./connectors/lakebase/index.js";
|
|
18
18
|
import { SeverityNumber, SpanStatusCode } from "./telemetry/index.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createLogger } from "../logging/logger.js";
|
|
2
1
|
import { TunnelError } from "../errors/tunnel.js";
|
|
3
2
|
import "../errors/index.js";
|
|
3
|
+
import { createLogger } from "../logging/logger.js";
|
|
4
4
|
import { isRemoteTunnelAllowedByEnv } from "../plugins/server/remote-tunnel/gate.js";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
|
package/dist/plugin/plugin.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { camelToKebab } from "../shared/src/naming.js";
|
|
2
|
-
import { createLogger } from "../logging/logger.js";
|
|
3
2
|
import { AppKitError } from "../errors/base.js";
|
|
4
3
|
import { AuthenticationError } from "../errors/authentication.js";
|
|
5
4
|
import "../errors/index.js";
|
|
6
5
|
import { ServiceContext } from "../context/service-context.js";
|
|
6
|
+
import { createLogger } from "../logging/logger.js";
|
|
7
7
|
import { getCurrentUserId, runInUserContext } from "../context/execution-context.js";
|
|
8
8
|
import { normalizeTelemetryOptions } from "../telemetry/config.js";
|
|
9
9
|
import { TelemetryManager } from "../telemetry/telemetry-manager.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { makeResultMessage } from "../../shared/src/sse/analytics.js";
|
|
2
|
-
import { createLogger } from "../../logging/logger.js";
|
|
3
2
|
import { AppKitError } from "../../errors/base.js";
|
|
4
3
|
import { ExecutionError } from "../../errors/execution.js";
|
|
5
4
|
import "../../errors/index.js";
|
|
5
|
+
import { createLogger } from "../../logging/logger.js";
|
|
6
6
|
import { getWarehouseId, getWorkspaceClient } from "../../context/execution-context.js";
|
|
7
7
|
import "../../context/index.js";
|
|
8
8
|
import { Plugin } from "../../plugin/plugin.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createLogger } from "../../logging/logger.js";
|
|
2
1
|
import { ExecutionError } from "../../errors/execution.js";
|
|
3
2
|
import "../../errors/index.js";
|
|
3
|
+
import { createLogger } from "../../logging/logger.js";
|
|
4
4
|
import { Type, tableFromIPC } from "apache-arrow";
|
|
5
5
|
|
|
6
6
|
//#region src/plugins/analytics/result-delivery.ts
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { agents } from "./agents/agents.js";
|
|
2
2
|
import "./agents/index.js";
|
|
3
3
|
import { aiSearch } from "./ai-search/ai-search.js";
|
|
4
|
-
import "./ai-search/index.js";
|
|
5
|
-
import { database } from "./database/database.js";
|
|
6
|
-
import "./database/index.js";
|
|
4
|
+
import "./ai-search/index.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { databaseSetupFailed } from "../../database/errors.js";
|
|
2
|
+
|
|
3
|
+
//#region src/plugins/database/config.ts
|
|
4
|
+
/** Reject invalid arguments instead of turning them into the default API. */
|
|
5
|
+
function assertDatabaseConfig(config) {
|
|
6
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) throw databaseSetupFailed("Expected a database configuration object or no argument.");
|
|
7
|
+
const prototype = Object.getPrototypeOf(config);
|
|
8
|
+
if (prototype !== Object.prototype && prototype !== null) throw databaseSetupFailed("Expected a plain database configuration object.");
|
|
9
|
+
if ("crudRoutes" in config) throw databaseSetupFailed("\"crudRoutes\" was renamed to \"api\". Use api: false to disable generated routes or api: { writes: false } for reads only.");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { assertDatabaseConfig };
|
|
14
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","names":[],"sources":["../../../src/plugins/database/config.ts"],"sourcesContent":["import { databaseSetupFailed } from \"../../database/errors\";\n\n/** Reject invalid arguments instead of turning them into the default API. */\nexport function assertDatabaseConfig(config: unknown): void {\n if (typeof config !== \"object\" || config === null || Array.isArray(config)) {\n throw databaseSetupFailed(\n \"Expected a database configuration object or no argument.\",\n );\n }\n const prototype = Object.getPrototypeOf(config);\n if (prototype !== Object.prototype && prototype !== null) {\n throw databaseSetupFailed(\n \"Expected a plain database configuration object.\",\n );\n }\n // Do not silently turn a previous opt-out into the default full API.\n if (\"crudRoutes\" in config) {\n throw databaseSetupFailed(\n '\"crudRoutes\" was renamed to \"api\". Use api: false to disable generated routes or api: { writes: false } for reads only.',\n );\n }\n}\n"],"mappings":";;;;AAGA,SAAgB,qBAAqB,QAAuB;AAC1D,KAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,OAAO,CACxE,OAAM,oBACJ,2DACD;CAEH,MAAM,YAAY,OAAO,eAAe,OAAO;AAC/C,KAAI,cAAc,OAAO,aAAa,cAAc,KAClD,OAAM,oBACJ,kDACD;AAGH,KAAI,gBAAgB,OAClB,OAAM,oBACJ,8HACD"}
|
|
@@ -6,12 +6,12 @@ import "../../plugin/index.js";
|
|
|
6
6
|
import { PluginManifest } from "../../registry/types.js";
|
|
7
7
|
import "../../registry/index.js";
|
|
8
8
|
import { DatabaseExports } from "./entity-types.js";
|
|
9
|
-
import { IDatabaseConfig } from "./types.js";
|
|
9
|
+
import { DefaultDatabaseSchema, IDatabaseConfig } from "./types.js";
|
|
10
10
|
import express from "express";
|
|
11
11
|
|
|
12
12
|
//#region src/plugins/database/database.d.ts
|
|
13
13
|
/** Schema-driven database plugin */
|
|
14
|
-
declare class DatabasePlugin<TSchema extends Schema> extends Plugin<IDatabaseConfig<TSchema>> {
|
|
14
|
+
declare class DatabasePlugin<TSchema extends Schema = DefaultDatabaseSchema> extends Plugin<IDatabaseConfig<TSchema>> {
|
|
15
15
|
/** Plugin metadata and required PostgreSQL resource. */
|
|
16
16
|
static manifest: PluginManifest<"database">;
|
|
17
17
|
protected config: IDatabaseConfig<TSchema>;
|
|
@@ -20,7 +20,8 @@ declare class DatabasePlugin<TSchema extends Schema> extends Plugin<IDatabaseCon
|
|
|
20
20
|
private draining;
|
|
21
21
|
private shutdownPromise;
|
|
22
22
|
private exposure;
|
|
23
|
-
|
|
23
|
+
private resolvedSchema;
|
|
24
|
+
constructor(config?: IDatabaseConfig<TSchema>);
|
|
24
25
|
/** Build and verify one candidate state before publishing its exports. */
|
|
25
26
|
setup(): Promise<void>;
|
|
26
27
|
/** Register generated CRUD, subject to the configured table and write restrictions. */
|
|
@@ -34,12 +35,28 @@ declare class DatabasePlugin<TSchema extends Schema> extends Plugin<IDatabaseCon
|
|
|
34
35
|
/** Trace one generated route with allowlisted, low-cardinality attributes. */
|
|
35
36
|
private runRouteSpan;
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
-
declare function database<TSchema extends Schema>(config: IDatabaseConfig<TSchema>): {
|
|
38
|
+
type DatabaseRegistration<TSchema extends Schema> = {
|
|
39
39
|
plugin: PluginConstructor<BasePluginConfig, DatabasePlugin<TSchema>>;
|
|
40
40
|
config: IDatabaseConfig<TSchema>;
|
|
41
41
|
name: "database";
|
|
42
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* Create the database plugin. Omit configuration to load
|
|
45
|
+
* `config/database/schema.ts`, or supply a typed schema override.
|
|
46
|
+
*/
|
|
47
|
+
declare function database<TSchema extends Schema>(config: IDatabaseConfig<TSchema> & {
|
|
48
|
+
readonly schema: TSchema;
|
|
49
|
+
}): DatabaseRegistration<TSchema> & {
|
|
50
|
+
config: IDatabaseConfig<TSchema> & {
|
|
51
|
+
readonly schema: TSchema;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Register the database plugin with opinionated defaults and full HTTP CRUD.
|
|
56
|
+
* By default, setup loads the named `schema` export from the application's
|
|
57
|
+
* `config/database/schema.ts`. Registration itself performs no file or database I/O.
|
|
58
|
+
*/
|
|
59
|
+
declare function database<TSchema extends Schema = DefaultDatabaseSchema>(config?: IDatabaseConfig<TSchema>): DatabaseRegistration<TSchema>;
|
|
43
60
|
//#endregion
|
|
44
61
|
export { database };
|
|
45
62
|
//# sourceMappingURL=database.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","names":[],"sources":["../../../src/plugins/database/database.ts"],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"database.d.ts","names":[],"sources":["../../../src/plugins/database/database.ts"],"mappings":";;;;;;;;;;;;;cAoCa,cAAA,iBACK,MAAA,GAAS,qBAAA,UACjB,MAAA,CAAO,eAAA,CAAgB,OAAA;;SAExB,QAAA,EAAuB,cAAA;EAAA,UACZ,MAAA,EAAQ,eAAA,CAAgB,OAAA;EAAA,QAClC,KAAA;EAAA,QACA,YAAA;EAAA,QACA,QAAA;EAAA,QACA,eAAA;EAAA,QACA,QAAA;EAAA,QACA,cAAA;cAEI,MAAA,GAAQ,eAAA,CAAgB,OAAA;EATN;EAoBxB,KAAA,CAAA,GAAS,OAAA;EAnBW;EAoE1B,YAAA,CAAa,MAAA,EAAQ,OAAA,CAAQ,MAAA;EA5DT;EAAA,QA6HZ,KAAA;EAjEa;EAsErB,OAAA,CAAA,GAOO,eAAA;EAIW;EAAZ,QAAA,CAAA,GAAY,OAAA;EAxJJ;EAAA,QA4KN,YAAA;AAAA;AAAA,KA8BL,oBAAA,iBAAqC,MAAA;EACxC,MAAA,EAAQ,iBAAA,CAAkB,gBAAA,EAAkB,cAAA,CAAe,OAAA;EAC3D,MAAA,EAAQ,eAAA,CAAgB,OAAA;EACxB,IAAA;AAAA;;;;;iBAOc,QAAA,iBAAyB,MAAA,CAAA,CACvC,MAAA,EAAQ,eAAA,CAAgB,OAAA;EAAA,SAAsB,MAAA,EAAQ,OAAA;AAAA,IACrD,oBAAA,CAAqB,OAAA;EACtB,MAAA,EAAQ,eAAA,CAAgB,OAAA;IAAA,SAAsB,MAAA,EAAQ,OAAA;EAAA;AAAA;;;;;;iBAOxC,QAAA,iBAAyB,MAAA,GAAS,qBAAA,CAAA,CAChD,MAAA,GAAS,eAAA,CAAgB,OAAA,IACxB,oBAAA,CAAqB,OAAA"}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Plugin } from "../../plugin/plugin.js";
|
|
2
2
|
import "../../plugin/index.js";
|
|
3
|
+
import { assertFinalizedSchema } from "../../database/schema-builder/define-schema.js";
|
|
3
4
|
import { DatabasePluginError, databaseSetupFailed } from "../../database/errors.js";
|
|
5
|
+
import { assertDatabaseConfig } from "./config.js";
|
|
4
6
|
import { compileCrudTables } from "./crud/contract.js";
|
|
5
7
|
import { resolveCrudExposure } from "./crud/exposure.js";
|
|
6
8
|
import { routeOutcome } from "./crud/response.js";
|
|
7
9
|
import { createCreateHandler, createDeleteHandler, createDetailHandler, createListHandler, createUpdateHandler } from "./crud/routes.js";
|
|
8
10
|
import { createDatabaseState } from "./lifecycle.js";
|
|
11
|
+
import { loadDefaultDatabaseSchema } from "./load-schema.js";
|
|
9
12
|
import manifest_default from "./manifest.js";
|
|
10
13
|
|
|
11
14
|
//#region src/plugins/database/database.ts
|
|
@@ -21,9 +24,10 @@ var DatabasePlugin = class extends Plugin {
|
|
|
21
24
|
tables: [],
|
|
22
25
|
writes: /* @__PURE__ */ new Map()
|
|
23
26
|
};
|
|
24
|
-
|
|
27
|
+
resolvedSchema = null;
|
|
28
|
+
constructor(config = {}) {
|
|
29
|
+
assertDatabaseConfig(config);
|
|
25
30
|
super({ schema: config.schema });
|
|
26
|
-
if ("crudRoutes" in config) throw databaseSetupFailed("\"crudRoutes\" was renamed to \"api\". Use api: false to disable generated routes or api: { writes: false } for reads only.");
|
|
27
31
|
this.config = {
|
|
28
32
|
schema: config.schema,
|
|
29
33
|
api: config.api,
|
|
@@ -34,14 +38,23 @@ var DatabasePlugin = class extends Plugin {
|
|
|
34
38
|
async setup() {
|
|
35
39
|
if (this.draining || this.state) throw databaseSetupFailed();
|
|
36
40
|
if (!this.setupPromise) this.setupPromise = (async () => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
const schema = this.config.schema === void 0 ? await loadDefaultDatabaseSchema() : this.config.schema;
|
|
42
|
+
try {
|
|
43
|
+
assertFinalizedSchema(schema);
|
|
44
|
+
} catch {
|
|
45
|
+
throw databaseSetupFailed("schema must be a finalized AppKit schema created with defineSchema().");
|
|
46
|
+
}
|
|
47
|
+
if (this.draining) throw databaseSetupFailed();
|
|
48
|
+
const exposure = resolveCrudExposure(this.config.api, Object.keys(schema.$tables));
|
|
49
|
+
for (const name of Object.keys(this.hooks() ?? {})) if (!Object.hasOwn(schema.$tables, name)) throw databaseSetupFailed(`hooks names undeclared table ${JSON.stringify(name)}. Use a table declared in schema.`);
|
|
50
|
+
const candidate = await createDatabaseState(schema, (operation, options) => this.execute(operation, options), this.hooks());
|
|
40
51
|
if (this.draining) {
|
|
41
52
|
candidate.deactivate();
|
|
42
53
|
await candidate.pool.end().catch(() => void 0);
|
|
43
54
|
throw databaseSetupFailed();
|
|
44
55
|
}
|
|
56
|
+
this.resolvedSchema = schema;
|
|
57
|
+
this.exposure = exposure;
|
|
45
58
|
this.state = candidate;
|
|
46
59
|
})();
|
|
47
60
|
return this.setupPromise;
|
|
@@ -49,7 +62,9 @@ var DatabasePlugin = class extends Plugin {
|
|
|
49
62
|
/** Register generated CRUD, subject to the configured table and write restrictions. */
|
|
50
63
|
injectRoutes(router) {
|
|
51
64
|
if (this.exposure.tables.length === 0) return;
|
|
52
|
-
const
|
|
65
|
+
const schema = this.resolvedSchema;
|
|
66
|
+
if (!schema) throw databaseSetupFailed();
|
|
67
|
+
const tables = compileCrudTables(Object.fromEntries(this.exposure.tables.map((name) => [name, schema.$tables[name]])));
|
|
53
68
|
const hooks = this.hooks();
|
|
54
69
|
const entities = () => this.exports();
|
|
55
70
|
for (const table of tables.values()) {
|
|
@@ -138,8 +153,8 @@ var DatabasePlugin = class extends Plugin {
|
|
|
138
153
|
});
|
|
139
154
|
}
|
|
140
155
|
};
|
|
141
|
-
|
|
142
|
-
|
|
156
|
+
function database(config = {}) {
|
|
157
|
+
assertDatabaseConfig(config);
|
|
143
158
|
return {
|
|
144
159
|
plugin: DatabasePlugin,
|
|
145
160
|
config,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.js","names":["manifest"],"sources":["../../../src/plugins/database/database.ts"],"sourcesContent":["import type express from \"express\";\nimport type { BasePluginConfig, PluginConstructor } from \"shared\";\n\nimport {\n DatabasePluginError,\n databaseSetupFailed,\n} from \"../../database/errors\";\nimport type { Schema } from \"../../database/schema-builder\";\nimport { Plugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { compileCrudTables } from \"./crud/contract\";\nimport { type CrudExposure, resolveCrudExposure } from \"./crud/exposure\";\nimport { routeOutcome } from \"./crud/response\";\nimport {\n type CrudEntity,\n type CrudOperation,\n type CrudRouteDeps,\n createCreateHandler,\n createDeleteHandler,\n createDetailHandler,\n createListHandler,\n createUpdateHandler,\n} from \"./crud/routes\";\nimport type { DatabaseExports } from \"./entity-types\";\nimport { createDatabaseState, type DatabaseState } from \"./lifecycle\";\nimport manifest from \"./manifest.json\";\nimport type { DatabaseHooks, IDatabaseConfig } from \"./types\";\n\n/** Schema-driven database plugin */\nexport class DatabasePlugin<TSchema extends Schema> extends Plugin<\n IDatabaseConfig<TSchema>\n> {\n /** Plugin metadata and required PostgreSQL resource. */\n static manifest = manifest as PluginManifest<\"database\">;\n declare protected config: IDatabaseConfig<TSchema>;\n private state: DatabaseState | null = null;\n private setupPromise: Promise<void> | null = null;\n private draining = false;\n private shutdownPromise: Promise<void> | null = null;\n private exposure: CrudExposure = { tables: [], writes: new Map() };\n\n constructor(config: IDatabaseConfig<TSchema>) {\n super({ schema: config.schema });\n // Do not silently turn a previous opt-out into the default full API.\n if (\"crudRoutes\" in config) {\n throw databaseSetupFailed(\n '\"crudRoutes\" was renamed to \"api\". Use api: false to disable generated routes or api: { writes: false } for reads only.',\n );\n }\n this.config = {\n schema: config.schema,\n api: config.api,\n hooks: config.hooks,\n };\n }\n\n /** Build and verify one candidate state before publishing its exports. */\n async setup(): Promise<void> {\n if (this.draining || this.state) throw databaseSetupFailed();\n if (!this.setupPromise) {\n const attempt = (async () => {\n this.exposure = resolveCrudExposure(\n this.config.api,\n Object.keys(this.config.schema.$tables),\n );\n // A hook key naming no declared table would silently never run.\n for (const name of Object.keys(this.hooks() ?? {})) {\n if (!Object.hasOwn(this.config.schema.$tables, name)) {\n throw databaseSetupFailed(\n `hooks names undeclared table ${JSON.stringify(name)}. Use a table declared in schema.`,\n );\n }\n }\n const candidate = await createDatabaseState(\n this.config.schema,\n (operation, options) => this.execute(operation, options),\n this.hooks(),\n );\n if (this.draining) {\n // Setup may finish while shutdown is waiting; never publish that state.\n candidate.deactivate();\n await candidate.pool.end().catch(() => undefined);\n throw databaseSetupFailed();\n }\n this.state = candidate;\n })();\n this.setupPromise = attempt;\n }\n return this.setupPromise;\n }\n\n /** Register generated CRUD, subject to the configured table and write restrictions. */\n injectRoutes(router: express.Router): void {\n if (this.exposure.tables.length === 0) return;\n const tables = compileCrudTables(\n Object.fromEntries(\n this.exposure.tables.map((name) => [\n name,\n this.config.schema.$tables[name],\n ]),\n ),\n );\n const hooks = this.hooks();\n // Every exposed name is a declared table, so its export is an entity client.\n const entities = () =>\n this.exports() as unknown as Record<string, CrudEntity>;\n\n for (const table of tables.values()) {\n const deps: CrudRouteDeps = {\n table,\n entity: () => entities()[table.name],\n serialize: hooks?.[table.name]?.serialize,\n runRouteSpan: (operation, route, run) =>\n this.runRouteSpan(table.name, operation, route, run),\n };\n this.route(router, {\n name: `${table.name}.list`,\n method: \"get\",\n path: `/${table.name}`,\n handler: createListHandler(deps),\n });\n const writes = this.exposure.writes.get(table.name);\n if (writes?.has(\"create\")) {\n this.route(router, {\n name: `${table.name}.create`,\n method: \"post\",\n path: `/${table.name}`,\n handler: createCreateHandler(deps),\n });\n }\n // Addressing one row needs a public key.\n if (!table.primaryKey) continue;\n this.route(router, {\n name: `${table.name}.detail`,\n method: \"get\",\n path: `/${table.name}/:id`,\n handler: createDetailHandler(deps),\n });\n if (writes?.has(\"update\")) {\n this.route(router, {\n name: `${table.name}.update`,\n method: \"patch\",\n path: `/${table.name}/:id`,\n handler: createUpdateHandler(deps),\n });\n }\n if (writes?.has(\"delete\")) {\n this.route(router, {\n name: `${table.name}.delete`,\n method: \"delete\",\n path: `/${table.name}/:id`,\n handler: createDeleteHandler(deps),\n });\n }\n }\n }\n\n /** Typed hook keys are schema table names, which routing addresses at runtime. */\n private hooks(): DatabaseHooks | undefined {\n return this.config.hooks as DatabaseHooks | undefined;\n }\n\n /** Return the typed database API only while the plugin is active. */\n exports() {\n if (!this.state || this.draining)\n throw new DatabasePluginError(\"INTERNAL\", \"read\");\n // AppKit binds exported functions onto this object on every access.\n return Object.assign(\n Object.create(null),\n this.state.exports,\n ) as DatabaseExports;\n }\n\n /** Stop new work, wait for setup, and close the owned pool exactly once. */\n async shutdown(): Promise<void> {\n if (this.shutdownPromise) return this.shutdownPromise;\n this.draining = true;\n this.shutdownPromise = (async () => {\n await this.setupPromise?.catch(() => undefined);\n const state = this.state;\n state?.deactivate();\n this.state = null;\n if (state) {\n try {\n await state.pool.end();\n } catch {\n throw new DatabasePluginError(\"INTERNAL\", \"shutdown\");\n }\n }\n })();\n return this.shutdownPromise;\n }\n\n /** Trace one generated route with allowlisted, low-cardinality attributes. */\n private runRouteSpan(\n table: string,\n operation: CrudOperation,\n route: string,\n run: () => Promise<void>,\n ): Promise<void> {\n return this.telemetry.startActiveSpan(\n \"database.crud.route\",\n {\n attributes: {\n table_name: table,\n operation,\n \"http.route\": `/api/${this.name}${route}`,\n },\n },\n async (span) => {\n try {\n await run();\n span.setAttribute(\"outcome\", \"success\");\n } catch (error) {\n span.setAttribute(\"outcome\", routeOutcome(error));\n throw error;\n } finally {\n span.end();\n }\n },\n );\n }\n}\n\n/** Create a typed database plugin registration for a finalized schema. */\nexport function database<TSchema extends Schema>(\n config: IDatabaseConfig<TSchema>,\n) {\n return {\n plugin: DatabasePlugin as unknown as PluginConstructor<\n BasePluginConfig,\n DatabasePlugin<TSchema>\n >,\n config,\n name: \"database\" as const,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"database.js","names":["manifest"],"sources":["../../../src/plugins/database/database.ts"],"sourcesContent":["import type express from \"express\";\nimport type { BasePluginConfig, PluginConstructor } from \"shared\";\n\nimport {\n DatabasePluginError,\n databaseSetupFailed,\n} from \"../../database/errors\";\nimport type { Schema } from \"../../database/schema-builder\";\nimport { assertFinalizedSchema } from \"../../database/schema-builder/define-schema\";\nimport { Plugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { assertDatabaseConfig } from \"./config\";\nimport { compileCrudTables } from \"./crud/contract\";\nimport { type CrudExposure, resolveCrudExposure } from \"./crud/exposure\";\nimport { routeOutcome } from \"./crud/response\";\nimport {\n type CrudEntity,\n type CrudOperation,\n type CrudRouteDeps,\n createCreateHandler,\n createDeleteHandler,\n createDetailHandler,\n createListHandler,\n createUpdateHandler,\n} from \"./crud/routes\";\nimport type { DatabaseExports } from \"./entity-types\";\nimport { createDatabaseState, type DatabaseState } from \"./lifecycle\";\nimport { loadDefaultDatabaseSchema } from \"./load-schema\";\nimport manifest from \"./manifest.json\";\nimport type {\n DatabaseHooks,\n DefaultDatabaseSchema,\n IDatabaseConfig,\n} from \"./types\";\n\n/** Schema-driven database plugin */\nexport class DatabasePlugin<\n TSchema extends Schema = DefaultDatabaseSchema,\n> extends Plugin<IDatabaseConfig<TSchema>> {\n /** Plugin metadata and required PostgreSQL resource. */\n static manifest = manifest as PluginManifest<\"database\">;\n declare protected config: IDatabaseConfig<TSchema>;\n private state: DatabaseState | null = null;\n private setupPromise: Promise<void> | null = null;\n private draining = false;\n private shutdownPromise: Promise<void> | null = null;\n private exposure: CrudExposure = { tables: [], writes: new Map() };\n private resolvedSchema: Schema | null = null;\n\n constructor(config: IDatabaseConfig<TSchema> = {}) {\n assertDatabaseConfig(config);\n super({ schema: config.schema });\n this.config = {\n schema: config.schema,\n api: config.api,\n hooks: config.hooks,\n };\n }\n\n /** Build and verify one candidate state before publishing its exports. */\n async setup(): Promise<void> {\n if (this.draining || this.state) throw databaseSetupFailed();\n if (!this.setupPromise) {\n const attempt = (async () => {\n const schema =\n this.config.schema === undefined\n ? await loadDefaultDatabaseSchema()\n : this.config.schema;\n try {\n assertFinalizedSchema(schema);\n } catch {\n throw databaseSetupFailed(\n \"schema must be a finalized AppKit schema created with defineSchema().\",\n );\n }\n if (this.draining) throw databaseSetupFailed();\n const exposure = resolveCrudExposure(\n this.config.api,\n Object.keys(schema.$tables),\n );\n // A hook key naming no declared table would silently never run.\n for (const name of Object.keys(this.hooks() ?? {})) {\n if (!Object.hasOwn(schema.$tables, name)) {\n throw databaseSetupFailed(\n `hooks names undeclared table ${JSON.stringify(name)}. Use a table declared in schema.`,\n );\n }\n }\n const candidate = await createDatabaseState(\n schema,\n (operation, options) => this.execute(operation, options),\n this.hooks(),\n );\n if (this.draining) {\n // Setup may finish while shutdown is waiting; never publish that state.\n candidate.deactivate();\n await candidate.pool.end().catch(() => undefined);\n throw databaseSetupFailed();\n }\n this.resolvedSchema = schema;\n this.exposure = exposure;\n this.state = candidate;\n })();\n this.setupPromise = attempt;\n }\n return this.setupPromise;\n }\n\n /** Register generated CRUD, subject to the configured table and write restrictions. */\n injectRoutes(router: express.Router): void {\n if (this.exposure.tables.length === 0) return;\n const schema = this.resolvedSchema;\n if (!schema) throw databaseSetupFailed();\n const tables = compileCrudTables(\n Object.fromEntries(\n this.exposure.tables.map((name) => [name, schema.$tables[name]]),\n ),\n );\n const hooks = this.hooks();\n // Every exposed name is a declared table, so its export is an entity client.\n const entities = () =>\n this.exports() as unknown as Record<string, CrudEntity>;\n\n for (const table of tables.values()) {\n const deps: CrudRouteDeps = {\n table,\n entity: () => entities()[table.name],\n serialize: hooks?.[table.name]?.serialize,\n runRouteSpan: (operation, route, run) =>\n this.runRouteSpan(table.name, operation, route, run),\n };\n this.route(router, {\n name: `${table.name}.list`,\n method: \"get\",\n path: `/${table.name}`,\n handler: createListHandler(deps),\n });\n const writes = this.exposure.writes.get(table.name);\n if (writes?.has(\"create\")) {\n this.route(router, {\n name: `${table.name}.create`,\n method: \"post\",\n path: `/${table.name}`,\n handler: createCreateHandler(deps),\n });\n }\n // Addressing one row needs a public key.\n if (!table.primaryKey) continue;\n this.route(router, {\n name: `${table.name}.detail`,\n method: \"get\",\n path: `/${table.name}/:id`,\n handler: createDetailHandler(deps),\n });\n if (writes?.has(\"update\")) {\n this.route(router, {\n name: `${table.name}.update`,\n method: \"patch\",\n path: `/${table.name}/:id`,\n handler: createUpdateHandler(deps),\n });\n }\n if (writes?.has(\"delete\")) {\n this.route(router, {\n name: `${table.name}.delete`,\n method: \"delete\",\n path: `/${table.name}/:id`,\n handler: createDeleteHandler(deps),\n });\n }\n }\n }\n\n /** Typed hook keys are schema table names, which routing addresses at runtime. */\n private hooks(): DatabaseHooks | undefined {\n return this.config.hooks as DatabaseHooks | undefined;\n }\n\n /** Return the typed database API only while the plugin is active. */\n exports() {\n if (!this.state || this.draining)\n throw new DatabasePluginError(\"INTERNAL\", \"read\");\n // AppKit binds exported functions onto this object on every access.\n return Object.assign(\n Object.create(null),\n this.state.exports,\n ) as DatabaseExports;\n }\n\n /** Stop new work, wait for setup, and close the owned pool exactly once. */\n async shutdown(): Promise<void> {\n if (this.shutdownPromise) return this.shutdownPromise;\n this.draining = true;\n this.shutdownPromise = (async () => {\n await this.setupPromise?.catch(() => undefined);\n const state = this.state;\n state?.deactivate();\n this.state = null;\n if (state) {\n try {\n await state.pool.end();\n } catch {\n throw new DatabasePluginError(\"INTERNAL\", \"shutdown\");\n }\n }\n })();\n return this.shutdownPromise;\n }\n\n /** Trace one generated route with allowlisted, low-cardinality attributes. */\n private runRouteSpan(\n table: string,\n operation: CrudOperation,\n route: string,\n run: () => Promise<void>,\n ): Promise<void> {\n return this.telemetry.startActiveSpan(\n \"database.crud.route\",\n {\n attributes: {\n table_name: table,\n operation,\n \"http.route\": `/api/${this.name}${route}`,\n },\n },\n async (span) => {\n try {\n await run();\n span.setAttribute(\"outcome\", \"success\");\n } catch (error) {\n span.setAttribute(\"outcome\", routeOutcome(error));\n throw error;\n } finally {\n span.end();\n }\n },\n );\n }\n}\n\ntype DatabaseRegistration<TSchema extends Schema> = {\n plugin: PluginConstructor<BasePluginConfig, DatabasePlugin<TSchema>>;\n config: IDatabaseConfig<TSchema>;\n name: \"database\";\n};\n\n/**\n * Create the database plugin. Omit configuration to load\n * `config/database/schema.ts`, or supply a typed schema override.\n */\nexport function database<TSchema extends Schema>(\n config: IDatabaseConfig<TSchema> & { readonly schema: TSchema },\n): DatabaseRegistration<TSchema> & {\n config: IDatabaseConfig<TSchema> & { readonly schema: TSchema };\n};\n/**\n * Register the database plugin with opinionated defaults and full HTTP CRUD.\n * By default, setup loads the named `schema` export from the application's\n * `config/database/schema.ts`. Registration itself performs no file or database I/O.\n */\nexport function database<TSchema extends Schema = DefaultDatabaseSchema>(\n config?: IDatabaseConfig<TSchema>,\n): DatabaseRegistration<TSchema>;\nexport function database<TSchema extends Schema = DefaultDatabaseSchema>(\n config: IDatabaseConfig<TSchema> = {},\n): DatabaseRegistration<TSchema> {\n assertDatabaseConfig(config);\n return {\n plugin: DatabasePlugin as unknown as PluginConstructor<\n BasePluginConfig,\n DatabasePlugin<TSchema>\n >,\n config,\n name: \"database\" as const,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createLogger } from "../../logging/logger.js";
|
|
2
1
|
import { DatabaseValidationError } from "../../errors/database-validation.js";
|
|
3
2
|
import "../../errors/index.js";
|
|
4
|
-
import {
|
|
3
|
+
import { createLogger } from "../../logging/logger.js";
|
|
4
|
+
import { initializeLakebasePool } from "../../connectors/lakebase/index.js";
|
|
5
5
|
import { assertFinalizedSchema } from "../../database/schema-builder/define-schema.js";
|
|
6
6
|
import { DatabasePluginError, classifyDatabaseError } from "../../database/errors.js";
|
|
7
7
|
import { IDLE_IN_TRANSACTION_TIMEOUT_MS, STATEMENT_TIMEOUT_MS, TRANSACTION_TIMEOUT_MS } from "./defaults.js";
|
|
@@ -112,7 +112,7 @@ async function createDatabaseState(schema, execute, hooks) {
|
|
|
112
112
|
if (!active) throw new DatabasePluginError("INTERNAL", "runtime");
|
|
113
113
|
};
|
|
114
114
|
try {
|
|
115
|
-
pool =
|
|
115
|
+
pool = await initializeLakebasePool({
|
|
116
116
|
statement_timeout: STATEMENT_TIMEOUT_MS,
|
|
117
117
|
idle_in_transaction_session_timeout: IDLE_IN_TRANSACTION_TIMEOUT_MS
|
|
118
118
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.js","names":[],"sources":["../../../src/plugins/database/lifecycle.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"lifecycle.js","names":[],"sources":["../../../src/plugins/database/lifecycle.ts"],"sourcesContent":["import { initializeLakebasePool } from \"../../connectors/lakebase\";\nimport {\n classifyDatabaseError,\n DatabasePluginError,\n} from \"../../database/errors\";\nimport type { DataPath } from \"../../database/runtime\";\nimport {\n createDrizzleDataPath,\n createDrizzleDb,\n} from \"../../database/runtime/engine/drizzle-data-path\";\nimport type { Schema } from \"../../database/schema-builder\";\nimport { assertFinalizedSchema } from \"../../database/schema-builder/define-schema\";\nimport { DatabaseValidationError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport {\n IDLE_IN_TRANSACTION_TIMEOUT_MS,\n STATEMENT_TIMEOUT_MS,\n TRANSACTION_TIMEOUT_MS,\n} from \"./defaults\";\nimport { EntityClient, type EntityExecute } from \"./entity-client\";\nimport type {\n DatabaseExports,\n SqlTag,\n TransactionClient,\n} from \"./entity-types\";\nimport { createMutationScope, type MutationScope } from \"./scope\";\nimport type { DatabaseHooks } from \"./types\";\n\nconst logger = createLogger(\"database\");\n\n/** Resources owned by one successfully initialized plugin instance. */\nexport interface DatabaseState {\n readonly pool: Awaited<ReturnType<typeof initializeLakebasePool>>;\n readonly exports: DatabaseExports;\n readonly deactivate: () => void;\n}\n\ninterface ExportContext {\n readonly schema: Schema;\n readonly getDataPath: () => DataPath;\n readonly execute: EntityExecute;\n readonly assertActive: () => void;\n readonly scope: MutationScope;\n readonly hooks?: DatabaseHooks;\n readonly transactionBound: boolean;\n}\n\n/** Execute value-only SQL on the bound DataPath without per-statement retries. */\nfunction buildSql(context: ExportContext): SqlTag {\n return async <T = Record<string, unknown>>(\n strings: TemplateStringsArray,\n ...values: unknown[]\n ) => {\n try {\n context.assertActive();\n if (context.transactionBound) {\n context.scope.consumeTransactionOperation();\n }\n // DataPath accepts value interpolation only and keeps Drizzle private.\n return await context.getDataPath().raw<T>(strings, ...values);\n } catch (error) {\n throw classifyDatabaseError(error, \"read\");\n }\n };\n}\n\n/** Build entities and SQL bound to either the root pool or one transaction. */\nfunction buildTransactionClient(context: ExportContext): TransactionClient {\n const result: Record<string, unknown> = Object.create(null);\n for (const [name, table] of Object.entries(context.schema.$tables)) {\n result[name] = new EntityClient({\n table,\n getDataPath: context.getDataPath,\n execute: context.execute,\n assertActive: context.assertActive,\n scope: context.scope,\n hooks: context.hooks?.[name],\n transactionBound: context.transactionBound,\n runInTransaction: (run) =>\n runTransaction(context, (tx) => run(entityOf(tx, name))),\n });\n }\n result.sql = buildSql(context);\n return result as TransactionClient;\n}\n\n/** Typegen owns the entity keys, so reaching one by table name stays dynamic. */\nfunction entityOf(tx: TransactionClient, name: string): EntityClient {\n return (tx as unknown as Record<string, EntityClient>)[name];\n}\n\n/** Reject from inside the driver callback so the driver rolls back on expiry. */\nfunction runWithTransactionDeadline<T>(run: () => Promise<T>): Promise<T> {\n let timer: ReturnType<typeof setTimeout>;\n const deadline = new Promise<never>((_, reject) => {\n timer = setTimeout(\n () => reject(new DatabasePluginError(\"TRANSIENT\", \"transaction\")),\n TRANSACTION_TIMEOUT_MS,\n );\n timer.unref?.();\n });\n return Promise.race([Promise.resolve().then(run), deadline]).finally(() =>\n clearTimeout(timer),\n );\n}\n\n/**\n * Run inside one transaction, joining the active one rather than nesting:\n * PostgreSQL has no nested transaction, and a savepoint would let part of a\n * hooked mutation survive a rollback.\n */\nfunction runTransaction<T>(\n context: ExportContext,\n callback: (tx: TransactionClient) => Promise<T>,\n): Promise<T> {\n context.assertActive();\n const active = context.scope.activeTransaction();\n if (active) return callback(active);\n return context.getDataPath().transaction(async (txDataPath) => {\n let open = true;\n const assertTransactionActive = () => {\n context.assertActive();\n if (!open) throw new DatabasePluginError(\"INTERNAL\", \"transaction\");\n };\n // The outer transaction owns execution; inner clients use its connection.\n const directExecute: EntityExecute = async (operation) => ({\n ok: true,\n data: await operation(),\n });\n const tx = buildTransactionClient({\n ...context,\n getDataPath: () => txDataPath,\n execute: directExecute,\n assertActive: assertTransactionActive,\n transactionBound: true,\n });\n try {\n return await runWithTransactionDeadline(() =>\n context.scope.runWithTransaction(tx, () => callback(tx)),\n );\n } finally {\n // Captured clients must not outlive the transaction callback.\n open = false;\n }\n });\n}\n\n/** Add the transaction entry point to the root database surface. */\nfunction buildDatabaseExports(context: ExportContext): DatabaseExports {\n const result = buildTransactionClient(context) as DatabaseExports;\n result.transaction = async <T>(\n callback: (tx: TransactionClient) => Promise<T>,\n ) => {\n try {\n return await runTransaction(context, callback);\n } catch (error) {\n if (error instanceof DatabaseValidationError) throw error;\n throw classifyDatabaseError(error, \"transaction\");\n }\n };\n return result;\n}\n\n/** Validate the schema, create one pool-backed API, and verify connectivity. */\nexport async function createDatabaseState<TSchema extends Schema>(\n schema: TSchema,\n execute: EntityExecute,\n hooks?: DatabaseHooks,\n): Promise<DatabaseState> {\n try {\n assertFinalizedSchema(schema);\n } catch (error) {\n logger.error(\"Database schema failed validation: %O\", error);\n throw new DatabasePluginError(\"SETUP_FAILED\", \"setup\");\n }\n\n let active = true;\n let pool: DatabaseState[\"pool\"] | undefined;\n const assertActive = () => {\n if (!active) throw new DatabasePluginError(\"INTERNAL\", \"runtime\");\n };\n try {\n pool = await initializeLakebasePool({\n statement_timeout: STATEMENT_TIMEOUT_MS,\n idle_in_transaction_session_timeout: IDLE_IN_TRANSACTION_TIMEOUT_MS,\n });\n const db = createDrizzleDb(pool, schema);\n const dataPath = createDrizzleDataPath(db, schema, {\n columnAccess: \"trusted\",\n });\n const exports = buildDatabaseExports({\n schema,\n getDataPath: () => dataPath,\n execute,\n assertActive,\n // One scope per state keeps concurrent instances from sharing a transaction.\n scope: createMutationScope(),\n hooks,\n transactionBound: false,\n });\n // Do not publish exports until an authenticated statement succeeds.\n await dataPath.raw`select 1`;\n return {\n pool,\n exports,\n deactivate: () => {\n active = false;\n },\n };\n } catch (error) {\n logger.error(\"Database setup failed: %O\", error);\n active = false;\n await pool?.end().catch(() => undefined);\n throw new DatabasePluginError(\"SETUP_FAILED\", \"setup\");\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { assertFinalizedSchema } from "../../database/schema-builder/define-schema.js";
|
|
2
|
+
import { databaseSetupFailed } from "../../database/errors.js";
|
|
3
|
+
import { stat } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/plugins/database/load-schema.ts
|
|
7
|
+
/** Same application-relative convention used by appkit generate-types. */
|
|
8
|
+
const DEFAULT_SCHEMA_FILE = "config/database/schema.ts";
|
|
9
|
+
/** Load application-owned declarations during setup, never during registration. */
|
|
10
|
+
async function loadDefaultDatabaseSchema(root = process.cwd()) {
|
|
11
|
+
const file = path.resolve(root, DEFAULT_SCHEMA_FILE);
|
|
12
|
+
const label = JSON.stringify(file);
|
|
13
|
+
let isFile;
|
|
14
|
+
try {
|
|
15
|
+
isFile = (await stat(file)).isFile();
|
|
16
|
+
} catch {
|
|
17
|
+
throw databaseSetupFailed(`Cannot read the default schema at ${label}. Create ${DEFAULT_SCHEMA_FILE} with a named "schema" export, or pass database({ schema }). The path is relative to the application's working directory.`);
|
|
18
|
+
}
|
|
19
|
+
if (!isFile) throw databaseSetupFailed(`The default schema at ${label} must be a file.`);
|
|
20
|
+
let module;
|
|
21
|
+
try {
|
|
22
|
+
const { createJiti } = await import("jiti");
|
|
23
|
+
module = await createJiti(import.meta.url, { moduleCache: false }).import(file);
|
|
24
|
+
} catch {
|
|
25
|
+
throw databaseSetupFailed(`Could not load the default schema at ${label}. Check the module and its imports, or pass database({ schema }).`);
|
|
26
|
+
}
|
|
27
|
+
if (typeof module !== "object" || module === null || !Object.hasOwn(module, "schema")) throw databaseSetupFailed(`The default schema module at ${label} must export a named "schema" created with defineSchema().`);
|
|
28
|
+
let schema;
|
|
29
|
+
try {
|
|
30
|
+
schema = module.schema;
|
|
31
|
+
assertFinalizedSchema(schema);
|
|
32
|
+
} catch {
|
|
33
|
+
throw databaseSetupFailed(`The "schema" export from ${label} must be a finalized AppKit schema created with defineSchema().`);
|
|
34
|
+
}
|
|
35
|
+
return schema;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { loadDefaultDatabaseSchema };
|
|
40
|
+
//# sourceMappingURL=load-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-schema.js","names":[],"sources":["../../../src/plugins/database/load-schema.ts"],"sourcesContent":["import { stat } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { databaseSetupFailed } from \"../../database/errors\";\nimport type { Schema } from \"../../database/schema-builder\";\nimport { assertFinalizedSchema } from \"../../database/schema-builder/define-schema\";\n\n/** Same application-relative convention used by appkit generate-types. */\nconst DEFAULT_SCHEMA_FILE = \"config/database/schema.ts\";\n\n/** Load application-owned declarations during setup, never during registration. */\nexport async function loadDefaultDatabaseSchema(\n root = process.cwd(),\n): Promise<Schema> {\n const file = path.resolve(root, DEFAULT_SCHEMA_FILE);\n const label = JSON.stringify(file);\n let isFile: boolean;\n try {\n isFile = (await stat(file)).isFile();\n } catch {\n throw databaseSetupFailed(\n `Cannot read the default schema at ${label}. Create ${DEFAULT_SCHEMA_FILE} with a named \"schema\" export, or pass database({ schema }). The path is relative to the application's working directory.`,\n );\n }\n if (!isFile) {\n throw databaseSetupFailed(`The default schema at ${label} must be a file.`);\n }\n\n let module: unknown;\n try {\n // Jiti also loads TypeScript declarations in a plain Node production run.\n // No module cache: a fresh plugin instance must not inherit a stale schema.\n const { createJiti } = await import(\"jiti\");\n module = await createJiti(import.meta.url, { moduleCache: false }).import(\n file,\n );\n } catch {\n // Import errors can contain credentials, row values, or application internals.\n throw databaseSetupFailed(\n `Could not load the default schema at ${label}. Check the module and its imports, or pass database({ schema }).`,\n );\n }\n if (\n typeof module !== \"object\" ||\n module === null ||\n !Object.hasOwn(module, \"schema\")\n ) {\n throw databaseSetupFailed(\n `The default schema module at ${label} must export a named \"schema\" created with defineSchema().`,\n );\n }\n let schema: unknown;\n try {\n schema = (module as { schema: unknown }).schema;\n assertFinalizedSchema(schema);\n } catch {\n throw databaseSetupFailed(\n `The \"schema\" export from ${label} must be a finalized AppKit schema created with defineSchema().`,\n );\n }\n return schema;\n}\n"],"mappings":";;;;;;;AAQA,MAAM,sBAAsB;;AAG5B,eAAsB,0BACpB,OAAO,QAAQ,KAAK,EACH;CACjB,MAAM,OAAO,KAAK,QAAQ,MAAM,oBAAoB;CACpD,MAAM,QAAQ,KAAK,UAAU,KAAK;CAClC,IAAI;AACJ,KAAI;AACF,YAAU,MAAM,KAAK,KAAK,EAAE,QAAQ;SAC9B;AACN,QAAM,oBACJ,qCAAqC,MAAM,WAAW,oBAAoB,2HAC3E;;AAEH,KAAI,CAAC,OACH,OAAM,oBAAoB,yBAAyB,MAAM,kBAAkB;CAG7E,IAAI;AACJ,KAAI;EAGF,MAAM,EAAE,eAAe,MAAM,OAAO;AACpC,WAAS,MAAM,WAAW,OAAO,KAAK,KAAK,EAAE,aAAa,OAAO,CAAC,CAAC,OACjE,KACD;SACK;AAEN,QAAM,oBACJ,wCAAwC,MAAM,mEAC/C;;AAEH,KACE,OAAO,WAAW,YAClB,WAAW,QACX,CAAC,OAAO,OAAO,QAAQ,SAAS,CAEhC,OAAM,oBACJ,gCAAgC,MAAM,4DACvC;CAEH,IAAI;AACJ,KAAI;AACF,WAAU,OAA+B;AACzC,wBAAsB,OAAO;SACvB;AACN,QAAM,oBACJ,4BAA4B,MAAM,iEACnC;;AAEH,QAAO"}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { DatabaseRegistry } from "../../database/contract/registry.js";
|
|
1
2
|
import { Schema } from "../../database/schema-builder/types.js";
|
|
2
3
|
import { EntityMutationHooks } from "./hooks.js";
|
|
3
4
|
|
|
4
5
|
//#region src/plugins/database/types.d.ts
|
|
6
|
+
type RegistryTableName = Extract<keyof DatabaseRegistry, string>;
|
|
7
|
+
/** @internal Infer conventional schema names from typegen, when available. */
|
|
8
|
+
type DefaultDatabaseSchema = Schema<[RegistryTableName] extends [never] ? string : RegistryTableName>;
|
|
5
9
|
/** Table names declared by one finalized schema. */
|
|
6
10
|
type SchemaTableName<TSchema extends Schema> = Extract<keyof TSchema["$tables"], string>;
|
|
7
11
|
/** Generated HTTP write operations. */
|
|
@@ -48,8 +52,13 @@ type EntityHooks<TTable extends string = string> = EntityMutationHooks<TTable> &
|
|
|
48
52
|
readonly serialize?: ReadSerializer;
|
|
49
53
|
};
|
|
50
54
|
/** Configuration for one schema-bound DatabasePlugin instance. */
|
|
51
|
-
type IDatabaseConfig<TSchema extends Schema> = {
|
|
52
|
-
|
|
55
|
+
type IDatabaseConfig<TSchema extends Schema = DefaultDatabaseSchema> = {
|
|
56
|
+
/**
|
|
57
|
+
* Explicit schema override. When omitted, setup loads the named `schema`
|
|
58
|
+
* export from `config/database/schema.ts` under the application's working
|
|
59
|
+
* directory. Missing or invalid declarations fail setup before pool creation.
|
|
60
|
+
*/
|
|
61
|
+
readonly schema?: TSchema;
|
|
53
62
|
/**
|
|
54
63
|
* Generated HTTP CRUD is enabled for all tables by default, using the app's
|
|
55
64
|
* service principal. Every admitted caller receives the enabled operations;
|
|
@@ -66,5 +75,5 @@ type IDatabaseConfig<TSchema extends Schema> = {
|
|
|
66
75
|
readonly hooks?: { readonly [TTable in SchemaTableName<TSchema>]?: EntityHooks<TTable> };
|
|
67
76
|
};
|
|
68
77
|
//#endregion
|
|
69
|
-
export { DatabaseApiConfig, DatabaseApiWriteOperation, DatabaseApiWritesConfig, EntityHooks, IDatabaseConfig, ReadSerializer, ReadSerializerContext };
|
|
78
|
+
export { DatabaseApiConfig, DatabaseApiWriteOperation, DatabaseApiWritesConfig, DefaultDatabaseSchema, EntityHooks, IDatabaseConfig, ReadSerializer, ReadSerializerContext };
|
|
70
79
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/plugins/database/types.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/plugins/database/types.ts"],"mappings":";;;;;KAIK,iBAAA,GAAoB,OAAA,OAAc,gBAAA;;KAG3B,qBAAA,GAAwB,MAAA,EACjC,iBAAA,6BAA8C,iBAAA;;KAIrC,eAAA,iBAAgC,MAAA,IAAU,OAAA,OAC9C,OAAA;AANR;AAAA,KAWY,yBAAA;;KAGA,uBAAA,iBAAwC,MAAA;EAbH,6EAiBlC,MAAA,YAAkB,eAAA,CAAgB,OAAA,KAlBP;EAAA,SAoB3B,UAAA,YAAsB,yBAAA;AAAA;;;;;AAfrC;;;;;;;;;;;;;KAmCY,iBAAA,iBAAkC,MAAA;EA7BlC,6EAiCG,MAAA,YAAkB,eAAA,CAAgB,OAAA;WAElC,MAAA,GAAS,uBAAA,CAAwB,OAAA;AAAA;AAhChD;AAAA,UAoCiB,qBAAA;EAAA,SACN,MAAA;EAAA,SACA,SAAA;AAAA;;;;;;KAQC,cAAA,IACV,GAAA,EAAK,MAAA,mBACL,OAAA,EAAS,qBAAA,KACN,MAAA;;KAGO,WAAA,mCACV,mBAAA,CAAoB,MAAA;EAAA,SACT,SAAA,GAAY,cAAA;AAAA;;KAOb,eAAA,iBAAgC,MAAA,GAAS,qBAAA;EAnCxB;;;;;EAAA,SAyClB,MAAA,GAAS,OAAA;EAnCI;;;;;;;;;;;;EAAA,SAgDb,GAAA,GAAM,iBAAA,CAAkB,OAAA;EAAA,SACxB,KAAA,yBACa,eAAA,CAAgB,OAAA,KAAY,WAAA,CAAY,MAAA;AAAA"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ApiError } from "../../shared/src/workspace-client/errors.js";
|
|
2
|
-
import { createLogger } from "../../logging/logger.js";
|
|
3
2
|
import { AuthenticationError } from "../../errors/authentication.js";
|
|
4
3
|
import "../../errors/index.js";
|
|
5
4
|
import { ServiceContext } from "../../context/service-context.js";
|
|
5
|
+
import { createLogger } from "../../logging/logger.js";
|
|
6
6
|
import { isUserContext } from "../../context/user-context.js";
|
|
7
7
|
import { getCurrentUserId, getExecutionContext, getWorkspaceClient, runInUserContext } from "../../context/execution-context.js";
|
|
8
8
|
import "../../context/index.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createLogger } from "../../logging/logger.js";
|
|
2
1
|
import { ExecutionError } from "../../errors/execution.js";
|
|
3
2
|
import { ValidationError } from "../../errors/validation.js";
|
|
4
3
|
import "../../errors/index.js";
|
|
4
|
+
import { createLogger } from "../../logging/logger.js";
|
|
5
5
|
import { getCurrentUserId, getWorkspaceClient } from "../../context/execution-context.js";
|
|
6
6
|
import "../../context/index.js";
|
|
7
7
|
import { Plugin } from "../../plugin/plugin.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createWorkspaceClient } from "../../shared/src/workspace-client/factory.js";
|
|
2
|
+
import { getClientOptions } from "../../context/client-options.js";
|
|
2
3
|
import { createLogger } from "../../logging/logger.js";
|
|
3
4
|
import { createLakebasePoolManager } from "../../connectors/lakebase/pool-manager.js";
|
|
4
|
-
import { getClientOptions } from "../../context/client-options.js";
|
|
5
5
|
import { getUserContext } from "../../context/execution-context.js";
|
|
6
6
|
import { RoutingPool } from "../../connectors/lakebase/routing-pool.js";
|
|
7
7
|
import { createLakebasePool, getLakebaseOrmConfig, getLakebasePgConfig, getUsernameWithApiLookup } from "../../connectors/lakebase/index.js";
|