@dbx-tools/appkit 0.1.9
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 +201 -0
- package/index.ts +18 -0
- package/package.json +55 -0
- package/src/appkit.ts +55 -0
- package/src/config.ts +445 -0
- package/src/create-app.ts +95 -0
- package/src/databricks.ts +121 -0
- package/src/lakebase-resolver.ts +705 -0
- package/src/pgaddress.ts +194 -0
- package/src/plugin.ts +129 -0
- package/src/provision.ts +111 -0
- package/tsconfig.json +41 -0
package/src/pgaddress.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
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
|
+
* Lakebase 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`, and `databaseResourceId` (the UC
|
|
23
|
+
* resource leaf, not `PGDATABASE`; the resolver looks up the real
|
|
24
|
+
* Postgres name 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
|
+
|
|
44
|
+
/** Postgres TLS mode passed through to `pg`. */
|
|
45
|
+
export type SslMode = "require" | "disable" | "prefer";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Optional Lakebase Postgres connection fields shared by parsed addresses,
|
|
49
|
+
* resolver/env inputs, and resolved connections.
|
|
50
|
+
*/
|
|
51
|
+
export interface LakebaseConnectionInputs {
|
|
52
|
+
/** Lakebase project id. */
|
|
53
|
+
project?: string;
|
|
54
|
+
/** Branch id within the project. */
|
|
55
|
+
branch?: string;
|
|
56
|
+
/** Canonical endpoint resource path (`projects/.../endpoints/...`). */
|
|
57
|
+
endpoint?: string;
|
|
58
|
+
/** Postgres database name (`PGDATABASE`). */
|
|
59
|
+
database?: string;
|
|
60
|
+
/** Postgres hostname (`PGHOST`). */
|
|
61
|
+
host?: string;
|
|
62
|
+
/** Postgres port (`PGPORT`). */
|
|
63
|
+
port?: number;
|
|
64
|
+
/** Postgres TLS mode (`PGSSLMODE`). */
|
|
65
|
+
sslMode?: SslMode;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Pieces recovered from parsing a single address or resource-path input. */
|
|
69
|
+
export interface ParsedAddress extends LakebaseConnectionInputs {
|
|
70
|
+
/** Endpoint leaf id (last segment of an endpoint resource path). */
|
|
71
|
+
endpointId?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Database resource id leaf from a `.../databases/{id}` path. Not the
|
|
74
|
+
* Postgres database name.
|
|
75
|
+
*/
|
|
76
|
+
databaseResourceId?: string;
|
|
77
|
+
/** Postgres user (URI-decoded if encoded). */
|
|
78
|
+
user?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const URL_SCHEME_RE = /^(postgres|postgresql):\/\//i;
|
|
82
|
+
const PROJECT_ID_RE = /^[a-z][a-z0-9-]{0,61}[a-z0-9]$|^[a-z]$/;
|
|
83
|
+
const HOSTNAME_HINT_RE = /^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)+$/i;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse a Lakebase connection input into whatever pieces it carries.
|
|
87
|
+
* See module docstring for the supported formats. Returns `{}` for
|
|
88
|
+
* `undefined`, empty strings, and unrecognized inputs.
|
|
89
|
+
*/
|
|
90
|
+
export function parseAddress(input: string | undefined | null): ParsedAddress {
|
|
91
|
+
if (!input) return {};
|
|
92
|
+
const s = input.trim();
|
|
93
|
+
if (!s) return {};
|
|
94
|
+
|
|
95
|
+
if (URL_SCHEME_RE.test(s)) return parseUri(s);
|
|
96
|
+
if (s.startsWith("projects/")) return parseResourcePathSegments(s);
|
|
97
|
+
// Resource ids never contain dots; a dotted input must be a hostname.
|
|
98
|
+
if (HOSTNAME_HINT_RE.test(s) && s.includes(".")) return { host: s };
|
|
99
|
+
if (PROJECT_ID_RE.test(s)) return { project: s };
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Parse a Lakebase `projects/...` resource path. Returns `{}` when the
|
|
105
|
+
* input is not a resource path (so bare branch ids are not mistaken for
|
|
106
|
+
* project ids).
|
|
107
|
+
*/
|
|
108
|
+
export function parseResourcePath(input: string | undefined | null): ParsedAddress {
|
|
109
|
+
if (!input) return {};
|
|
110
|
+
const s = input.trim();
|
|
111
|
+
if (!s.startsWith("projects/")) return {};
|
|
112
|
+
return parseResourcePathSegments(s);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseUri(s: string): ParsedAddress {
|
|
116
|
+
let url: URL;
|
|
117
|
+
try {
|
|
118
|
+
url = new URL(s);
|
|
119
|
+
} catch {
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
const result: ParsedAddress = {};
|
|
123
|
+
if (url.hostname) result.host = url.hostname;
|
|
124
|
+
if (url.port) {
|
|
125
|
+
const port = Number.parseInt(url.port, 10);
|
|
126
|
+
if (!Number.isNaN(port)) result.port = port;
|
|
127
|
+
}
|
|
128
|
+
if (url.username) {
|
|
129
|
+
try {
|
|
130
|
+
result.user = decodeURIComponent(url.username);
|
|
131
|
+
} catch {
|
|
132
|
+
result.user = url.username;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const db = url.pathname.replace(/^\//, "");
|
|
136
|
+
if (db) result.database = decodeURIComponent(db);
|
|
137
|
+
const sslmodeRaw = url.searchParams.get("sslmode") ?? url.searchParams.get("sslMode");
|
|
138
|
+
const sslmode = sslmodeRaw?.toLowerCase();
|
|
139
|
+
if (sslmode === "require" || sslmode === "disable" || sslmode === "prefer") {
|
|
140
|
+
result.sslMode = sslmode;
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseResourcePathSegments(s: string): ParsedAddress {
|
|
146
|
+
const parts = s.split("/");
|
|
147
|
+
if (parts[0] !== "projects" || parts.length < 2) {
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const project = parts[1];
|
|
152
|
+
if (!project) {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (parts.length === 2) {
|
|
157
|
+
return { project };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (parts.length === 4 && parts[2] === "branches" && parts[3]) {
|
|
161
|
+
return { project, branch: parts[3] };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (
|
|
165
|
+
parts.length === 6 &&
|
|
166
|
+
parts[2] === "branches" &&
|
|
167
|
+
parts[4] === "endpoints" &&
|
|
168
|
+
parts[3] &&
|
|
169
|
+
parts[5]
|
|
170
|
+
) {
|
|
171
|
+
return {
|
|
172
|
+
project,
|
|
173
|
+
branch: parts[3],
|
|
174
|
+
endpointId: parts[5],
|
|
175
|
+
endpoint: s,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (
|
|
180
|
+
parts.length === 6 &&
|
|
181
|
+
parts[2] === "branches" &&
|
|
182
|
+
parts[4] === "databases" &&
|
|
183
|
+
parts[3] &&
|
|
184
|
+
parts[5]
|
|
185
|
+
) {
|
|
186
|
+
return {
|
|
187
|
+
project,
|
|
188
|
+
branch: parts[3],
|
|
189
|
+
databaseResourceId: parts[5],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {};
|
|
194
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppKit plugin lookup: typed access to sibling plugins registered on the
|
|
3
|
+
* AppKit plugin context (`this.context` on any class that extends `Plugin`).
|
|
4
|
+
*
|
|
5
|
+
* Why these live here instead of in `@databricks/appkit`: AppKit exposes
|
|
6
|
+
* `this.context.getPlugins()`, which returns `ReadonlyMap<string, BasePlugin>`,
|
|
7
|
+
* but provides no typed lookup helper. Every caller ends up writing the same
|
|
8
|
+
* `as InstanceType<ReturnType<typeof someFactory>["plugin"]>` cast. These
|
|
9
|
+
* wrappers absorb that boilerplate.
|
|
10
|
+
*
|
|
11
|
+
* API shape: pass the plugin's factory (`lakebase`, `serving`, `genie`, or any
|
|
12
|
+
* `toPlugin(...)` result) directly. TypeScript infers both the instance type
|
|
13
|
+
* (so `.exports()` resolves) and the registered name (so the runtime lookup
|
|
14
|
+
* works) from that single value. No `<T>` annotation or string literal needed
|
|
15
|
+
* at the call site.
|
|
16
|
+
*
|
|
17
|
+
* Generic AppKit runtime (execution context, `WorkspaceClientLike`) lives in
|
|
18
|
+
* `./appkit`; this module is plugin-lookup only.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { type NameLike } from "@dbx-tools/shared-core";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Minimal structural shape of `this.context`. We mirror only the method we
|
|
25
|
+
* touch instead of depending on AppKit's `PluginContext` type, which is not
|
|
26
|
+
* part of the package's `exports` map and therefore cannot be imported. Any
|
|
27
|
+
* compatible object (real `PluginContext`, mocks, tests) satisfies this shape.
|
|
28
|
+
*/
|
|
29
|
+
export interface PluginContextLike {
|
|
30
|
+
getPlugins(): ReadonlyMap<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type PluginData = {
|
|
34
|
+
plugin: abstract new (...args: never[]) => unknown;
|
|
35
|
+
name: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Structural shape of an AppKit plugin factory (the result of
|
|
40
|
+
* `toPlugin(SomePluginClass)`). Calling it returns a `PluginData` tuple whose
|
|
41
|
+
* `plugin` field is the *class constructor* and whose `name` field carries the
|
|
42
|
+
* registered plugin name as a literal string.
|
|
43
|
+
*
|
|
44
|
+
* Defined structurally so we don't pull `@databricks/appkit` into this as a
|
|
45
|
+
* type dependency for the bound. Any function returning the same shape (e.g.
|
|
46
|
+
* `lakebase`, `serving`, `genie`, or a user-defined `toPlugin(MyPlugin)`)
|
|
47
|
+
* satisfies it.
|
|
48
|
+
*/
|
|
49
|
+
type PluginDataFactory = (...args: never[]) => PluginData;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Maps a plugin factory back to the *instance* type of its plugin class.
|
|
53
|
+
* Mirrors the inline pattern users would otherwise write:
|
|
54
|
+
* `InstanceType<ReturnType<typeof factory>["plugin"]>`.
|
|
55
|
+
*/
|
|
56
|
+
type PluginInstanceOf<F extends PluginDataFactory> = InstanceType<ReturnType<F>["plugin"]>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Registry name returned by `factory().name`, keyed by the factory function.
|
|
60
|
+
* Typical AppKit factories return stable metadata; caching avoids invoking
|
|
61
|
+
* `factory()` on every sibling lookup (which would allocate a fresh descriptor
|
|
62
|
+
* tuple each time).
|
|
63
|
+
*/
|
|
64
|
+
const dataCache = new WeakMap<PluginDataFactory, PluginData>();
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns the static `{ plugin, name }` descriptor for an AppKit plugin
|
|
68
|
+
* factory, caching per factory so repeated lookups do not allocate.
|
|
69
|
+
*/
|
|
70
|
+
export function data<F extends PluginDataFactory, D extends ReturnType<F>>(factory: F): D {
|
|
71
|
+
const cached = dataCache.get(factory);
|
|
72
|
+
if (cached !== undefined) {
|
|
73
|
+
return cached as D;
|
|
74
|
+
}
|
|
75
|
+
const result = factory();
|
|
76
|
+
dataCache.set(factory, result);
|
|
77
|
+
return result as D;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Look up a sibling plugin instance from the AppKit plugin context, keyed off
|
|
82
|
+
* the factory's registered name and typed via its plugin class.
|
|
83
|
+
*
|
|
84
|
+
* Returns `undefined` when the context is missing or the plugin is not
|
|
85
|
+
* registered. For required siblings prefer {@link require}.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* import { lakebase } from "@databricks/appkit";
|
|
89
|
+
* import { plugin } from "@dbx-tools/appkit";
|
|
90
|
+
*
|
|
91
|
+
* const lake = plugin.instance(this.context, lakebase);
|
|
92
|
+
* // ^^ inferred as LakebasePlugin | undefined
|
|
93
|
+
* lake?.exports().pool;
|
|
94
|
+
*/
|
|
95
|
+
export function instance<F extends PluginDataFactory>(
|
|
96
|
+
ctx: PluginContextLike | undefined,
|
|
97
|
+
factory: F,
|
|
98
|
+
): PluginInstanceOf<F> | undefined {
|
|
99
|
+
if (!ctx) return undefined;
|
|
100
|
+
const name = data(factory).name;
|
|
101
|
+
return ctx.getPlugins().get(name) as PluginInstanceOf<F> | undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Like {@link instance} but throws when the plugin is not registered. Use for
|
|
106
|
+
* siblings whose absence is a wiring bug rather than a runtime condition (e.g.
|
|
107
|
+
* requiring `lakebase` when the caller has `storage` / `memory` enabled).
|
|
108
|
+
*
|
|
109
|
+
* `caller` is prepended to the error message so cross-plugin failures are easy
|
|
110
|
+
* to attribute in logs.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* import { lakebase } from "@databricks/appkit";
|
|
114
|
+
* import { plugin } from "@dbx-tools/appkit";
|
|
115
|
+
*
|
|
116
|
+
* const pool = plugin.require(this.context, lakebase, "mastra").exports().pool;
|
|
117
|
+
*/
|
|
118
|
+
export function require<F extends PluginDataFactory>(
|
|
119
|
+
ctx: PluginContextLike | undefined,
|
|
120
|
+
factory: F,
|
|
121
|
+
caller?: NameLike | string,
|
|
122
|
+
): PluginInstanceOf<F> {
|
|
123
|
+
const found = instance(ctx, factory);
|
|
124
|
+
if (found) return found;
|
|
125
|
+
const prefix =
|
|
126
|
+
typeof caller === "string" ? `${caller}: ` : caller?.name ? `${caller.name}: ` : "";
|
|
127
|
+
const registeredName = data(factory).name;
|
|
128
|
+
throw new Error(`${prefix}required plugin not registered: ${registeredName}`);
|
|
129
|
+
}
|
package/src/provision.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lakebase cache-schema grant fix-up.
|
|
3
|
+
*
|
|
4
|
+
* AppKit's persistent cache (`CacheManager` -> `PersistentStorage`) uses a
|
|
5
|
+
* schema `appkit` and a table `appkit.appkit_cache_entries`. When the connecting
|
|
6
|
+
* Databricks identity lacks privileges on that schema, the cache migration
|
|
7
|
+
* throws, and because AppKit is typically run with `cache.strictPersistence:
|
|
8
|
+
* true`, the cache is silently switched to a disabled in-memory stub - so every
|
|
9
|
+
* persistent read (chart long-poll, history, etc.) misses and 404s.
|
|
10
|
+
*
|
|
11
|
+
* This grants the connecting role full rights on the cache schema, but only when
|
|
12
|
+
* the schema ALREADY EXISTS - it never creates the schema itself. Run from a
|
|
13
|
+
* LOCAL identity that owns the schema (or holds grant option on it); it is
|
|
14
|
+
* skipped inside a Databricks App, where the app SP cannot grant and its
|
|
15
|
+
* Postgres role does not exist until its first connection.
|
|
16
|
+
*
|
|
17
|
+
* Must run AFTER {@link applyLakebaseToEnv} has written the resolved connection
|
|
18
|
+
* to `process.env` (so `createLakebasePool` picks up host / database / endpoint)
|
|
19
|
+
* and BEFORE `createApp` initializes the cache.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { error, type log } from "@dbx-tools/shared-core";
|
|
23
|
+
import { createLakebasePool, getWorkspaceClient } from "@databricks/appkit";
|
|
24
|
+
|
|
25
|
+
import { isAppEnv } from "./databricks";
|
|
26
|
+
|
|
27
|
+
/** AppKit persistent-cache schema (see AppKit's `PersistentStorage`). */
|
|
28
|
+
const CACHE_SCHEMA = "appkit";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Quote a Postgres identifier: wrap in double quotes and double any embedded
|
|
32
|
+
* quote. Needed because Lakebase role names are often emails (`user@host`) that
|
|
33
|
+
* must be quoted to be valid identifiers.
|
|
34
|
+
*/
|
|
35
|
+
function quoteIdent(ident: string): string {
|
|
36
|
+
return `"${ident.replace(/"/g, '""')}"`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Idempotent grants that make the (already-existing) AppKit cache schema fully
|
|
41
|
+
* usable by `role`. The `ALTER DEFAULT PRIVILEGES` lines cover the cache table
|
|
42
|
+
* whenever the schema owner creates it later.
|
|
43
|
+
*/
|
|
44
|
+
function cacheGrantStatements(role: string): readonly string[] {
|
|
45
|
+
const schema = quoteIdent(CACHE_SCHEMA);
|
|
46
|
+
const target = quoteIdent(role);
|
|
47
|
+
return [
|
|
48
|
+
`GRANT USAGE, CREATE ON SCHEMA ${schema} TO ${target}`,
|
|
49
|
+
`GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA ${schema} TO ${target}`,
|
|
50
|
+
`GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA ${schema} TO ${target}`,
|
|
51
|
+
`ALTER DEFAULT PRIVILEGES IN SCHEMA ${schema} GRANT ALL ON TABLES TO ${target}`,
|
|
52
|
+
`ALTER DEFAULT PRIVILEGES IN SCHEMA ${schema} GRANT ALL ON SEQUENCES TO ${target}`,
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Grant `role` rights on the AppKit cache schema, but only when that schema
|
|
58
|
+
* already exists.
|
|
59
|
+
*
|
|
60
|
+
* No-ops inside a Databricks App env, when `role` is undefined, or when the
|
|
61
|
+
* schema is absent. Best-effort otherwise: any failure (e.g. the local identity
|
|
62
|
+
* doesn't own the schema) is logged and swallowed so it never blocks startup -
|
|
63
|
+
* a disabled cache is degraded, not fatal.
|
|
64
|
+
*
|
|
65
|
+
* @param role - Postgres role to grant to and connect as (the resolved
|
|
66
|
+
* workspace-client identity); skips when undefined.
|
|
67
|
+
*/
|
|
68
|
+
export async function provisionCacheSchema(
|
|
69
|
+
logger: log.Logger,
|
|
70
|
+
role: string | undefined,
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
if (isAppEnv()) {
|
|
73
|
+
logger.debug("autopg: skip cache provisioning (inside a Databricks App)");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!role) {
|
|
77
|
+
logger.warn("autopg: skip cache provisioning (could not resolve connecting role)");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Pass `user` explicitly: autopg resolves the connection target
|
|
81
|
+
// (host/database/endpoint) but not the identity, so `createLakebasePool`'s
|
|
82
|
+
// synchronous username lookup would otherwise throw. `getWorkspaceClient({})`
|
|
83
|
+
// returns a fresh default-auth client - literally `new WorkspaceClient({})`,
|
|
84
|
+
// but built against the SDK version AppKit's `createLakebasePool` expects
|
|
85
|
+
// (importing the SDK class directly here pulls a newer, type-incompatible copy).
|
|
86
|
+
const pool = createLakebasePool({
|
|
87
|
+
user: role,
|
|
88
|
+
workspaceClient: getWorkspaceClient({}),
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
const found = await pool.query(
|
|
92
|
+
"SELECT 1 FROM information_schema.schemata WHERE schema_name = $1",
|
|
93
|
+
[CACHE_SCHEMA],
|
|
94
|
+
);
|
|
95
|
+
if (found.rowCount === 0) {
|
|
96
|
+
logger.debug("autopg: skip cache provisioning (schema absent)", { schema: CACHE_SCHEMA });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const sql of cacheGrantStatements(role)) {
|
|
100
|
+
await pool.query(sql);
|
|
101
|
+
}
|
|
102
|
+
logger.info("autopg: granted cache schema", { schema: CACHE_SCHEMA, role });
|
|
103
|
+
} catch (err) {
|
|
104
|
+
logger.warn("autopg: cache provisioning failed (continuing)", {
|
|
105
|
+
schema: CACHE_SCHEMA,
|
|
106
|
+
error: error.errorMessage(err),
|
|
107
|
+
});
|
|
108
|
+
} finally {
|
|
109
|
+
await pool.end().catch(() => {});
|
|
110
|
+
}
|
|
111
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ~~ Generated by projen. To modify, edit .projenrc.js and run "pnpm exec projen".
|
|
2
|
+
{
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"outDir": "lib",
|
|
6
|
+
"alwaysStrict": true,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"experimentalDecorators": true,
|
|
10
|
+
"inlineSourceMap": true,
|
|
11
|
+
"inlineSources": true,
|
|
12
|
+
"lib": [
|
|
13
|
+
"ES2022"
|
|
14
|
+
],
|
|
15
|
+
"module": "ESNext",
|
|
16
|
+
"noEmitOnError": false,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"noImplicitAny": true,
|
|
19
|
+
"noImplicitReturns": true,
|
|
20
|
+
"noImplicitThis": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"resolveJsonModule": true,
|
|
24
|
+
"strict": true,
|
|
25
|
+
"strictNullChecks": true,
|
|
26
|
+
"strictPropertyInitialization": true,
|
|
27
|
+
"stripInternal": true,
|
|
28
|
+
"target": "ES2022",
|
|
29
|
+
"types": [
|
|
30
|
+
"node"
|
|
31
|
+
],
|
|
32
|
+
"moduleResolution": "bundler",
|
|
33
|
+
"skipLibCheck": true
|
|
34
|
+
},
|
|
35
|
+
"include": [
|
|
36
|
+
"src/**/*.ts"
|
|
37
|
+
],
|
|
38
|
+
"exclude": [
|
|
39
|
+
"node_modules"
|
|
40
|
+
]
|
|
41
|
+
}
|