@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.
@@ -0,0 +1,507 @@
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 { getWorkspaceClient } from "@databricks/appkit";
36
+ import { projectUtils, stringUtils } from "@dbx-tools/appkit-shared";
37
+ import { setTimeout as sleep } from "node:timers/promises";
38
+ import { parseAddress } from "./address.js";
39
+ const API_BASE = "/api/2.0/postgres";
40
+ const DEFAULT_PORT = 5432;
41
+ const DEFAULT_SSL_MODE = "require";
42
+ const DEFAULT_PG_VERSION = 17;
43
+ /** Lakebase project ids: `^[a-z][a-z0-9-]{0,61}[a-z0-9]$`. */
44
+ const PROJECT_ID_MAX_LEN = 63;
45
+ const OPERATION_TIMEOUT_MS = 5 * 60_000;
46
+ const OPERATION_POLL_MS = 2_000;
47
+ const ENDPOINT_READY_TIMEOUT_MS = 5 * 60_000;
48
+ const ENDPOINT_READY_POLL_MS = 2_000;
49
+ const ENDPOINT_NAME_RE = /^projects\/([^/]+)\/branches\/([^/]+)\/endpoints\/([^/]+)$/;
50
+ const DATABASE_NAME_RE = /^projects\/([^/]+)\/branches\/([^/]+)\/databases\/([^/]+)$/;
51
+ const BRANCH_NAME_RE = /^projects\/([^/]+)\/branches\/([^/]+)$/;
52
+ const PROJECT_NAME_RE = /^projects\/([^/]+)$/;
53
+ /**
54
+ * Pull resolver inputs from `process.env`, parse the address blob, and
55
+ * layer explicit config on top with this precedence:
56
+ *
57
+ * `config.<field>` > matching env var > whatever {@link parseAddress}
58
+ * recovered from the `endpoint` / `LAKEBASE_ENDPOINT` blob.
59
+ */
60
+ export function readInputs(config) {
61
+ const rawAddress = config.endpoint ?? process.env.LAKEBASE_ENDPOINT;
62
+ const parsed = parseAddress(rawAddress);
63
+ const portEnv = process.env.PGPORT;
64
+ return {
65
+ project: config.project ?? process.env.LAKEBASE_PROJECT ?? parsed.project,
66
+ branch: config.branch ?? process.env.LAKEBASE_BRANCH ?? parsed.branch,
67
+ // Only canonical endpoint resource paths survive here; URIs and
68
+ // bare hostnames set `host` instead and leave `endpoint` undefined
69
+ // until the REST resolver fills it in.
70
+ endpoint: parsed.endpoint,
71
+ database: config.database ?? process.env.PGDATABASE ?? parsed.database,
72
+ host: config.host ?? process.env.PGHOST ?? parsed.host,
73
+ port: config.port ??
74
+ (portEnv ? Number.parseInt(portEnv, 10) : undefined) ??
75
+ parsed.port,
76
+ sslMode: config.sslMode ??
77
+ process.env.PGSSLMODE ??
78
+ parsed.sslMode,
79
+ autoCreate: config.autoCreate,
80
+ };
81
+ }
82
+ /**
83
+ * Resolve a fully-populated Postgres connection record from config + env.
84
+ *
85
+ * Returns immediately without network traffic when env already supplies
86
+ * `endpoint`, `host`, and `database`. Otherwise issues REST calls and
87
+ * may auto-create a project (see module docstring).
88
+ */
89
+ export async function resolveConnection(config, log) {
90
+ const inputs = readInputs(config);
91
+ let { project, branch, endpoint, database, host } = inputs;
92
+ const port = inputs.port ?? DEFAULT_PORT;
93
+ const sslMode = inputs.sslMode ?? DEFAULT_SSL_MODE;
94
+ // Resource paths may carry redundant info; harvest project/branch
95
+ // from any canonical path that snuck in via PGDATABASE or similar.
96
+ if (endpoint && (!project || !branch)) {
97
+ const parsed = parseEndpointName(endpoint);
98
+ if (parsed) {
99
+ project ??= parsed.project;
100
+ branch ??= parsed.branch;
101
+ }
102
+ }
103
+ if (database && (!project || !branch)) {
104
+ const parsed = parseDatabaseName(database);
105
+ if (parsed) {
106
+ project ??= parsed.project;
107
+ branch ??= parsed.branch;
108
+ database = parsed.databaseId;
109
+ }
110
+ }
111
+ // Already complete: skip every REST call.
112
+ if (endpoint && host && database) {
113
+ return { project, branch, endpoint, database, host, port, sslMode };
114
+ }
115
+ const ws = getWorkspaceClient({});
116
+ // Host known but no resource path: scan the workspace to find which
117
+ // endpoint owns this host so we can populate LAKEBASE_ENDPOINT.
118
+ if (!project && host) {
119
+ const found = await findEndpointByHost(ws, host, log);
120
+ if (found) {
121
+ project = found.project;
122
+ branch = found.branch;
123
+ endpoint ??= found.endpoint;
124
+ }
125
+ }
126
+ // No project anywhere in config/env/address: list, pick, or create.
127
+ if (!project) {
128
+ project = await pickOrCreateProject(ws, config.autoCreate, log);
129
+ }
130
+ if (!branch) {
131
+ branch = await pickBranch(ws, project, log);
132
+ }
133
+ if (!endpoint) {
134
+ const ep = await pickEndpoint(ws, project, branch, log);
135
+ endpoint = ep.name;
136
+ host ??= ep.host;
137
+ }
138
+ if (!host && endpoint) {
139
+ const parsed = parseEndpointName(endpoint);
140
+ if (parsed) {
141
+ const ep = await waitEndpointReady(ws, parsed.project, parsed.branch, parsed.endpointId, log);
142
+ host = ep.status?.hosts?.host;
143
+ log.info("autopg: resolved host from endpoint", { host });
144
+ }
145
+ }
146
+ if (!database) {
147
+ database = await pickDatabase(ws, project, branch, log);
148
+ }
149
+ return { project, branch, endpoint, database, host, port, sslMode };
150
+ }
151
+ /**
152
+ * Write resolved values back to `process.env` so the `lakebase` plugin
153
+ * (which reads env directly) picks them up during its own `setup()`.
154
+ * Existing env values are preserved; only missing keys are filled in,
155
+ * which keeps explicit overrides authoritative.
156
+ */
157
+ export function applyToEnv(resolved) {
158
+ if (resolved.endpoint)
159
+ process.env.LAKEBASE_ENDPOINT ??= resolved.endpoint;
160
+ if (resolved.host)
161
+ process.env.PGHOST ??= resolved.host;
162
+ if (resolved.database)
163
+ process.env.PGDATABASE ??= resolved.database;
164
+ process.env.PGPORT ??= String(resolved.port);
165
+ process.env.PGSSLMODE ??= resolved.sslMode;
166
+ if (resolved.project)
167
+ process.env.LAKEBASE_PROJECT ??= resolved.project;
168
+ if (resolved.branch)
169
+ process.env.LAKEBASE_BRANCH ??= resolved.branch;
170
+ }
171
+ /** Parse `projects/{p}/branches/{b}/endpoints/{e}` into its components. */
172
+ export function parseEndpointName(endpoint) {
173
+ const m = ENDPOINT_NAME_RE.exec(endpoint);
174
+ if (!m)
175
+ return null;
176
+ return { project: m[1], branch: m[2], endpointId: m[3] };
177
+ }
178
+ /** Parse `projects/{p}/branches/{b}/databases/{d}` into its components. */
179
+ export function parseDatabaseName(database) {
180
+ const m = DATABASE_NAME_RE.exec(database);
181
+ if (!m)
182
+ return null;
183
+ return { project: m[1], branch: m[2], databaseId: m[3] };
184
+ }
185
+ /** Extract the branch id from a full branch resource path. */
186
+ function branchIdFromName(name) {
187
+ if (!name)
188
+ return undefined;
189
+ const m = BRANCH_NAME_RE.exec(name);
190
+ return m?.[2];
191
+ }
192
+ /** Extract the project id from a full project resource path. */
193
+ function projectIdFromName(name) {
194
+ if (!name)
195
+ return undefined;
196
+ const m = PROJECT_NAME_RE.exec(name);
197
+ return m?.[1];
198
+ }
199
+ /** GET helper that always parses JSON and forwards through `apiClient`. */
200
+ async function getJson(ws, path) {
201
+ const res = await ws.apiClient.request({
202
+ path,
203
+ method: "GET",
204
+ headers: new Headers({ Accept: "application/json" }),
205
+ raw: false,
206
+ });
207
+ return res;
208
+ }
209
+ /** POST helper for create / mutate calls; returns the parsed JSON body. */
210
+ async function postJson(ws, path, body, query) {
211
+ const res = await ws.apiClient.request({
212
+ path,
213
+ method: "POST",
214
+ query,
215
+ headers: new Headers({
216
+ Accept: "application/json",
217
+ "Content-Type": "application/json",
218
+ }),
219
+ raw: false,
220
+ payload: body,
221
+ });
222
+ return res;
223
+ }
224
+ async function listProjects(ws) {
225
+ const res = await getJson(ws, `${API_BASE}/projects`);
226
+ return res.projects ?? [];
227
+ }
228
+ async function listBranches(ws, project) {
229
+ const res = await getJson(ws, `${API_BASE}/projects/${project}/branches`);
230
+ return res.branches ?? [];
231
+ }
232
+ async function listEndpoints(ws, project, branch) {
233
+ const res = await getJson(ws, `${API_BASE}/projects/${project}/branches/${branch}/endpoints`);
234
+ return res.endpoints ?? [];
235
+ }
236
+ async function listDatabases(ws, project, branch) {
237
+ const res = await getJson(ws, `${API_BASE}/projects/${project}/branches/${branch}/databases`);
238
+ return res.databases ?? [];
239
+ }
240
+ async function getEndpoint(ws, project, branch, endpointId) {
241
+ return getJson(ws, `${API_BASE}/projects/${project}/branches/${branch}/endpoints/${endpointId}`);
242
+ }
243
+ /**
244
+ * Scan the workspace for an endpoint whose `status.hosts.host` matches
245
+ * the provided hostname. Used to recover the owning project/branch/
246
+ * endpoint resource path when the caller only supplied a Postgres URI.
247
+ *
248
+ * O(projects * branches * endpoints) - fine for typical workspaces
249
+ * (single digits per tier); pagination is intentionally not followed
250
+ * since this is a best-effort fallback.
251
+ */
252
+ async function findEndpointByHost(ws, host, log) {
253
+ const projects = await listProjects(ws);
254
+ for (const p of projects) {
255
+ const projectId = projectIdFromName(p.name);
256
+ if (!projectId)
257
+ continue;
258
+ const branches = await listBranches(ws, projectId);
259
+ for (const b of branches) {
260
+ const branchId = branchIdFromName(b.name);
261
+ if (!branchId)
262
+ continue;
263
+ const endpoints = await listEndpoints(ws, projectId, branchId);
264
+ const match = endpoints.find((e) => e.status?.hosts?.host === host);
265
+ if (match?.name) {
266
+ log.info("autopg: matched endpoint by host", {
267
+ host,
268
+ endpoint: match.name,
269
+ });
270
+ return {
271
+ project: projectId,
272
+ branch: branchId,
273
+ endpoint: match.name,
274
+ };
275
+ }
276
+ }
277
+ }
278
+ log.debug("autopg: no endpoint matched host", { host });
279
+ return null;
280
+ }
281
+ /**
282
+ * Pick the project to use, or create one when the workspace is empty.
283
+ *
284
+ * Selection order:
285
+ * 1. Exactly one project listed -> use it.
286
+ * 2. Zero projects AND `autoCreate !== false` -> ensure a project with
287
+ * the resolved id exists, then return its id.
288
+ * 3. Zero projects AND `autoCreate === false` -> throw.
289
+ * 4. Multiple projects -> throw with the candidate list (set
290
+ * `LAKEBASE_PROJECT` to disambiguate).
291
+ */
292
+ async function pickOrCreateProject(ws, autoCreate, log) {
293
+ const projects = await listProjects(ws);
294
+ if (projects.length === 1) {
295
+ const id = projectIdFromName(projects[0].name);
296
+ if (id) {
297
+ log.info("autopg: using only project", { project: id });
298
+ return id;
299
+ }
300
+ }
301
+ if (projects.length === 0) {
302
+ if (autoCreate === false) {
303
+ throw new Error("autopg: no Lakebase projects found and `autoCreate: false`; create a project or set LAKEBASE_PROJECT");
304
+ }
305
+ const id = autoCreate ?? (await defaultProjectId());
306
+ return ensureProject(ws, id, log);
307
+ }
308
+ const candidates = projects
309
+ .map((p) => projectIdFromName(p.name))
310
+ .filter((id) => Boolean(id))
311
+ .join(", ");
312
+ throw new Error(`autopg: multiple projects found; set LAKEBASE_PROJECT or config.project. Candidates: ${candidates}`);
313
+ }
314
+ /**
315
+ * Derive a Lakebase project id from the host repo's `package.json`
316
+ * name (via {@link projectUtils.name}) slugified to satisfy the
317
+ * Lakebase id constraint (`^[a-z][a-z0-9-]{0,61}[a-z0-9]$`).
318
+ *
319
+ * Throws when the slug ends up empty or starts with a digit, since the
320
+ * server would reject it anyway - callers should pass an explicit
321
+ * `autoCreate` id in that case.
322
+ */
323
+ async function defaultProjectId() {
324
+ const name = await projectUtils.name();
325
+ const slug = stringUtils.toSlugWithOptions({ maxLength: PROJECT_ID_MAX_LEN }, name);
326
+ if (!slug || !/^[a-z]/.test(slug)) {
327
+ throw new Error(`autopg: could not derive a Lakebase project id from project name '${name}'; pass autoCreate explicitly`);
328
+ }
329
+ return slug;
330
+ }
331
+ /**
332
+ * Ensure a Lakebase project with `projectId` exists. Creates it and
333
+ * waits for the create operation to complete. An `ALREADY_EXISTS`
334
+ * response is treated as success - someone else (a concurrent boot,
335
+ * a sibling process) won the race and the project we wanted is now
336
+ * sitting there ready for downstream pickers.
337
+ *
338
+ * Project creation typically provisions a default `production` branch
339
+ * alongside; downstream pickers handle the rest.
340
+ */
341
+ async function ensureProject(ws, projectId, log) {
342
+ log.warn("autopg: no projects found; creating", { project: projectId });
343
+ try {
344
+ const op = await postJson(ws, `${API_BASE}/projects`, { spec: { pg_version: DEFAULT_PG_VERSION } }, { project_id: projectId });
345
+ await waitForOperation(ws, op, log);
346
+ log.info("autopg: created project", { project: projectId });
347
+ }
348
+ catch (err) {
349
+ if (!isAlreadyExistsError(err))
350
+ throw err;
351
+ log.info("autopg: project already exists (race); proceeding", {
352
+ project: projectId,
353
+ });
354
+ }
355
+ return projectId;
356
+ }
357
+ /**
358
+ * Recognize the Databricks SDK's `ALREADY_EXISTS` failure modes so a
359
+ * lost race during `ensureProject` becomes a no-op instead of an error.
360
+ *
361
+ * The SDK throws `ApiError { errorCode, statusCode }` for structured
362
+ * server errors and `HttpError { code }` for transport-layer 4xx/5xx.
363
+ * Both surface a human message that often carries "already exists" so
364
+ * we use that as a final fallback for forward compatibility.
365
+ */
366
+ function isAlreadyExistsError(error) {
367
+ if (!error || typeof error !== "object")
368
+ return false;
369
+ const e = error;
370
+ if (e.statusCode === 409 || e.code === 409)
371
+ return true;
372
+ if (e.errorCode && /already.?exists/i.test(e.errorCode))
373
+ return true;
374
+ if (e.message && /already.?exists/i.test(e.message))
375
+ return true;
376
+ return false;
377
+ }
378
+ /**
379
+ * Poll a Lakebase long-running operation until `done: true`. Returns
380
+ * the final operation envelope (which may carry `response` or `error`).
381
+ *
382
+ * Throws when:
383
+ * - the response carries an `error` field;
384
+ * - `op.name` is missing (nothing to poll);
385
+ * - the timeout elapses before `done: true`.
386
+ */
387
+ async function waitForOperation(ws, op, log) {
388
+ if (op.done) {
389
+ if (op.error) {
390
+ throw new Error(`autopg: operation failed: ${JSON.stringify(op.error)}`);
391
+ }
392
+ return op;
393
+ }
394
+ const opName = op.name;
395
+ if (!opName) {
396
+ throw new Error("autopg: operation response has no name to poll");
397
+ }
398
+ const start = Date.now();
399
+ while (Date.now() - start < OPERATION_TIMEOUT_MS) {
400
+ await sleep(OPERATION_POLL_MS);
401
+ const current = await getJson(ws, `${API_BASE}/${opName}`);
402
+ log.debug("autopg: operation status", { op: opName, done: current.done });
403
+ if (current.done) {
404
+ if (current.error) {
405
+ throw new Error(`autopg: operation '${opName}' failed: ${JSON.stringify(current.error)}`);
406
+ }
407
+ return current;
408
+ }
409
+ }
410
+ throw new Error(`autopg: operation '${opName}' did not complete within ${OPERATION_TIMEOUT_MS}ms`);
411
+ }
412
+ /**
413
+ * Poll `getEndpoint` until the compute reports a usable
414
+ * `status.current_state`. `READY` and `IDLE` are both acceptable -
415
+ * `IDLE` just means the compute has scaled to zero but a connection
416
+ * will wake it. Returns the final endpoint payload (with `hosts.host`).
417
+ */
418
+ async function waitEndpointReady(ws, project, branch, endpointId, log) {
419
+ const start = Date.now();
420
+ let last = null;
421
+ while (Date.now() - start < ENDPOINT_READY_TIMEOUT_MS) {
422
+ last = await getEndpoint(ws, project, branch, endpointId);
423
+ const state = last.status?.current_state;
424
+ if (state === "READY" || state === "IDLE")
425
+ return last;
426
+ if (last.status?.hosts?.host && state !== "INITIALIZING") {
427
+ // Compute is in some other state (STARTING, etc.) but hostname is
428
+ // already published - good enough to connect; lakebase's OAuth
429
+ // token request will wake it.
430
+ return last;
431
+ }
432
+ log.debug("autopg: waiting for endpoint", { endpointId, state });
433
+ await sleep(ENDPOINT_READY_POLL_MS);
434
+ }
435
+ throw new Error(`autopg: endpoint '${endpointId}' under projects/${project}/branches/${branch} did not become ready within ${ENDPOINT_READY_TIMEOUT_MS}ms (last state: ${last?.status?.current_state ?? "unknown"})`);
436
+ }
437
+ /**
438
+ * Pick the default branch for a project. Prefers the branch flagged
439
+ * `status.default: true` (server-side default, typically `production`
440
+ * unless the project owner changed it). Falls back to the only branch
441
+ * when there's exactly one. Otherwise throws with the candidate list.
442
+ */
443
+ async function pickBranch(ws, project, log) {
444
+ const branches = await listBranches(ws, project);
445
+ if (branches.length === 0) {
446
+ throw new Error(`autopg: project '${project}' has no branches; cannot resolve a default`);
447
+ }
448
+ const flagged = branches.find((b) => b.status?.default === true);
449
+ const choice = branchIdFromName(flagged?.name) ??
450
+ (branches.length === 1 ? branchIdFromName(branches[0].name) : undefined);
451
+ if (!choice) {
452
+ const candidates = branches
453
+ .map((b) => branchIdFromName(b.name))
454
+ .filter((id) => Boolean(id))
455
+ .join(", ");
456
+ throw new Error(`autopg: project '${project}' has multiple branches and none marked default; set LAKEBASE_BRANCH or config.branch. Candidates: ${candidates}`);
457
+ }
458
+ log.info("autopg: resolved branch", { project, branch: choice });
459
+ return choice;
460
+ }
461
+ /**
462
+ * Pick the primary endpoint for a (project, branch). Prefers
463
+ * `status.endpoint_type === ENDPOINT_TYPE_READ_WRITE`; falls back to
464
+ * the only endpoint when there's exactly one. Returns `{ name, host }`
465
+ * so the caller can populate both `LAKEBASE_ENDPOINT` and `PGHOST`
466
+ * from a single call.
467
+ */
468
+ async function pickEndpoint(ws, project, branch, log) {
469
+ const endpoints = await listEndpoints(ws, project, branch);
470
+ if (endpoints.length === 0) {
471
+ throw new Error(`autopg: branch 'projects/${project}/branches/${branch}' has no endpoints; cannot resolve LAKEBASE_ENDPOINT`);
472
+ }
473
+ const primary = endpoints.find((e) => e.status?.endpoint_type === "ENDPOINT_TYPE_READ_WRITE") ??
474
+ (endpoints.length === 1 ? endpoints[0] : undefined);
475
+ if (!primary?.name) {
476
+ const names = endpoints.map((e) => e.name).filter(Boolean);
477
+ throw new Error(`autopg: branch has no primary READ_WRITE endpoint; set LAKEBASE_ENDPOINT or config.endpoint. Candidates: ${names.join(", ")}`);
478
+ }
479
+ const host = primary.status?.hosts?.host;
480
+ log.info("autopg: resolved endpoint", { endpoint: primary.name, host });
481
+ return { name: primary.name, host };
482
+ }
483
+ /**
484
+ * Pick the default postgres database for a (project, branch). The
485
+ * Postgres database NAME (`status.postgres_database`) is what
486
+ * `PGDATABASE` needs - this differs from the resource id, which can
487
+ * use a different separator (e.g. resource `databricks-postgres`
488
+ * surfaces as database `databricks_postgres`). Prefers
489
+ * `databricks_postgres` (the Lakebase default), otherwise the only
490
+ * database.
491
+ */
492
+ async function pickDatabase(ws, project, branch, log) {
493
+ const databases = await listDatabases(ws, project, branch);
494
+ if (databases.length === 0) {
495
+ throw new Error(`autopg: branch 'projects/${project}/branches/${branch}' has no databases; cannot resolve PGDATABASE`);
496
+ }
497
+ const names = databases
498
+ .map((d) => d.status?.postgres_database)
499
+ .filter((n) => Boolean(n));
500
+ const choice = names.find((n) => n === "databricks_postgres") ??
501
+ (names.length === 1 ? names[0] : undefined);
502
+ if (!choice) {
503
+ throw new Error(`autopg: multiple databases and no 'databricks_postgres'; set PGDATABASE or config.database. Candidates: ${names.join(", ")}`);
504
+ }
505
+ log.info("autopg: resolved database", { database: choice });
506
+ return choice;
507
+ }
package/index.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/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "main": "dist/index.js",
3
+ "types": "dist/index.d.ts",
4
+ "exports": {
5
+ ".": {
6
+ "source": "./index.ts",
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ }
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "index.ts",
14
+ "src"
15
+ ],
16
+ "license": "Apache-2.0",
17
+ "homepage": "https://github.com/reggie-db/dbx-tools-appkit#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/reggie-db/dbx-tools-appkit/issues"
20
+ },
21
+ "publishConfig": {
22
+ "registry": "https://registry.npmjs.org/",
23
+ "access": "public"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/reggie-db/dbx-tools-appkit.git",
28
+ "directory": "packages/autopg"
29
+ },
30
+ "name": "@dbx-tools/appkit-autopg",
31
+ "version": "0.1.0",
32
+ "module": "index.ts",
33
+ "type": "module",
34
+ "dependencies": {
35
+ "@dbx-tools/appkit-shared": "workspace:*"
36
+ },
37
+ "peerDependencies": {
38
+ "@databricks/appkit": "^0.35"
39
+ }
40
+ }