@dbx-tools/appkit-autopg 0.1.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/README.md +113 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/src/address.d.ts +69 -0
- package/dist/src/address.js +132 -0
- package/dist/src/autopg.d.ts +59 -0
- package/dist/src/autopg.js +74 -0
- package/dist/src/resolver.d.ts +118 -0
- package/dist/src/resolver.js +507 -0
- package/index.ts +18 -0
- package/package.json +40 -0
- package/src/address.ts +148 -0
- package/src/autopg.ts +93 -0
- package/src/resolver.ts +771 -0
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# @dbx-tools/appkit-autopg
|
|
2
|
+
|
|
3
|
+
`autopg()` is a one-line helper that fills in every Lakebase Postgres
|
|
4
|
+
env var the AppKit `lakebase` plugin needs from whatever fragments your
|
|
5
|
+
deployment actually carries. Run it once before `createApp(...)` and stop
|
|
6
|
+
hand-rolling connection strings:
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { createApp, lakebase, server } from "@databricks/appkit";
|
|
10
|
+
import { autopg } from "@dbx-tools/appkit-autopg";
|
|
11
|
+
|
|
12
|
+
await autopg();
|
|
13
|
+
await createApp({ plugins: [server(), lakebase()] });
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Why a top-level helper instead of an AppKit plugin
|
|
17
|
+
|
|
18
|
+
AppKit's `static phase` field orders plugin `setup()` _invocation_, not
|
|
19
|
+
async _completion_. `lakebase.setup()` synchronously throws on a missing
|
|
20
|
+
`PGHOST` after its first `await`, so a sibling plugin that performs REST
|
|
21
|
+
discovery during `setup()` races and loses every time. Awaiting
|
|
22
|
+
`autopg()` before `createApp(...)` sidesteps the race - by the time any
|
|
23
|
+
plugin runs, `process.env` is fully populated.
|
|
24
|
+
|
|
25
|
+
## What it accepts
|
|
26
|
+
|
|
27
|
+
`autopg()` looks at, in priority order:
|
|
28
|
+
|
|
29
|
+
1. Explicit `autopg({ project, branch, endpoint, database, host, port, sslMode, autoCreate })`
|
|
30
|
+
2. Env vars - the same ones `lakebase` already reads:
|
|
31
|
+
- `LAKEBASE_PROJECT`, `LAKEBASE_BRANCH`, `LAKEBASE_ENDPOINT`
|
|
32
|
+
- `PGHOST`, `PGDATABASE`, `PGPORT`, `PGSSLMODE`
|
|
33
|
+
3. Whatever the address parser can recover from
|
|
34
|
+
`LAKEBASE_ENDPOINT` / `config.endpoint`
|
|
35
|
+
|
|
36
|
+
The address input is permissive - any of these work:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Canonical resource path
|
|
40
|
+
LAKEBASE_ENDPOINT="projects/dbx-tools/branches/main/endpoints/primary"
|
|
41
|
+
|
|
42
|
+
# Full Postgres URI (auth, host, db, sslmode all extracted)
|
|
43
|
+
LAKEBASE_ENDPOINT="postgresql://me%40databricks.com@ep-foo.database.azuredatabricks.net/databricks_postgres?sslmode=require"
|
|
44
|
+
|
|
45
|
+
# Bare Lakebase hostname (resolver reverse-looks-up the project)
|
|
46
|
+
LAKEBASE_ENDPOINT="ep-steep-forest-e199v43w.database.eastus2.azuredatabricks.net"
|
|
47
|
+
|
|
48
|
+
# Bare project id (resolver picks the default branch + endpoint + db)
|
|
49
|
+
LAKEBASE_ENDPOINT="dbx-tools"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Three resolution modes
|
|
53
|
+
|
|
54
|
+
After parsing, the resolver fills gaps in this order:
|
|
55
|
+
|
|
56
|
+
1. **Reverse-lookup** - given just a host, scan
|
|
57
|
+
`projects` -> `branches` -> `endpoints` for a matching
|
|
58
|
+
`status.hosts.host` and recover the owning resource path.
|
|
59
|
+
2. **Pick default** - given a `project` (and optionally a `branch`),
|
|
60
|
+
prefer the server-marked default child (`status.default`,
|
|
61
|
+
`ENDPOINT_TYPE_READ_WRITE`, `databricks_postgres`) and fall back to
|
|
62
|
+
"the only one" when a listing returns a single result.
|
|
63
|
+
3. **Auto-create** - when no projects exist at all, create one whose id
|
|
64
|
+
defaults to a slugified `projectUtils.name()` (override with
|
|
65
|
+
`autoCreate: "my-id"` or disable with `autoCreate: false`). The
|
|
66
|
+
create call is idempotent: an `ALREADY_EXISTS` response from a
|
|
67
|
+
concurrent boot is treated as success. Then poll the default
|
|
68
|
+
endpoint until `current_state` is `READY` or `IDLE`.
|
|
69
|
+
|
|
70
|
+
## Options
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
await autopg({
|
|
74
|
+
// Skip writing process.env (just inspect the returned record).
|
|
75
|
+
exportEnv: false,
|
|
76
|
+
// Pin individual fields - any of these short-circuit the resolver.
|
|
77
|
+
project: "dbx-tools",
|
|
78
|
+
branch: "main",
|
|
79
|
+
endpoint: "projects/dbx-tools/branches/main/endpoints/primary",
|
|
80
|
+
database: "databricks_postgres",
|
|
81
|
+
// Auto-create behavior.
|
|
82
|
+
autoCreate: false, // throw if no project exists
|
|
83
|
+
// autoCreate: "my-custom-id", // create with this id
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`autopg()` returns a `Resolved` record (`project`, `branch`, `endpoint`,
|
|
88
|
+
`database`, `host`, `port`, `sslMode`). When `exportEnv: true` (the
|
|
89
|
+
default) it also writes the same values to `process.env`, only filling
|
|
90
|
+
gaps - existing values are preserved.
|
|
91
|
+
|
|
92
|
+
## Address parser
|
|
93
|
+
|
|
94
|
+
The address parser is exported as well if you want it without the
|
|
95
|
+
resolver wrapper:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { parseAddress } from "@dbx-tools/appkit-autopg";
|
|
99
|
+
|
|
100
|
+
parseAddress("postgresql://user@ep-foo.database.azuredatabricks.net/dbpg");
|
|
101
|
+
// { user, host, database, port?, sslMode?, project?, branch?, endpointId? }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Required permissions
|
|
105
|
+
|
|
106
|
+
The Databricks user / SP behind `getWorkspaceClient()` needs
|
|
107
|
+
`postgres.projects.{list,create}`, `postgres.branches.list`,
|
|
108
|
+
`postgres.endpoints.{list,get}`, and `postgres.databases.list` on the
|
|
109
|
+
account.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
Apache-2.0
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dbx-tools/appkit-autopg` - top-level Lakebase Postgres auto-
|
|
3
|
+
* discovery helper. Resolves project / branch / endpoint / database /
|
|
4
|
+
* host from env vars and the Databricks REST API, then writes the
|
|
5
|
+
* standard `PGHOST` / `PGDATABASE` / `LAKEBASE_ENDPOINT` env vars so
|
|
6
|
+
* the AppKit `lakebase` plugin can connect without manual wiring.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { autopg } from "@dbx-tools/appkit-autopg";
|
|
10
|
+
* import { createApp, lakebase, server } from "@databricks/appkit";
|
|
11
|
+
*
|
|
12
|
+
* await autopg();
|
|
13
|
+
* await createApp({ plugins: [lakebase(), server()] });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export * from "./src/address.js";
|
|
17
|
+
export * from "./src/autopg.js";
|
|
18
|
+
export * from "./src/resolver.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dbx-tools/appkit-autopg` - top-level Lakebase Postgres auto-
|
|
3
|
+
* discovery helper. Resolves project / branch / endpoint / database /
|
|
4
|
+
* host from env vars and the Databricks REST API, then writes the
|
|
5
|
+
* standard `PGHOST` / `PGDATABASE` / `LAKEBASE_ENDPOINT` env vars so
|
|
6
|
+
* the AppKit `lakebase` plugin can connect without manual wiring.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { autopg } from "@dbx-tools/appkit-autopg";
|
|
10
|
+
* import { createApp, lakebase, server } from "@databricks/appkit";
|
|
11
|
+
*
|
|
12
|
+
* await autopg();
|
|
13
|
+
* await createApp({ plugins: [lakebase(), server()] });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export * from "./src/address.js";
|
|
17
|
+
export * from "./src/autopg.js";
|
|
18
|
+
export * from "./src/resolver.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flexible address parser for Lakebase Postgres connection inputs.
|
|
3
|
+
*
|
|
4
|
+
* Accepts whatever shape a user is likely to paste into
|
|
5
|
+
* `LAKEBASE_ENDPOINT` (or the matching config field) and extracts
|
|
6
|
+
* every recognizable piece. Whatever it can't recover is left for the
|
|
7
|
+
* REST resolver to discover.
|
|
8
|
+
*
|
|
9
|
+
* Recognized formats:
|
|
10
|
+
*
|
|
11
|
+
* - **Postgres URI** -
|
|
12
|
+
* `postgresql://user@host:port/db?sslmode=require` (also `postgres://`).
|
|
13
|
+
* Yields `user`, `host`, `port`, `database`, `sslMode`.
|
|
14
|
+
*
|
|
15
|
+
* - **Canonical endpoint resource path** -
|
|
16
|
+
* `projects/{p}/branches/{b}/endpoints/{e}` -
|
|
17
|
+
* yields `project`, `branch`, `endpointId`, and the original string as
|
|
18
|
+
* `endpoint` (already in lakebase's expected form).
|
|
19
|
+
*
|
|
20
|
+
* - **Database resource path** -
|
|
21
|
+
* `projects/{p}/branches/{b}/databases/{d}` -
|
|
22
|
+
* yields `project`, `branch`. The database leaf isn't surfaced because
|
|
23
|
+
* it's a resource id, not the Postgres database name; the resolver
|
|
24
|
+
* will look up the real `status.postgres_database` value via REST.
|
|
25
|
+
*
|
|
26
|
+
* - **Branch resource path** -
|
|
27
|
+
* `projects/{p}/branches/{b}` - yields `project`, `branch`.
|
|
28
|
+
*
|
|
29
|
+
* - **Project resource path** -
|
|
30
|
+
* `projects/{p}` - yields `project`.
|
|
31
|
+
*
|
|
32
|
+
* - **Bare hostname** -
|
|
33
|
+
* `ep-steep-forest-e199v43w.database.eastus2.azuredatabricks.net` -
|
|
34
|
+
* yields `host` only; the resolver reverse-looks up the owning
|
|
35
|
+
* endpoint to recover the resource path.
|
|
36
|
+
*
|
|
37
|
+
* - **Bare project id** -
|
|
38
|
+
* `dbx-tools-demo` (1-63 chars, lowercase letters/digits/hyphens) -
|
|
39
|
+
* yields `project`.
|
|
40
|
+
*
|
|
41
|
+
* Returns an empty object for inputs it doesn't recognize.
|
|
42
|
+
*/
|
|
43
|
+
import type { SslMode } from "./resolver.js";
|
|
44
|
+
export interface ParsedAddress {
|
|
45
|
+
/** Lakebase project id. */
|
|
46
|
+
project?: string;
|
|
47
|
+
/** Branch id within the project. */
|
|
48
|
+
branch?: string;
|
|
49
|
+
/** Endpoint leaf id (last segment of an endpoint resource path). */
|
|
50
|
+
endpointId?: string;
|
|
51
|
+
/** Canonical endpoint resource path; only set for matching inputs. */
|
|
52
|
+
endpoint?: string;
|
|
53
|
+
/** Postgres database name (PGDATABASE) when parsed from a URI path. */
|
|
54
|
+
database?: string;
|
|
55
|
+
/** Postgres hostname. */
|
|
56
|
+
host?: string;
|
|
57
|
+
/** Postgres port. */
|
|
58
|
+
port?: number;
|
|
59
|
+
/** Postgres user (URI-decoded if encoded). */
|
|
60
|
+
user?: string;
|
|
61
|
+
/** Postgres TLS mode. */
|
|
62
|
+
sslMode?: SslMode;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse a Lakebase connection input into whatever pieces it carries.
|
|
66
|
+
* See module docstring for the supported formats. Returns `{}` for
|
|
67
|
+
* `undefined`, empty strings, and unrecognized inputs.
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseAddress(input: string | undefined | null): ParsedAddress;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flexible address parser for Lakebase Postgres connection inputs.
|
|
3
|
+
*
|
|
4
|
+
* Accepts whatever shape a user is likely to paste into
|
|
5
|
+
* `LAKEBASE_ENDPOINT` (or the matching config field) and extracts
|
|
6
|
+
* every recognizable piece. Whatever it can't recover is left for the
|
|
7
|
+
* REST resolver to discover.
|
|
8
|
+
*
|
|
9
|
+
* Recognized formats:
|
|
10
|
+
*
|
|
11
|
+
* - **Postgres URI** -
|
|
12
|
+
* `postgresql://user@host:port/db?sslmode=require` (also `postgres://`).
|
|
13
|
+
* Yields `user`, `host`, `port`, `database`, `sslMode`.
|
|
14
|
+
*
|
|
15
|
+
* - **Canonical endpoint resource path** -
|
|
16
|
+
* `projects/{p}/branches/{b}/endpoints/{e}` -
|
|
17
|
+
* yields `project`, `branch`, `endpointId`, and the original string as
|
|
18
|
+
* `endpoint` (already in lakebase's expected form).
|
|
19
|
+
*
|
|
20
|
+
* - **Database resource path** -
|
|
21
|
+
* `projects/{p}/branches/{b}/databases/{d}` -
|
|
22
|
+
* yields `project`, `branch`. The database leaf isn't surfaced because
|
|
23
|
+
* it's a resource id, not the Postgres database name; the resolver
|
|
24
|
+
* will look up the real `status.postgres_database` value via REST.
|
|
25
|
+
*
|
|
26
|
+
* - **Branch resource path** -
|
|
27
|
+
* `projects/{p}/branches/{b}` - yields `project`, `branch`.
|
|
28
|
+
*
|
|
29
|
+
* - **Project resource path** -
|
|
30
|
+
* `projects/{p}` - yields `project`.
|
|
31
|
+
*
|
|
32
|
+
* - **Bare hostname** -
|
|
33
|
+
* `ep-steep-forest-e199v43w.database.eastus2.azuredatabricks.net` -
|
|
34
|
+
* yields `host` only; the resolver reverse-looks up the owning
|
|
35
|
+
* endpoint to recover the resource path.
|
|
36
|
+
*
|
|
37
|
+
* - **Bare project id** -
|
|
38
|
+
* `dbx-tools-demo` (1-63 chars, lowercase letters/digits/hyphens) -
|
|
39
|
+
* yields `project`.
|
|
40
|
+
*
|
|
41
|
+
* Returns an empty object for inputs it doesn't recognize.
|
|
42
|
+
*/
|
|
43
|
+
const URL_SCHEME_RE = /^(postgres|postgresql):\/\//i;
|
|
44
|
+
const RESOURCE_ENDPOINT_RE = /^projects\/([^/]+)\/branches\/([^/]+)\/endpoints\/([^/]+)$/;
|
|
45
|
+
const RESOURCE_DATABASE_RE = /^projects\/([^/]+)\/branches\/([^/]+)\/databases\/([^/]+)$/;
|
|
46
|
+
const RESOURCE_BRANCH_RE = /^projects\/([^/]+)\/branches\/([^/]+)$/;
|
|
47
|
+
const RESOURCE_PROJECT_RE = /^projects\/([^/]+)$/;
|
|
48
|
+
const PROJECT_ID_RE = /^[a-z][a-z0-9-]{0,61}[a-z0-9]$|^[a-z]$/;
|
|
49
|
+
const HOSTNAME_HINT_RE = /^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)+$/i;
|
|
50
|
+
/**
|
|
51
|
+
* Parse a Lakebase connection input into whatever pieces it carries.
|
|
52
|
+
* See module docstring for the supported formats. Returns `{}` for
|
|
53
|
+
* `undefined`, empty strings, and unrecognized inputs.
|
|
54
|
+
*/
|
|
55
|
+
export function parseAddress(input) {
|
|
56
|
+
if (!input)
|
|
57
|
+
return {};
|
|
58
|
+
const s = input.trim();
|
|
59
|
+
if (!s)
|
|
60
|
+
return {};
|
|
61
|
+
if (URL_SCHEME_RE.test(s))
|
|
62
|
+
return parseUri(s);
|
|
63
|
+
if (s.startsWith("projects/"))
|
|
64
|
+
return parseResourcePath(s);
|
|
65
|
+
// Resource ids never contain dots; a dotted input must be a hostname.
|
|
66
|
+
if (HOSTNAME_HINT_RE.test(s) && s.includes("."))
|
|
67
|
+
return { host: s };
|
|
68
|
+
if (PROJECT_ID_RE.test(s))
|
|
69
|
+
return { project: s };
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
function parseUri(s) {
|
|
73
|
+
let url;
|
|
74
|
+
try {
|
|
75
|
+
url = new URL(s);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
const result = {};
|
|
81
|
+
if (url.hostname)
|
|
82
|
+
result.host = url.hostname;
|
|
83
|
+
if (url.port) {
|
|
84
|
+
const port = Number.parseInt(url.port, 10);
|
|
85
|
+
if (!Number.isNaN(port))
|
|
86
|
+
result.port = port;
|
|
87
|
+
}
|
|
88
|
+
if (url.username) {
|
|
89
|
+
try {
|
|
90
|
+
result.user = decodeURIComponent(url.username);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Malformed percent-escape; keep the raw form.
|
|
94
|
+
result.user = url.username;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const db = url.pathname.replace(/^\//, "");
|
|
98
|
+
if (db)
|
|
99
|
+
result.database = decodeURIComponent(db);
|
|
100
|
+
// Postgres clients accept `sslmode`; URL params are case-sensitive but
|
|
101
|
+
// we tolerate either since users paste both.
|
|
102
|
+
const sslmodeRaw = url.searchParams.get("sslmode") ?? url.searchParams.get("sslMode");
|
|
103
|
+
const sslmode = sslmodeRaw?.toLowerCase();
|
|
104
|
+
if (sslmode === "require" || sslmode === "disable" || sslmode === "prefer") {
|
|
105
|
+
result.sslMode = sslmode;
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
function parseResourcePath(s) {
|
|
110
|
+
const ep = RESOURCE_ENDPOINT_RE.exec(s);
|
|
111
|
+
if (ep) {
|
|
112
|
+
return {
|
|
113
|
+
project: ep[1],
|
|
114
|
+
branch: ep[2],
|
|
115
|
+
endpointId: ep[3],
|
|
116
|
+
endpoint: s,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// databases/{d} is a resource id (often kebab-case), not the actual
|
|
120
|
+
// Postgres database name. Surface project + branch only; the resolver
|
|
121
|
+
// will fetch the real `postgres_database` value.
|
|
122
|
+
const db = RESOURCE_DATABASE_RE.exec(s);
|
|
123
|
+
if (db)
|
|
124
|
+
return { project: db[1], branch: db[2] };
|
|
125
|
+
const br = RESOURCE_BRANCH_RE.exec(s);
|
|
126
|
+
if (br)
|
|
127
|
+
return { project: br[1], branch: br[2] };
|
|
128
|
+
const pr = RESOURCE_PROJECT_RE.exec(s);
|
|
129
|
+
if (pr)
|
|
130
|
+
return { project: pr[1] };
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level Lakebase auto-discovery helper.
|
|
3
|
+
*
|
|
4
|
+
* Read this once at process startup BEFORE `createApp(...)` so the
|
|
5
|
+
* `lakebase` plugin (and anyone else who reads `process.env` during
|
|
6
|
+
* `setup()`) sees a fully-populated environment:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { autopg } from "@dbx-tools/appkit-autopg";
|
|
10
|
+
* import { createApp, lakebase, server } from "@databricks/appkit";
|
|
11
|
+
*
|
|
12
|
+
* await autopg(); // resolves + writes process.env
|
|
13
|
+
* await createApp({ plugins: [lakebase(), server()] });
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* `autopg` is intentionally NOT an AppKit plugin. AppKit's `static phase`
|
|
17
|
+
* field only orders plugin `setup()` invocation, not async completion -
|
|
18
|
+
* `lakebase.setup()` calls `parsePoolConfig` synchronously after its
|
|
19
|
+
* first `await` and would throw on `PGHOST` before any sibling plugin's
|
|
20
|
+
* REST resolution could finish. Awaiting `autopg()` upfront sidesteps
|
|
21
|
+
* the race entirely.
|
|
22
|
+
*
|
|
23
|
+
* Inputs flow in this priority order:
|
|
24
|
+
* 1. Explicit `config.<field>` argument
|
|
25
|
+
* 2. Matching env var (`LAKEBASE_PROJECT`, `LAKEBASE_BRANCH`,
|
|
26
|
+
* `LAKEBASE_ENDPOINT`, `PGHOST`, `PGDATABASE`, `PGPORT`, `PGSSLMODE`)
|
|
27
|
+
* 3. Derived from the Lakebase Autoscaling REST API under
|
|
28
|
+
* `/api/2.0/postgres/` via the Databricks workspace client
|
|
29
|
+
*
|
|
30
|
+
* Resolved values are written back to `process.env` (only filling gaps;
|
|
31
|
+
* existing values are preserved) so the downstream `lakebase` plugin
|
|
32
|
+
* picks them up. Pass `{ exportEnv: false }` to keep `process.env`
|
|
33
|
+
* untouched and just inspect the returned record.
|
|
34
|
+
*/
|
|
35
|
+
import { type Resolved, type ResolverInputs } from "./resolver.js";
|
|
36
|
+
/** Options accepted by {@link autopg}. */
|
|
37
|
+
export interface AutopgOptions extends ResolverInputs {
|
|
38
|
+
/**
|
|
39
|
+
* When `true` (the default), resolved values are written to
|
|
40
|
+
* `process.env` so the `lakebase` plugin sees them at startup.
|
|
41
|
+
* Set to `false` to leave `process.env` untouched and just receive
|
|
42
|
+
* the resolved record back.
|
|
43
|
+
*/
|
|
44
|
+
exportEnv?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve Lakebase Postgres connection info from config + env (and the
|
|
48
|
+
* Databricks REST API when needed), write the resolved values to
|
|
49
|
+
* `process.env`, and return the fully-populated record.
|
|
50
|
+
*
|
|
51
|
+
* Always safe to call: when env already provides every field, it
|
|
52
|
+
* returns immediately without any network traffic.
|
|
53
|
+
*
|
|
54
|
+
* @throws when a `project` is set (directly or via env) but the
|
|
55
|
+
* Databricks API returns no branches / endpoints / databases to
|
|
56
|
+
* choose from. The error message lists the available candidates so
|
|
57
|
+
* the caller can pin the right one via env or config.
|
|
58
|
+
*/
|
|
59
|
+
export declare function autopg(opts?: AutopgOptions): Promise<Resolved>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level Lakebase auto-discovery helper.
|
|
3
|
+
*
|
|
4
|
+
* Read this once at process startup BEFORE `createApp(...)` so the
|
|
5
|
+
* `lakebase` plugin (and anyone else who reads `process.env` during
|
|
6
|
+
* `setup()`) sees a fully-populated environment:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { autopg } from "@dbx-tools/appkit-autopg";
|
|
10
|
+
* import { createApp, lakebase, server } from "@databricks/appkit";
|
|
11
|
+
*
|
|
12
|
+
* await autopg(); // resolves + writes process.env
|
|
13
|
+
* await createApp({ plugins: [lakebase(), server()] });
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* `autopg` is intentionally NOT an AppKit plugin. AppKit's `static phase`
|
|
17
|
+
* field only orders plugin `setup()` invocation, not async completion -
|
|
18
|
+
* `lakebase.setup()` calls `parsePoolConfig` synchronously after its
|
|
19
|
+
* first `await` and would throw on `PGHOST` before any sibling plugin's
|
|
20
|
+
* REST resolution could finish. Awaiting `autopg()` upfront sidesteps
|
|
21
|
+
* the race entirely.
|
|
22
|
+
*
|
|
23
|
+
* Inputs flow in this priority order:
|
|
24
|
+
* 1. Explicit `config.<field>` argument
|
|
25
|
+
* 2. Matching env var (`LAKEBASE_PROJECT`, `LAKEBASE_BRANCH`,
|
|
26
|
+
* `LAKEBASE_ENDPOINT`, `PGHOST`, `PGDATABASE`, `PGPORT`, `PGSSLMODE`)
|
|
27
|
+
* 3. Derived from the Lakebase Autoscaling REST API under
|
|
28
|
+
* `/api/2.0/postgres/` via the Databricks workspace client
|
|
29
|
+
*
|
|
30
|
+
* Resolved values are written back to `process.env` (only filling gaps;
|
|
31
|
+
* existing values are preserved) so the downstream `lakebase` plugin
|
|
32
|
+
* picks them up. Pass `{ exportEnv: false }` to keep `process.env`
|
|
33
|
+
* untouched and just inspect the returned record.
|
|
34
|
+
*/
|
|
35
|
+
import { logUtils } from "@dbx-tools/appkit-shared";
|
|
36
|
+
import { applyToEnv, resolveConnection, } from "./resolver.js";
|
|
37
|
+
/**
|
|
38
|
+
* Resolve Lakebase Postgres connection info from config + env (and the
|
|
39
|
+
* Databricks REST API when needed), write the resolved values to
|
|
40
|
+
* `process.env`, and return the fully-populated record.
|
|
41
|
+
*
|
|
42
|
+
* Always safe to call: when env already provides every field, it
|
|
43
|
+
* returns immediately without any network traffic.
|
|
44
|
+
*
|
|
45
|
+
* @throws when a `project` is set (directly or via env) but the
|
|
46
|
+
* Databricks API returns no branches / endpoints / databases to
|
|
47
|
+
* choose from. The error message lists the available candidates so
|
|
48
|
+
* the caller can pin the right one via env or config.
|
|
49
|
+
*/
|
|
50
|
+
export async function autopg(opts = {}) {
|
|
51
|
+
const { exportEnv = true, ...inputs } = opts;
|
|
52
|
+
const log = logUtils.logger("autopg");
|
|
53
|
+
const resolved = await resolveConnection(inputs, log);
|
|
54
|
+
if (exportEnv) {
|
|
55
|
+
applyToEnv(resolved);
|
|
56
|
+
log.info("env updated", redactForLog(resolved));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
log.info("resolved (env untouched)", redactForLog(resolved));
|
|
60
|
+
}
|
|
61
|
+
return resolved;
|
|
62
|
+
}
|
|
63
|
+
/** Strip resolved record to log-safe primitive fields. */
|
|
64
|
+
function redactForLog(resolved) {
|
|
65
|
+
return {
|
|
66
|
+
project: resolved.project,
|
|
67
|
+
branch: resolved.branch,
|
|
68
|
+
endpoint: resolved.endpoint,
|
|
69
|
+
database: resolved.database,
|
|
70
|
+
host: resolved.host,
|
|
71
|
+
port: resolved.port,
|
|
72
|
+
sslMode: resolved.sslMode,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lakebase Postgres connection resolver.
|
|
3
|
+
*
|
|
4
|
+
* Reads the same env vars the `lakebase` plugin consumes (`PGHOST`,
|
|
5
|
+
* `PGDATABASE`, `PGPORT`, `PGSSLMODE`, `LAKEBASE_ENDPOINT`) and fills in
|
|
6
|
+
* whichever pieces are missing using the Lakebase Autoscaling REST API
|
|
7
|
+
* under `/api/2.0/postgres/` via the Databricks workspace client.
|
|
8
|
+
*
|
|
9
|
+
* `LAKEBASE_ENDPOINT` (and `config.endpoint`) accept anything
|
|
10
|
+
* {@link parseAddress} understands - canonical resource paths, Postgres
|
|
11
|
+
* URIs, bare hostnames, or bare project ids. The resolver layers
|
|
12
|
+
* whatever pieces fall out of parsing under explicit config / env
|
|
13
|
+
* values, then fills the remaining gaps via the API:
|
|
14
|
+
*
|
|
15
|
+
* 1. Reverse-lookup: when a host is known but no resource path is,
|
|
16
|
+
* scan projects -> branches -> endpoints for a matching
|
|
17
|
+
* `status.hosts.host` and recover the owning project/branch/endpoint.
|
|
18
|
+
* 2. Pick: when a project is known but child resources aren't, prefer
|
|
19
|
+
* the server-side default (`status.default`, `ENDPOINT_TYPE_READ_WRITE`,
|
|
20
|
+
* `databricks_postgres`) and fall back to "the only one" when a
|
|
21
|
+
* listing returns a single result.
|
|
22
|
+
* 3. Auto-create: when no projects exist at all, create one whose
|
|
23
|
+
* id defaults to `projectUtils.name()` slugified (override
|
|
24
|
+
* with `config.autoCreate: "my-id"` or disable with
|
|
25
|
+
* `config.autoCreate: false`). The create call is idempotent - an
|
|
26
|
+
* `ALREADY_EXISTS` response from a concurrent boot is treated as
|
|
27
|
+
* success. Then poll the default endpoint until it reports
|
|
28
|
+
* `current_state` `READY` or `IDLE`.
|
|
29
|
+
*
|
|
30
|
+
* The {@link autopg} helper then writes the resolved values back to
|
|
31
|
+
* `process.env` so the downstream `lakebase` plugin picks them up.
|
|
32
|
+
*
|
|
33
|
+
* @see https://docs.databricks.com/api/workspace/postgres
|
|
34
|
+
*/
|
|
35
|
+
import { type logUtils } from "@dbx-tools/appkit-shared";
|
|
36
|
+
/** Postgres TLS mode passed through to `pg`. */
|
|
37
|
+
export type SslMode = "require" | "disable" | "prefer";
|
|
38
|
+
/**
|
|
39
|
+
* User-supplied inputs (config or env) before any API resolution. Every
|
|
40
|
+
* field is optional - the resolver tries to fill in missing pieces from
|
|
41
|
+
* the Lakebase API when it has enough context (typically a `project`).
|
|
42
|
+
*/
|
|
43
|
+
export interface ResolverInputs {
|
|
44
|
+
/** Lakebase project id, e.g. `my-app`. Triggers API discovery when set. */
|
|
45
|
+
project?: string;
|
|
46
|
+
/** Branch id within the project. Defaults to the server-marked default. */
|
|
47
|
+
branch?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Lakebase address - accepts a canonical endpoint/branch/project
|
|
50
|
+
* resource path, a Postgres URI (`postgresql://user@host/db?...`),
|
|
51
|
+
* a bare Lakebase hostname, or a bare project id. Whatever pieces it
|
|
52
|
+
* carries seed the resolver before REST lookups happen. Reads from
|
|
53
|
+
* `LAKEBASE_ENDPOINT` when not set.
|
|
54
|
+
*/
|
|
55
|
+
endpoint?: string;
|
|
56
|
+
/** Postgres database name (e.g. `databricks_postgres`). */
|
|
57
|
+
database?: string;
|
|
58
|
+
/** Postgres hostname; auto-derived from the endpoint when missing. */
|
|
59
|
+
host?: string;
|
|
60
|
+
/** Postgres port. Defaults to 5432. */
|
|
61
|
+
port?: number;
|
|
62
|
+
/** TLS mode. Defaults to `require`. */
|
|
63
|
+
sslMode?: SslMode;
|
|
64
|
+
/**
|
|
65
|
+
* What to do when no project exists in the workspace at all.
|
|
66
|
+
* - `undefined` (default): derive a project id from
|
|
67
|
+
* {@link projectUtils.name} (the host repo's `package.json`
|
|
68
|
+
* name) slugified to Lakebase id constraints, then create it.
|
|
69
|
+
* - `string`: create a new project with this exact id.
|
|
70
|
+
* - `false`: skip creation and throw with a clear error message.
|
|
71
|
+
*/
|
|
72
|
+
autoCreate?: string | false;
|
|
73
|
+
}
|
|
74
|
+
/** Fully-resolved connection. `port` and `sslMode` always have a value. */
|
|
75
|
+
export interface Resolved {
|
|
76
|
+
project?: string;
|
|
77
|
+
branch?: string;
|
|
78
|
+
endpoint?: string;
|
|
79
|
+
database?: string;
|
|
80
|
+
host?: string;
|
|
81
|
+
port: number;
|
|
82
|
+
sslMode: SslMode;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Pull resolver inputs from `process.env`, parse the address blob, and
|
|
86
|
+
* layer explicit config on top with this precedence:
|
|
87
|
+
*
|
|
88
|
+
* `config.<field>` > matching env var > whatever {@link parseAddress}
|
|
89
|
+
* recovered from the `endpoint` / `LAKEBASE_ENDPOINT` blob.
|
|
90
|
+
*/
|
|
91
|
+
export declare function readInputs(config: ResolverInputs): ResolverInputs;
|
|
92
|
+
/**
|
|
93
|
+
* Resolve a fully-populated Postgres connection record from config + env.
|
|
94
|
+
*
|
|
95
|
+
* Returns immediately without network traffic when env already supplies
|
|
96
|
+
* `endpoint`, `host`, and `database`. Otherwise issues REST calls and
|
|
97
|
+
* may auto-create a project (see module docstring).
|
|
98
|
+
*/
|
|
99
|
+
export declare function resolveConnection(config: ResolverInputs, log: logUtils.Logger): Promise<Resolved>;
|
|
100
|
+
/**
|
|
101
|
+
* Write resolved values back to `process.env` so the `lakebase` plugin
|
|
102
|
+
* (which reads env directly) picks them up during its own `setup()`.
|
|
103
|
+
* Existing env values are preserved; only missing keys are filled in,
|
|
104
|
+
* which keeps explicit overrides authoritative.
|
|
105
|
+
*/
|
|
106
|
+
export declare function applyToEnv(resolved: Resolved): void;
|
|
107
|
+
/** Parse `projects/{p}/branches/{b}/endpoints/{e}` into its components. */
|
|
108
|
+
export declare function parseEndpointName(endpoint: string): {
|
|
109
|
+
project: string;
|
|
110
|
+
branch: string;
|
|
111
|
+
endpointId: string;
|
|
112
|
+
} | null;
|
|
113
|
+
/** Parse `projects/{p}/branches/{b}/databases/{d}` into its components. */
|
|
114
|
+
export declare function parseDatabaseName(database: string): {
|
|
115
|
+
project: string;
|
|
116
|
+
branch: string;
|
|
117
|
+
databaseId: string;
|
|
118
|
+
} | null;
|