@dbx-tools/appkit 0.3.29 → 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 +77 -29
- package/index.ts +1 -0
- package/package.json +4 -4
- package/src/appkit.ts +21 -2
- package/src/config.ts +34 -16
- package/src/create-app.ts +104 -26
- package/src/databricks.ts +28 -3
- package/src/lakebase-resolver.ts +347 -150
- package/src/pgaddress.ts +28 -9
- package/src/plugin.ts +17 -2
- package/src/provision.ts +38 -6
- package/test/config.test.ts +63 -0
- package/test/lakebase-resolver.test.ts +82 -0
- package/test/pgaddress.test.ts +76 -0
- package/test/plugin.test.ts +64 -0
- package/test/provision.test.ts +35 -0
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
|
|
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
|
|
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("
|
|
89
|
-
cli: {
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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": "
|
|
21
|
-
"@dbx-tools/
|
|
22
|
-
"@dbx-tools/core": "0.3.
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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`
|
|
6
|
-
* and hard-coded `app.yaml` env entries (from
|
|
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,6 +15,7 @@
|
|
|
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
20
|
import { functionModule, json, object, log, string } from "@dbx-tools/shared-core";
|
|
20
21
|
import { parse as parseYamlText } from "yaml";
|
|
@@ -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
|
-
/**
|
|
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
|
|
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)
|
|
191
|
+
return parseYamlText(text);
|
|
188
192
|
}
|
|
189
193
|
|
|
190
194
|
function pickAppResourceId(apps: Record<string, BundleApp>): string | undefined {
|
|
@@ -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 (
|
|
325
|
+
if (!object.isRecord(data)) {
|
|
322
326
|
return undefined;
|
|
323
327
|
}
|
|
324
|
-
return { path: configFile, data
|
|
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 (
|
|
373
|
-
const record = current as Record<string, unknown>;
|
|
376
|
+
if (!object.isRecord(current)) return undefined;
|
|
374
377
|
const part = parts[i]!;
|
|
375
|
-
const next =
|
|
378
|
+
const next = current[part];
|
|
376
379
|
if (i === parts.length - 1) {
|
|
377
380
|
if (typeof next === "string" && next) return next;
|
|
378
|
-
if (
|
|
379
|
-
const 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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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(
|
|
68
|
-
|
|
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(
|
|
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
|
-
/**
|
|
97
|
-
|
|
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 >
|
|
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
|
-
/**
|
|
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`
|