@dbx-tools/appkit 0.3.28 → 0.3.30

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/README.md CHANGED
@@ -6,7 +6,7 @@ Import this package when backend code needs AppKit execution context, typed
6
6
  plugin lookup, Databricks SDK cancellation, layered config resolution, or
7
7
  Lakebase auto-configuration without taking on a heavier feature package.
8
8
 
9
- Key features:
9
+ **Key features:**
10
10
 
11
11
  - Auto-configuration before AppKit setup, especially for Lakebase/Postgres env
12
12
  values that AppKit plugins read during initialization.
@@ -21,7 +21,7 @@ Key features:
21
21
  - Lakebase cache-schema provisioning for deployments where the app identity must
22
22
  be granted access before persistent cache initialization.
23
23
 
24
- ## Why Not Just AppKit?
24
+ ## Why Use This Over Native AppKit
25
25
 
26
26
  Use native AppKit directly when your app can read its required env vars before
27
27
  `createApp()` and does not need extra setup around plugin exports or config
@@ -41,7 +41,8 @@ Use this package when the friction is around bootstrapping and reuse:
41
41
 
42
42
  ## Create An Auto-Configured App
43
43
 
44
- `createApp.createApp` is a drop-in wrapper around AppKit `createApp`. It runs
44
+ `createApp.createApp` is a drop-in wrapper around AppKit `createApp` with the
45
+ same config and the same typed plugin-export map. It runs
45
46
  `createApp.autoConfigure()` first so enabled capabilities can populate
46
47
  environment variables before plugin setup runs.
47
48
 
@@ -64,6 +65,11 @@ explicit options, and local-only discovery is skipped inside a Databricks App
64
65
  environment. This makes the same entrypoint usable in local development,
65
66
  Databricks Asset Bundle validation, and deployed Apps.
66
67
 
68
+ Boot-time resolution runs as the service principal before any plugin exists, so
69
+ it sits outside AppKit's interceptor chain. It carries its own timeout, and
70
+ `createApp.autoConfigure()` accepts an `AbortSignal` when a caller wants to
71
+ cancel it earlier.
72
+
67
73
  Use the lower-level functions when you need to inspect or customize the result:
68
74
 
69
75
  ```ts
@@ -77,16 +83,54 @@ const resolved = await lakebaseResolver.resolveLakebaseConnection({
77
83
  lakebaseResolver.applyLakebaseToEnv(resolved);
78
84
  ```
79
85
 
86
+ ## Configuration
87
+
88
+ `createApp.createApp()` accepts everything AppKit's `createApp` does, plus:
89
+
90
+ | Option | Type | Default | Description |
91
+ | --------------- | ------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
92
+ | `autoConfigure` | `"provision" \| "env" \| false` | `"provision"` | What to run before AppKit boots. `"provision"` resolves the Lakebase connection into `process.env` and grants the AppKit cache schema; `"env"` resolves the connection only; `false` skips auto-configuration. Omit it to gate the default on a `lakebase` plugin being registered, or set it explicitly to run regardless. |
93
+
94
+ `lakebaseResolver.resolveLakebaseConnection()` accepts:
95
+
96
+ | Option | Type | Default | Description |
97
+ | ------------ | ------------------------------------ | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
98
+ | `endpoint` | `string` | `LAKEBASE_ENDPOINT` | Any address `pgaddress.parseAddress()` understands: resource path, Postgres URI, hostname, or bare project id. |
99
+ | `project` | `string` | discovered | Lakebase project id. Resolved from the workspace when unset. |
100
+ | `branch` | `string` | project default | Branch id within the project. |
101
+ | `database` | `string` | `PGDATABASE`, else `databricks_postgres` | Postgres database name. |
102
+ | `host` | `string` | `PGHOST`, else the endpoint's host | Postgres hostname. |
103
+ | `port` | `number` | `PGPORT`, else `5432` | Postgres port. |
104
+ | `sslMode` | `"require" \| "disable" \| "prefer"` | `PGSSLMODE`, else `require` | Postgres TLS mode. |
105
+ | `autoCreate` | `string \| false` | slug of the package name | Project id to create when the workspace has none. `false` fails instead of creating. |
106
+
107
+ Environment variables read during resolution:
108
+
109
+ | Variable | Description |
110
+ | ------------------- | ------------------------------------------------------------------------------------------------ |
111
+ | `LAKEBASE_ENDPOINT` | Endpoint resource path, Postgres URI, hostname, or project id. |
112
+ | `PGHOST` | Postgres hostname. Skips the endpoint lookup when set with `PGDATABASE` and `LAKEBASE_ENDPOINT`. |
113
+ | `PGDATABASE` | Postgres database name. |
114
+ | `PGPORT` | Postgres port. A value outside 1-65535 fails with a `ValidationError`. |
115
+ | `PGSSLMODE` | `require`, `disable`, or `prefer`. Any other value fails with a `ValidationError`. |
116
+ | `PGUSER` | Connecting role. Filled from the workspace identity when unset. |
117
+
118
+ Resolved values are written back to `process.env` by
119
+ `lakebaseResolver.applyLakebaseToEnv()`, which never overwrites a variable that
120
+ is already set.
121
+
80
122
  ## Resolve Local And Bundle Config
81
123
 
82
124
  `config.resolveConfigValue()` checks explicit options, CLI overrides, env vars,
83
- Databricks Asset Bundle validation output, and `app.yaml` env entries.
125
+ Databricks Asset Bundle validation output, and `app.yaml` env entries, in AppKit's
126
+ precedence order: explicit config, then environment variable, then the app or
127
+ bundle definition.
84
128
 
85
129
  ```ts
86
130
  import { config } from "@dbx-tools/appkit";
87
131
 
88
- const warehouseId = await config.resolveConfigValue("SQL_WAREHOUSE_ID", {
89
- cli: { SQL_WAREHOUSE_ID: flags.warehouse },
132
+ const warehouseId = await config.resolveConfigValue("DATABRICKS_WAREHOUSE_ID", {
133
+ cli: { DATABRICKS_WAREHOUSE_ID: flags.warehouse },
90
134
  sources: config.withCliSources(),
91
135
  });
92
136
  ```
@@ -136,14 +180,15 @@ Databricks SDK calls accept a `Context`. Many app and web APIs use
136
180
  ```ts
137
181
  import { databricks } from "@dbx-tools/appkit";
138
182
 
139
- const context = databricks.toContext(request.signal);
140
- await client.apiClient.request({
141
- path: "/api/2.0/serving-endpoints",
142
- method: "GET",
143
- headers: new Headers(),
144
- raw: false,
145
- context,
146
- });
183
+ await client.apiClient.request(
184
+ {
185
+ path: "/api/2.0/serving-endpoints",
186
+ method: "GET",
187
+ headers: new Headers(),
188
+ raw: false,
189
+ },
190
+ databricks.toContext(request.signal),
191
+ );
147
192
  ```
148
193
 
149
194
  `databricks.isAppEnv()` checks the Databricks App environment shape for setup
@@ -166,7 +211,9 @@ const required = plugin.require(this.context, lakebase, "my-plugin").exports();
166
211
  ```
167
212
 
168
213
  Use this in AppKit plugins that depend on sibling plugin exports but should not
169
- hard-code registered names or casts at every call site.
214
+ hard-code registered names or casts at every call site. A missing required
215
+ plugin throws AppKit's `ConfigurationError`, naming both the caller and the
216
+ plugin to register.
170
217
 
171
218
  ## Provision Lakebase Cache Schema
172
219
 
@@ -176,25 +223,26 @@ been resolved and before AppKit initializes its persistent cache.
176
223
 
177
224
  ```ts
178
225
  import { provision } from "@dbx-tools/appkit";
179
- import { log } from "@dbx-tools/shared-core";
180
226
 
181
- await provision.provisionCacheSchema(
182
- log.logger("appkit-cache"),
183
- "app-service-principal@databricks.com",
184
- );
227
+ await provision.provisionCacheSchema("app-service-principal@databricks.com");
185
228
  ```
186
229
 
230
+ Pass a second argument to report progress on your own logger. The grants are
231
+ skipped inside a Databricks App, and any failure is logged rather than thrown so
232
+ a degraded cache never blocks startup.
233
+
187
234
  ## Modules
188
235
 
189
- - `createApp` - `createApp()` wrapper and `autoConfigure()`.
190
- - `lakebaseResolver` - Lakebase connection discovery, default picking, optional
191
- auto-create, and env application.
192
- - `pgaddress` - permissive Lakebase/Postgres address parser.
193
- - `config` - local/env/bundle/app-yaml config lookup.
194
- - `appkit` - execution context lookup and initialization.
195
- - `databricks` - App env detection and SDK context cancellation adapters.
196
- - `plugin` - typed AppKit plugin data, instance, and required-instance lookup.
197
- - `provision` - cache schema provisioning helpers.
236
+ | Module | Responsibility |
237
+ | ------------------ | ------------------------------------------------------------------------------------------ |
238
+ | `createApp` | `createApp()` wrapper and `autoConfigure()`. |
239
+ | `lakebaseResolver` | Lakebase connection discovery, default picking, optional auto-create, and env application. |
240
+ | `pgaddress` | Permissive Lakebase/Postgres address parser. |
241
+ | `config` | Local/env/bundle/app-yaml config lookup. |
242
+ | `appkit` | Execution context lookup and initialization. |
243
+ | `databricks` | App env detection and SDK context cancellation adapters. |
244
+ | `plugin` | Typed AppKit plugin data, instance, and required-instance lookup. |
245
+ | `provision` | Cache schema provisioning helpers. |
198
246
 
199
247
  The shell-facing wrapper for auto-config is
200
248
  [`@dbx-tools/cli-appkit-env`](../../cli/appkit-env). Higher-level agent composition
package/index.ts CHANGED
@@ -12,6 +12,7 @@ export * as plugin from "./src/plugin";
12
12
  export * as provision from "./src/provision";
13
13
  export type { ExecutionContextLike, WorkspaceClientLike } from "./src/appkit";
14
14
  export type { BundleValidateJson, ConfigFile, ConfigSource, ConfigMapValue, ResolveConfigValueOptions } from "./src/config";
15
+ export type { AutoConfigureMode, CreateAppConfig } from "./src/create-app";
15
16
  export type { ContextLike } from "./src/databricks";
16
17
  export type { LakebaseResolverInputs, LakebaseConnection } from "./src/lakebase-resolver";
17
18
  export type { SslMode, LakebaseConnectionInputs, ParsedAddress } from "./src/pgaddress";
package/package.json CHANGED
@@ -17,16 +17,16 @@
17
17
  "dependencies": {
18
18
  "@databricks/sdk-experimental": "^0.17.0",
19
19
  "yaml": "^2.9.0",
20
- "zod": "^4.3.6",
21
- "@dbx-tools/core": "0.3.28",
22
- "@dbx-tools/shared-core": "0.3.28"
20
+ "zod": "4.3.6",
21
+ "@dbx-tools/core": "0.3.30",
22
+ "@dbx-tools/shared-core": "0.3.30"
23
23
  },
24
24
  "main": "index.ts",
25
25
  "license": "UNLICENSED",
26
26
  "publishConfig": {
27
27
  "access": "public"
28
28
  },
29
- "version": "0.3.28",
29
+ "version": "0.3.30",
30
30
  "types": "index.ts",
31
31
  "type": "module",
32
32
  "exports": {
package/src/appkit.ts CHANGED
@@ -12,6 +12,9 @@
12
12
  */
13
13
 
14
14
  import { createApp, getExecutionContext, InitializationError } from "@databricks/appkit";
15
+ import { log } from "@dbx-tools/shared-core";
16
+
17
+ const logger = log.logger("appkit");
15
18
 
16
19
  /**
17
20
  * The AppKit per-request execution context returned by `getExecutionContext()`
@@ -33,7 +36,14 @@ export type WorkspaceClientLike = ExecutionContextLike["client"];
33
36
  /**
34
37
  * The current AppKit execution context, or `undefined` when AppKit isn't
35
38
  * initialized (outside a request scope). Swallows AppKit's
36
- * `InitializationError`; any other error propagates.
39
+ * {@link InitializationError}; any other error propagates.
40
+ *
41
+ * @example
42
+ * import { appkit } from "@dbx-tools/appkit";
43
+ * import { WorkspaceClient } from "@databricks/sdk-experimental";
44
+ *
45
+ * // OBO-scoped inside a request, service principal from a CLI or script.
46
+ * const client = appkit.tryGetExecutionContext()?.client ?? new WorkspaceClient({});
37
47
  */
38
48
  export function tryGetExecutionContext(): ExecutionContextLike | undefined {
39
49
  try {
@@ -49,9 +59,18 @@ export function tryGetExecutionContext(): ExecutionContextLike | undefined {
49
59
  return undefined;
50
60
  }
51
61
 
52
- /** Initialize a bare AppKit app (no plugins) when none is running yet. */
62
+ /**
63
+ * Initialize a bare AppKit app (no plugins) when none is running yet.
64
+ *
65
+ * @example
66
+ * import { appkit } from "@dbx-tools/appkit";
67
+ *
68
+ * await appkit.ensureInitialized();
69
+ * const client = appkit.tryGetExecutionContext()?.client;
70
+ */
53
71
  export async function ensureInitialized(): Promise<void> {
54
72
  if (!tryGetExecutionContext()) {
73
+ logger.debug("initializing a bare AppKit app");
55
74
  await createApp({ plugins: [] });
56
75
  }
57
76
  }
package/src/config.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  * Layered configuration resolution for local development against a Databricks
3
3
  * App / Asset Bundle.
4
4
  *
5
- * Default sources: `env`, then Databricks App `config.env` (from {@link bundle})
6
- * and hard-coded `app.yaml` env entries (from {@link appYaml}). Opt in to `cli`
7
- * when a dev command wants flag overrides.
5
+ * Default sources: `explicit`, then `env`, then Databricks App `config.env`
6
+ * (from {@link bundle}) and hard-coded `app.yaml` env entries (from
7
+ * {@link appYaml}). Opt in to `cli` when a dev command wants flag overrides.
8
8
  *
9
9
  * Server-only (`node:child_process`, bundle root discovery). Databricks-app
10
10
  * specific, so it lives in node-appkit rather than shared-core.
@@ -15,8 +15,9 @@
15
15
  import { spawnSync } from "node:child_process";
16
16
  import { readFile } from "node:fs/promises";
17
17
  import { dirname, resolve } from "node:path";
18
+ import { ValidationError } from "@databricks/appkit";
18
19
  import { file, project } from "@dbx-tools/core";
19
- import { functionModule, object, log, string } from "@dbx-tools/shared-core";
20
+ import { functionModule, json, object, log, string } from "@dbx-tools/shared-core";
20
21
  import { parse as parseYamlText } from "yaml";
21
22
  import { z } from "zod";
22
23
 
@@ -36,7 +37,7 @@ export interface ConfigFile {
36
37
  /** Supported configuration sources, consulted in array order. */
37
38
  export type ConfigSource = "explicit" | "cli" | "env" | "bundle";
38
39
 
39
- const defaultConfigSources: ConfigSource[] = ["env", "bundle"];
40
+ const defaultConfigSources: ConfigSource[] = ["explicit", "env", "bundle"];
40
41
 
41
42
  const APP_YAML_NAMES = ["app.yaml", "app.yml"] as const;
42
43
 
@@ -113,9 +114,12 @@ export interface ResolveConfigValueOptions {
113
114
  bundleData?: ConfigFile;
114
115
  /** Parsed `app.yaml` contents. Defaults to {@link appYaml} (skipped inside a Databricks App). */
115
116
  appData?: ConfigFile;
116
- /** Sources to consult, first truthy string wins. Defaults to `env`, then `bundle`. */
117
+ /**
118
+ * Sources to consult, first truthy string wins. Defaults to `explicit`, then
119
+ * `env`, then `bundle`.
120
+ */
117
121
  sources?: ConfigSource[];
118
- /** Programmatic overrides. When set, `explicit` is appended to `sources` unless already listed. */
122
+ /** Programmatic overrides. When set, `explicit` is prepended to `sources` unless already listed. */
119
123
  explicit?: Record<string, ConfigMapValue>;
120
124
  /** CLI flag values (when `cli` is listed in `sources`). */
121
125
  cli?: Record<string, ConfigMapValue>;
@@ -184,7 +188,7 @@ function readAppEnv(keys: Iterable<string>, envMap: Record<string, string>): str
184
188
  }
185
189
 
186
190
  function parseYaml(text: string): unknown {
187
- return parseYamlText(text) as unknown;
191
+ return parseYamlText(text);
188
192
  }
189
193
 
190
194
  function pickAppResourceId(apps: Record<string, BundleApp>): string | undefined {
@@ -293,7 +297,7 @@ function validateBundle(root: string): BundleValidateJson | undefined {
293
297
  bundleValidateCache.set(key, undefined);
294
298
  return undefined;
295
299
  }
296
- const data = JSON.parse(text) as BundleValidateJson;
300
+ const data = json.parse<BundleValidateJson>(text);
297
301
  bundleValidateCache.set(key, data);
298
302
  return data;
299
303
  } catch {
@@ -318,10 +322,10 @@ async function loadAppYaml(cwd: string): Promise<ConfigFile | undefined> {
318
322
  try {
319
323
  const text = await readFile(configFile, "utf8");
320
324
  const data = parseYaml(text);
321
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
325
+ if (!object.isRecord(data)) {
322
326
  return undefined;
323
327
  }
324
- return { path: configFile, data: data as Record<string, unknown> };
328
+ return { path: configFile, data };
325
329
  } catch {
326
330
  logger.warn("failed to parse app yaml", { path: configFile });
327
331
  }
@@ -369,14 +373,13 @@ export function getBundlePath(data: BundleValidateJson, path: string): string |
369
373
 
370
374
  let current: unknown = data;
371
375
  for (let i = 0; i < parts.length; i++) {
372
- if (typeof current !== "object" || current === null) return undefined;
373
- const record = current as Record<string, unknown>;
376
+ if (!object.isRecord(current)) return undefined;
374
377
  const part = parts[i]!;
375
- const next = record[part];
378
+ const next = current[part];
376
379
  if (i === parts.length - 1) {
377
380
  if (typeof next === "string" && next) return next;
378
- if (typeof next === "object" && next !== null && "value" in next) {
379
- const value = (next as { value?: unknown }).value;
381
+ if (object.isRecord(next)) {
382
+ const value = next.value;
380
383
  return typeof value === "string" && value ? value : undefined;
381
384
  }
382
385
  return undefined;
@@ -399,7 +402,7 @@ async function resolveAppEnvMap(
399
402
  function resolveSources(options: ResolveConfigValueOptions): ConfigSource[] {
400
403
  const sources = [...(options.sources ?? defaultConfigSources)];
401
404
  if (options.explicit !== undefined && !sources.includes("explicit")) {
402
- sources.push("explicit");
405
+ sources.unshift("explicit");
403
406
  }
404
407
  return sources;
405
408
  }
@@ -407,6 +410,17 @@ function resolveSources(options: ResolveConfigValueOptions): ConfigSource[] {
407
410
  /**
408
411
  * Resolve a configuration string from the configured sources. Returns the first
409
412
  * non-empty value, or `undefined` when nothing matches.
413
+ *
414
+ * Precedence follows AppKit's: explicit config, then environment variable, then
415
+ * whatever the Databricks App / bundle definition supplies.
416
+ *
417
+ * @example
418
+ * import { config } from "@dbx-tools/appkit";
419
+ *
420
+ * const warehouseId = await config.resolveConfigValue("DATABRICKS_WAREHOUSE_ID", {
421
+ * cli: { DATABRICKS_WAREHOUSE_ID: flags.warehouse },
422
+ * sources: config.withCliSources(),
423
+ * });
410
424
  */
411
425
  export async function resolveConfigValue(
412
426
  name: string,
@@ -433,7 +447,11 @@ export async function resolveConfigValue(
433
447
  yield readAppEnv(keys, appEnvMap);
434
448
  break;
435
449
  default:
436
- throw new Error(`Unknown config source: ${source}`);
450
+ throw ValidationError.invalidValue(
451
+ "sources",
452
+ source,
453
+ "one of: explicit, cli, env, bundle",
454
+ );
437
455
  }
438
456
  }
439
457
  })();
package/src/create-app.ts CHANGED
@@ -10,19 +10,22 @@
10
10
  * import { createApp } from "@dbx-tools/appkit";
11
11
  * import { lakebase, server } from "@databricks/appkit";
12
12
  *
13
- * await createApp({ plugins: [server(), lakebase()] });
13
+ * await createApp.createApp({ plugins: [server(), lakebase()] });
14
14
  * ```
15
15
  *
16
16
  * Auto-configuration runs BEFORE delegating so plugins see a fully populated
17
17
  * `process.env` during their synchronous `setup()`. Lakebase Postgres runs when
18
- * a `lakebase` plugin is present, or when `autoConfigure: true` is set on the
19
- * config object.
18
+ * a `lakebase` plugin is present, or when {@link AutoConfigureMode} is set
19
+ * explicitly on the config object.
20
20
  *
21
21
  * @module
22
22
  */
23
23
 
24
24
  import { createApp as appkitCreateApp, getUsernameWithApiLookup } from "@databricks/appkit";
25
- import { log } from "@dbx-tools/shared-core";
25
+ // AppKit's root barrel re-exports `PluginData` but not `PluginMap`; the package
26
+ // publishes this subpath for exactly that type.
27
+ import type { PluginMap } from "@databricks/appkit/dist/shared/src/plugin";
28
+ import { async, log } from "@dbx-tools/shared-core";
26
29
 
27
30
  import {
28
31
  applyLakebaseToEnv,
@@ -31,31 +34,92 @@ import {
31
34
  } from "./lakebase-resolver";
32
35
  import { provisionCacheSchema } from "./provision";
33
36
 
34
- type CreateAppConfig = Parameters<typeof appkitCreateApp>[0] & {
35
- autoConfigure?: boolean | "provision";
37
+ type AppKitCreateAppConfig = NonNullable<Parameters<typeof appkitCreateApp>[0]>;
38
+ type AppKitPlugins = NonNullable<AppKitCreateAppConfig["plugins"]>;
39
+
40
+ /**
41
+ * What auto-configuration does before AppKit boots.
42
+ *
43
+ * - `"provision"`: resolve the Lakebase connection into `process.env`, then
44
+ * grant the AppKit cache schema to the connecting role.
45
+ * - `"env"`: resolve the Lakebase connection into `process.env` only.
46
+ *
47
+ * Omit it to get `"provision"` gated on a `lakebase` plugin being registered;
48
+ * set it explicitly to run regardless of the plugin list, or pass `false` to
49
+ * skip auto-configuration entirely.
50
+ */
51
+ export type AutoConfigureMode = "env" | "provision";
52
+
53
+ /** AppKit's `createApp` config plus the dbx-tools auto-configuration switch. */
54
+ export type CreateAppConfig<T extends AppKitPlugins = AppKitPlugins> = Omit<
55
+ AppKitCreateAppConfig,
56
+ "plugins" | "onPluginsReady"
57
+ > & {
58
+ plugins?: T;
59
+ onPluginsReady?: (appkit: PluginMap<T>) => void | Promise<void>;
60
+ /** Auto-configuration to run before AppKit boots. Defaults to `"provision"`. */
61
+ autoConfigure?: AutoConfigureMode | false;
36
62
  };
37
63
 
38
64
  const logger = log.logger("create-app");
39
65
 
40
66
  const LAKEBASE_PLUGIN = "lakebase";
67
+ const DEFAULT_AUTO_CONFIGURE: AutoConfigureMode = "provision";
41
68
 
42
- function usesPlugin(config: CreateAppConfig | undefined, name: string): boolean {
69
+ /**
70
+ * Upper bound on boot-time auto-configuration. It runs as the service principal
71
+ * before any plugin is constructed, so it sits outside AppKit's interceptor
72
+ * chain and inherits no timeout, retry, or telemetry from it. The budget covers
73
+ * the resolver's own worst case (create a project, then wait for its endpoint)
74
+ * plus the cache-schema grants.
75
+ */
76
+ const AUTO_CONFIGURE_TIMEOUT_MS = 11 * 60_000;
77
+
78
+ function usesPlugin<T extends AppKitPlugins>(
79
+ config: CreateAppConfig<T> | undefined,
80
+ name: string,
81
+ ): boolean {
43
82
  return Boolean(config?.plugins?.some((entry) => entry.name === name));
44
83
  }
45
84
 
46
85
  /**
47
86
  * Run enabled auto-configuration steps without calling AppKit's `createApp`.
48
- * Lakebase Postgres resolves when `autoConfigure` is `true` or a `lakebase`
49
- * plugin is listed in `config.plugins`. Defaults to `"provision"` (Lakebase env
50
- * + optional cache schema). Pass `autoConfigure: false` to skip entirely.
87
+ *
88
+ * Lakebase Postgres resolves when {@link CreateAppConfig.autoConfigure} is set
89
+ * explicitly or a `lakebase` plugin is listed in `config.plugins`. `signal`
90
+ * cancels the resolution; it is combined with an internal boot timeout either
91
+ * way.
92
+ *
93
+ * @example
94
+ * import { createApp } from "@dbx-tools/appkit";
95
+ *
96
+ * // Populate PGHOST / PGDATABASE / LAKEBASE_ENDPOINT without booting AppKit.
97
+ * await createApp.autoConfigure({ autoConfigure: "env" });
51
98
  */
52
- export async function autoConfigure(config?: CreateAppConfig): Promise<void> {
53
- const { autoConfigure = "provision" } = config ?? {};
54
- if (autoConfigure !== false) {
55
- if (autoConfigure === true || usesPlugin(config, LAKEBASE_PLUGIN)) {
56
- await autoConfigureLakebase(autoConfigure === "provision");
57
- }
99
+ export async function autoConfigure<T extends AppKitPlugins>(
100
+ config?: CreateAppConfig<T>,
101
+ signal?: AbortSignal,
102
+ ): Promise<void> {
103
+ const mode = config?.autoConfigure ?? DEFAULT_AUTO_CONFIGURE;
104
+ const explicit = config?.autoConfigure !== undefined;
105
+ const lakebasePluginPresent = usesPlugin(config, LAKEBASE_PLUGIN);
106
+ if (mode === false || !(explicit || lakebasePluginPresent)) {
107
+ logger.info("ready", {
108
+ autoConfigure: mode,
109
+ lakebasePluginPresent,
110
+ provisioned: false,
111
+ skippedReason: mode === false ? "disabled" : "no lakebase plugin",
112
+ });
113
+ return;
58
114
  }
115
+
116
+ const controller = new AbortController();
117
+ async.tieAbortSignal(controller, signal);
118
+ async.tieAbortSignal(controller, AbortSignal.timeout(AUTO_CONFIGURE_TIMEOUT_MS));
119
+
120
+ const provision = mode === "provision";
121
+ await autoConfigureLakebase(provision, controller.signal);
122
+ logger.info("ready", { autoConfigure: mode, lakebasePluginPresent, provisioned: provision });
59
123
  }
60
124
 
61
125
  /**
@@ -64,23 +128,21 @@ export async function autoConfigure(config?: CreateAppConfig): Promise<void> {
64
128
  * {@link resolveLakebaseConnection} and {@link applyLakebaseToEnv} directly when
65
129
  * finer control is needed.
66
130
  */
67
- async function autoConfigureLakebase(provision: boolean): Promise<LakebaseConnection> {
68
- const resolved = await resolveLakebaseConnection();
131
+ async function autoConfigureLakebase(
132
+ provision: boolean,
133
+ signal: AbortSignal,
134
+ ): Promise<LakebaseConnection> {
135
+ const resolved = await resolveLakebaseConnection(undefined, signal);
69
136
  applyLakebaseToEnv(resolved);
70
137
  const user = await getUsernameWithApiLookup({});
71
138
  if (user) process.env.PGUSER ??= user;
72
139
  logger.info("env updated", { ...redactLakebaseConnection(resolved), user });
73
140
  if (provision) {
74
- await provisionCacheSchema(logger, user);
141
+ await provisionCacheSchema(user, logger);
75
142
  }
76
143
  return resolved;
77
144
  }
78
145
 
79
- const create = async (config?: CreateAppConfig) => {
80
- await autoConfigure(config);
81
- return appkitCreateApp(config);
82
- };
83
-
84
146
  function redactLakebaseConnection(resolved: LakebaseConnection): Record<string, unknown> {
85
147
  return {
86
148
  project: resolved.project,
@@ -93,5 +155,21 @@ function redactLakebaseConnection(resolved: LakebaseConnection): Record<string,
93
155
  };
94
156
  }
95
157
 
96
- /** Auto-configuring drop-in for AppKit's `createApp`. */
97
- export const createApp = create as unknown as typeof appkitCreateApp;
158
+ /**
159
+ * Auto-configuring drop-in for AppKit's `createApp`: same config, same typed
160
+ * plugin-export map, with {@link autoConfigure} run first.
161
+ *
162
+ * @example
163
+ * import { createApp } from "@dbx-tools/appkit";
164
+ * import { lakebase, server } from "@databricks/appkit";
165
+ *
166
+ * const app = await createApp.createApp({ plugins: [server(), lakebase()] });
167
+ */
168
+ export async function createApp<T extends AppKitPlugins>(
169
+ config?: CreateAppConfig<T>,
170
+ ): Promise<PluginMap<T>> {
171
+ await autoConfigure(config);
172
+ const appConfig = { ...config };
173
+ delete appConfig.autoConfigure;
174
+ return appkitCreateApp<T>(appConfig);
175
+ }
package/src/databricks.ts CHANGED
@@ -12,13 +12,24 @@
12
12
 
13
13
  import type { CancellationToken } from "@databricks/sdk-experimental";
14
14
  import { Context } from "@databricks/sdk-experimental";
15
- import { async } from "@dbx-tools/shared-core";
15
+ import { async, log } from "@dbx-tools/shared-core";
16
+
17
+ const logger = log.logger("databricks");
18
+
19
+ /** Highest valid TCP port number. */
20
+ export const MAX_TCP_PORT = 65_535;
16
21
 
17
22
  /**
18
23
  * Detect the Databricks App runtime from environment shape: requires a
19
24
  * non-empty `DATABRICKS_APP_NAME`, a `DATABRICKS_HOST` that parses as an
20
25
  * `http`/`https` URL, and a `DATABRICKS_APP_PORT` that is a valid TCP port.
21
26
  * Reads `process.env` when no `env` is passed.
27
+ *
28
+ * @example
29
+ * import { databricks } from "@dbx-tools/appkit";
30
+ *
31
+ * // Skip bundle / app.yaml discovery when the app is already deployed.
32
+ * const local = !databricks.isAppEnv();
22
33
  */
23
34
  export function isAppEnv(env: Record<string, string | undefined> = process.env): boolean {
24
35
  const appName = env.DATABRICKS_APP_NAME?.trim();
@@ -32,14 +43,17 @@ export function isAppEnv(env: Record<string, string | undefined> = process.env):
32
43
  try {
33
44
  const url = new URL(host);
34
45
  if (!["http:", "https:"].includes(url.protocol)) {
46
+ logger.debug("app env rejected: host is not an http(s) URL");
35
47
  return false;
36
48
  }
37
49
  } catch {
50
+ logger.debug("app env rejected: host is not a URL");
38
51
  return false;
39
52
  }
40
53
 
41
54
  const portNumber = Number(port);
42
- if (!Number.isInteger(portNumber) || portNumber < 1 || portNumber > 65535) {
55
+ if (!Number.isInteger(portNumber) || portNumber < 1 || portNumber > MAX_TCP_PORT) {
56
+ logger.debug("app env rejected: DATABRICKS_APP_PORT is not a TCP port");
43
57
  return false;
44
58
  }
45
59
 
@@ -49,7 +63,18 @@ export function isAppEnv(env: Record<string, string | undefined> = process.env):
49
63
  /** Either an SDK `Context` or a WHATWG `AbortSignal`. */
50
64
  export type ContextLike = Context | AbortSignal;
51
65
 
52
- /** Wrap a `Context` (returned as-is) or `AbortSignal` (adapted) as an SDK `Context`. */
66
+ /**
67
+ * Wrap a `Context` (returned as-is) or `AbortSignal` (adapted) as an SDK `Context`.
68
+ *
69
+ * @example
70
+ * import { databricks } from "@dbx-tools/appkit";
71
+ * import { getWorkspaceClient } from "@databricks/appkit";
72
+ *
73
+ * await getWorkspaceClient({}).apiClient.request(
74
+ * { path: "/api/2.0/serving-endpoints", method: "GET", headers: new Headers(), raw: false },
75
+ * databricks.toContext(request.signal),
76
+ * );
77
+ */
53
78
  export function toContext(input: ContextLike): Context;
54
79
  /**
55
80
  * Derive an SDK `Context` from `controller.signal`, optionally tying `input`