@dbx-tools/shared 0.1.18

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 ADDED
@@ -0,0 +1,234 @@
1
+ # @dbx-tools/shared
2
+
3
+ Shared utilities used by the other `@dbx-tools/appkit-*` plugins. The
4
+ package has zero AppKit runtime dependency (it lists `@databricks/appkit`
5
+ as a peer) so it's safe to consume from any plugin or app code.
6
+
7
+ Each utility module is exported as a namespace so call sites read
8
+ naturally and never collide with similarly named helpers from other
9
+ libraries:
10
+
11
+ ```ts
12
+ import {
13
+ apiUtils,
14
+ appkitUtils,
15
+ commonUtils,
16
+ httpUtils,
17
+ logUtils,
18
+ netUtils,
19
+ projectUtils,
20
+ stringUtils,
21
+ } from "@dbx-tools/shared";
22
+ ```
23
+
24
+ > `apiUtils` and `projectUtils` import Node-only modules (`@databricks/appkit`,
25
+ > `node:fs`) and are intentionally **not** re-exported from the browser
26
+ > entry. Vite / Webpack / esbuild builds that honor the `browser`
27
+ > condition will resolve `@dbx-tools/shared` to a barrel that
28
+ > omits both - import them only from server-side code.
29
+
30
+ ## `appkitUtils` - typed sibling-plugin lookup
31
+
32
+ AppKit's `this.context.getPlugins()` returns `ReadonlyMap<string, BasePlugin>`,
33
+ so every cross-plugin call ends up writing the same
34
+ `as InstanceType<ReturnType<typeof someFactory>["plugin"]>` cast.
35
+ `appkitUtils.instance` / `appkitUtils.require` absorb that boilerplate:
36
+
37
+ ```ts
38
+ import { lakebase } from "@databricks/appkit";
39
+ import { appkitUtils } from "@dbx-tools/shared";
40
+
41
+ const lake = appkitUtils.instance(this.context, lakebase);
42
+ // ^^ inferred as LakebasePlugin | undefined
43
+ const pool = lake?.exports().pool;
44
+
45
+ // Throws "<caller>: required plugin not registered: lakebase" when missing.
46
+ const pool2 = appkitUtils
47
+ .require(this.context, lakebase, "mastra")
48
+ .exports().pool;
49
+ ```
50
+
51
+ `appkitUtils.data(factory)` caches the static `{ plugin, name }` descriptor
52
+ per factory so repeated lookups don't allocate. Use it directly when you
53
+ need the registered name for a manifest dependency.
54
+
55
+ ## `httpUtils` - framework-neutral header helpers
56
+
57
+ Public surface: `forEachHeaderValue`, `parseCookies`. The header-shaped
58
+ helpers work uniformly against any of:
59
+
60
+ - Express `req` (Node-style `req.headers`)
61
+ - Web Fetch `Request` (`Headers` instance)
62
+ - Hono `Context.req` (`c.req.raw.headers`)
63
+ - `node:http` `IncomingMessage`
64
+ - Plain `Record<string, string | string[] | undefined>`
65
+
66
+ ```ts
67
+ import { httpUtils } from "@dbx-tools/shared";
68
+
69
+ app.use((req, res, next) => {
70
+ const session = httpUtils.parseCookies(req).session;
71
+
72
+ // Walk every value of a (possibly repeated) header without committing
73
+ // to a specific framework's accessor shape.
74
+ let bearer: string | undefined;
75
+ httpUtils.forEachHeaderValue(req, "authorization", (value) => {
76
+ if (value.startsWith("Bearer ")) bearer = value.slice(7);
77
+ });
78
+ });
79
+ ```
80
+
81
+ ## `netUtils` - URL parsing + free-port helpers
82
+
83
+ Public surface: `joinUrl`, `parseUrl`, plus the server-only
84
+ `getRandomPort`. The URL helpers are pure JS and ship in the
85
+ browser bundle too; `getRandomPort` binds a transient `node:net`
86
+ listener and is therefore server-only (importing `netUtils` from
87
+ the browser entry simply omits it).
88
+
89
+ ```ts
90
+ import { netUtils } from "@dbx-tools/shared";
91
+
92
+ // Tolerant URL coercion - bare hostnames, partial inputs, or
93
+ // objects with a `.url` field all round-trip through. Returns
94
+ // `null` on failure (matches WHATWG `URL.parse(...)` semantics).
95
+ const url = netUtils.parseUrl("example.com"); // https://example.com/
96
+
97
+ // Path-segment join: nullish/blank inputs are skipped, leading /
98
+ // trailing slashes are normalized, nested arrays recurse.
99
+ netUtils.joinUrl("/api/", ["v2", "items"], null); // "/api/v2/items"
100
+
101
+ // Grab a free local port (server only).
102
+ const port = await netUtils.getRandomPort();
103
+ ```
104
+
105
+ ## `apiUtils` - authenticated Databricks REST calls (server only)
106
+
107
+ Wraps `fetch` against `https://<workspace-host>/api/2.0/<path>` with the
108
+ auth header your AppKit execution context already carries, plus an
109
+ optional `CacheManager.getOrExecute` hook so per-user TTL'd reads are a
110
+ single positional arg:
111
+
112
+ ```ts
113
+ import { apiUtils } from "@dbx-tools/shared";
114
+
115
+ // Bare GET against the workspace /api/2.0 namespace - leading /api/2.0
116
+ // is auto-stripped so you can pass either form.
117
+ const data = await apiUtils.fetchApi<{ endpoints?: unknown[] }>(
118
+ "serving-endpoints",
119
+ );
120
+
121
+ // With a per-user cache (useful for "list everything" calls):
122
+ const cached = await apiUtils.fetchApi<{ endpoints?: unknown[] }>(
123
+ "serving-endpoints",
124
+ undefined,
125
+ { userKey: req.userId, options: { ttl: 300 } },
126
+ );
127
+
128
+ // POST + custom client (e.g. service-account script outside a request).
129
+ await apiUtils.fetchApi<{ id: string }>(
130
+ ["serving-endpoints", endpointName, "invocations"],
131
+ {
132
+ body: JSON.stringify({ inputs }),
133
+ headers: { "Content-Type": "application/json" },
134
+ },
135
+ undefined,
136
+ serviceClient,
137
+ );
138
+ ```
139
+
140
+ Defaults to `POST` when `init.body` is set, `GET` otherwise. The wrapper
141
+ needs an active `getExecutionContext()` to resolve the workspace client
142
+ unless a `WorkspaceClient` is passed in explicitly, so it's server-only.
143
+
144
+ ## `stringUtils` - identifier + slug helpers
145
+
146
+ `toIdentifier` / `toSlug` are deterministic, length-bounded, and always
147
+ lower-case at the type level (the `lowerCase` option literal is fixed to
148
+ `true` so an explicit `false` is a compile error):
149
+
150
+ ```ts
151
+ import { stringUtils } from "@dbx-tools/shared";
152
+
153
+ stringUtils.toIdentifier("My Cool Project!"); // "my_cool_project"
154
+ stringUtils.toSlug("My Cool Project!"); // "my-cool-project"
155
+
156
+ stringUtils.toIdentifierWithOptions({ maxLength: 12 }, "very long project name");
157
+ // "very_long_43c1" <- hash suffix when truncated
158
+ ```
159
+
160
+ ## `projectUtils` - project name + git-remote parsing
161
+
162
+ ```ts
163
+ import { projectUtils } from "@dbx-tools/shared";
164
+
165
+ // Discovers a stable name for the current project. Order:
166
+ // 1. `package.json` name (root of an npm/bun workspace if applicable)
167
+ // 2. Closest `git remote origin` repo name
168
+ // 3. Process `cwd` basename
169
+ const name = await projectUtils.name();
170
+
171
+ // Strip "owner/" + ".git" from a remote URL.
172
+ projectUtils.parseGitRemote("git@github.com:org/my-repo.git"); // "my-repo"
173
+ ```
174
+
175
+ ## `commonUtils` - memoize + hashing
176
+
177
+ ```ts
178
+ import { commonUtils } from "@dbx-tools/shared";
179
+
180
+ // Memoize by all-args; sync results cache forever, async failures bust.
181
+ const fetchUser = commonUtils.memoize(async (id: string) => loadUser(id));
182
+
183
+ // Short, deterministic hash for cache keys / slug suffixes / etc.
184
+ // Pure-JS FNV-1a in Crockford-style base-32 (digits + lowercase
185
+ // alphabet minus i/l/o/u). Browser-safe.
186
+ commonUtils.fnvHash("databricks-claude-sonnet-4-6"); // e.g. "k3p9q7"
187
+ commonUtils.fnvHashWithOptions({ length: 4 }, "user@example.com");
188
+ ```
189
+
190
+ `@memoized` is a TC39 stage-1 method decorator built on the same
191
+ `memoize` (requires `experimentalDecorators: true` in `tsconfig.json`).
192
+ `fnvHash` is intentionally **not** cryptographically secure - use it for
193
+ keys and slugs, never for tokens or signatures.
194
+
195
+ ## `logUtils` - tagged console logger
196
+
197
+ `logger(plugin)` returns a leveled `{ debug, info, warn, error }` interface
198
+ that auto-tags every line with the plugin's name:
199
+
200
+ ```ts
201
+ import { logUtils } from "@dbx-tools/shared";
202
+
203
+ class MyPlugin extends Plugin<MyConfig> {
204
+ private log = logUtils.logger(this); // tags as "[my-plugin]"
205
+ override async setup() {
206
+ this.log.info("setup", { mode: this.config.mode });
207
+ }
208
+ }
209
+ ```
210
+
211
+ The logger is intentionally console-backed (no extra deps). For richer
212
+ sinks pass your own `{ debug, info, warn, error }` object - the plugins
213
+ in this repo accept any matching shape.
214
+
215
+ ### `LOG_LEVEL` filtering
216
+
217
+ Each call checks `process.env.LOG_LEVEL` (case-insensitive, default
218
+ `info`) and drops anything below the threshold *before* string
219
+ formatting, so leaving `log.debug({...heavy details})` calls in
220
+ production code costs nothing as long as `LOG_LEVEL` isn't `debug`.
221
+
222
+ ```bash
223
+ LOG_LEVEL=debug bun dev # full verbosity
224
+ LOG_LEVEL=warn bun start # production: hide info chatter
225
+ ```
226
+
227
+ The lookup is per-call (not module-load), so test runners can flip
228
+ the threshold after the module has been imported. In browser bundles
229
+ where `process.env.LOG_LEVEL` is undefined, the default `info`
230
+ threshold applies.
231
+
232
+ ## License
233
+
234
+ Apache-2.0
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Browser-safe entry point for `@dbx-tools/shared`. Mirrors
3
+ * the server-side {@link ./index.ts} barrel except `projectUtils` is
4
+ * absent - it imports `node:fs` / `node:child_process` / `node:path` /
5
+ * `node:util` at module load, which Vite stubs for browsers and which
6
+ * blows up at the first property access from the stub.
7
+ *
8
+ * Resolution: the package's `exports` map points the `browser`
9
+ * condition at this file. Vite (and any other browser-aware bundler
10
+ * that honors `exports.<entry>.browser`) picks it up automatically;
11
+ * Node always uses `index.ts`. Don't import `./src/project.js` from
12
+ * here, even transitively - that's the entire point of the split.
13
+ *
14
+ * Other utility namespaces are re-exported as-is. `common.ts` ships a
15
+ * pure-JS FNV-1a `fnvHash` (no `node:crypto`) that `string.ts` uses
16
+ * for slug suffixes, so the whole barrel is safe in the browser;
17
+ * `http.ts` / `log.ts` already had no node-only imports.
18
+ *
19
+ * `apiUtils` and `appkitUtils` are intentionally **not** re-exported
20
+ * here. Both import from `@databricks/appkit`, whose main barrel
21
+ * re-exports server-only typegen helpers
22
+ * (`extractServingEndpoints`, the `appKit*TypesPlugin` Vite plugins)
23
+ * that transitively load `@ast-grep/napi`'s native `.node` binary.
24
+ * Letting either land in the browser bundle drags the entire appkit
25
+ * tree (including ast-grep) into the client. They live only on
26
+ * `index.ts` (the server entry).
27
+ */
28
+ export * as commonUtils from "./src/common.js";
29
+ export * as httpUtils from "./src/http.js";
30
+ export * as logUtils from "./src/log.js";
31
+ export * as netUtils from "./src/net.browser.js";
32
+ export * as stringUtils from "./src/string.js";
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Browser-safe entry point for `@dbx-tools/shared`. Mirrors
3
+ * the server-side {@link ./index.ts} barrel except `projectUtils` is
4
+ * absent - it imports `node:fs` / `node:child_process` / `node:path` /
5
+ * `node:util` at module load, which Vite stubs for browsers and which
6
+ * blows up at the first property access from the stub.
7
+ *
8
+ * Resolution: the package's `exports` map points the `browser`
9
+ * condition at this file. Vite (and any other browser-aware bundler
10
+ * that honors `exports.<entry>.browser`) picks it up automatically;
11
+ * Node always uses `index.ts`. Don't import `./src/project.js` from
12
+ * here, even transitively - that's the entire point of the split.
13
+ *
14
+ * Other utility namespaces are re-exported as-is. `common.ts` ships a
15
+ * pure-JS FNV-1a `fnvHash` (no `node:crypto`) that `string.ts` uses
16
+ * for slug suffixes, so the whole barrel is safe in the browser;
17
+ * `http.ts` / `log.ts` already had no node-only imports.
18
+ *
19
+ * `apiUtils` and `appkitUtils` are intentionally **not** re-exported
20
+ * here. Both import from `@databricks/appkit`, whose main barrel
21
+ * re-exports server-only typegen helpers
22
+ * (`extractServingEndpoints`, the `appKit*TypesPlugin` Vite plugins)
23
+ * that transitively load `@ast-grep/napi`'s native `.node` binary.
24
+ * Letting either land in the browser bundle drags the entire appkit
25
+ * tree (including ast-grep) into the client. They live only on
26
+ * `index.ts` (the server entry).
27
+ */
28
+ export * as commonUtils from "./src/common.js";
29
+ export * as httpUtils from "./src/http.js";
30
+ export * as logUtils from "./src/log.js";
31
+ export * as netUtils from "./src/net.browser.js";
32
+ export * as stringUtils from "./src/string.js";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Server-side entry point for `@dbx-tools/shared`. Re-exports
3
+ * everything from the browser-safe {@link ./index.client.ts} barrel
4
+ * and adds the server-only namespaces:
5
+ * - `projectUtils` imports `node:fs` / `node:child_process` /
6
+ * `node:path` / `node:util`.
7
+ * - `apiUtils` / `appkitUtils` both import from `@databricks/appkit`,
8
+ * whose barrel transitively pulls in the typegen helpers and the
9
+ * `@ast-grep/napi` native binary. Keeping them out of the
10
+ * browser entry stops that whole subtree from being bundled
11
+ * for the client.
12
+ *
13
+ * Resolution: this file is the `import` / `default` target in the
14
+ * package's `exports` map. Vite (and any other browser-aware
15
+ * bundler that honors `exports.<entry>.browser`) picks
16
+ * `index.client.ts` instead, so the node-only branches never ship
17
+ * to the client. Add new browser-safe helpers to `index.client.ts`
18
+ * to keep this file as the thin server-only delta.
19
+ */
20
+ export * as appkitUtils from "./src/appkit.js";
21
+ export * as apiUtils from "./src/api.js";
22
+ export * as netUtils from "./src/net.js";
23
+ export * as projectUtils from "./src/project.js";
24
+ export * from "./index.client.js";
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Server-side entry point for `@dbx-tools/shared`. Re-exports
3
+ * everything from the browser-safe {@link ./index.client.ts} barrel
4
+ * and adds the server-only namespaces:
5
+ * - `projectUtils` imports `node:fs` / `node:child_process` /
6
+ * `node:path` / `node:util`.
7
+ * - `apiUtils` / `appkitUtils` both import from `@databricks/appkit`,
8
+ * whose barrel transitively pulls in the typegen helpers and the
9
+ * `@ast-grep/napi` native binary. Keeping them out of the
10
+ * browser entry stops that whole subtree from being bundled
11
+ * for the client.
12
+ *
13
+ * Resolution: this file is the `import` / `default` target in the
14
+ * package's `exports` map. Vite (and any other browser-aware
15
+ * bundler that honors `exports.<entry>.browser`) picks
16
+ * `index.client.ts` instead, so the node-only branches never ship
17
+ * to the client. Add new browser-safe helpers to `index.client.ts`
18
+ * to keep this file as the thin server-only delta.
19
+ */
20
+ export * as appkitUtils from "./src/appkit.js";
21
+ export * as apiUtils from "./src/api.js";
22
+ export * as netUtils from "./src/net.js";
23
+ export * as projectUtils from "./src/project.js";
24
+ export * from "./index.client.js";
@@ -0,0 +1,90 @@
1
+ import type { WorkspaceClient } from "@databricks/sdk-experimental";
2
+ import { Context } from "@databricks/sdk-experimental";
3
+ import { CacheManager } from "@databricks/appkit";
4
+ type GetOrExecuteParams = Parameters<CacheManager["getOrExecute"]>;
5
+ type ApiRequestInit = RequestInit & {
6
+ cache?: {
7
+ key?: GetOrExecuteParams[0];
8
+ userKey?: GetOrExecuteParams[2];
9
+ options: GetOrExecuteParams[3];
10
+ };
11
+ workspaceClient?: WorkspaceClient;
12
+ };
13
+ /**
14
+ * Build the absolute `URL` for a Databricks workspace REST endpoint
15
+ * without issuing a request. Mirrors {@link fetchApi}'s path handling
16
+ * (single string or array of segments, leading `/api/2.0` stripped)
17
+ * so callers can construct request URLs that match what `fetchApi`
18
+ * would have used. Resolves the host from the supplied
19
+ * `WorkspaceClient` or, when omitted, from the active
20
+ * `getExecutionContext().client`.
21
+ */
22
+ export declare function apiUrl(path: string[] | string, workspaceClient?: WorkspaceClient): Promise<URL>;
23
+ /**
24
+ * Issue an authenticated request against a Databricks workspace REST
25
+ * endpoint, resolving the host from the supplied `WorkspaceClient`
26
+ * and stamping the OAuth/PAT auth header in for you. The response
27
+ * body is returned parsed as JSON.
28
+ *
29
+ * `path` may be a single string or an array of segments. A leading
30
+ * `/api/2.0` is auto-stripped so callers can pass either style
31
+ * (`"/api/2.0/serving-endpoints"` or `"/serving-endpoints"`) without
32
+ * doubling it in the final URL.
33
+ *
34
+ * `init` is an optional WHATWG `RequestInit`. Useful fields:
35
+ *
36
+ * - `body`: request payload. Strings / `Buffer` / `FormData` /
37
+ * `URLSearchParams` pass through; for JSON, stringify the object
38
+ * yourself and set `headers["Content-Type"] = "application/json"`.
39
+ * - `headers`: extra request headers, merged in **before** the auth
40
+ * header is applied so the workspace's `Authorization` always
41
+ * wins on conflict.
42
+ * - `method`: HTTP verb. If omitted, defaults to `POST` when
43
+ * `init.body` is present and `GET` otherwise.
44
+ *
45
+ * `cache` is an optional handle to `CacheManager.getOrExecute`: pass
46
+ * `{ options: { ttl: 300 } }` for a per-user, time-boxed cache; the
47
+ * `userId` from the active execution context becomes part of the
48
+ * cache key by default.
49
+ *
50
+ * `workspaceClient` is optional; when omitted the request uses the
51
+ * caller's `getExecutionContext().client` (i.e. the per-request
52
+ * OBO client). Pass an explicit client for service-account work
53
+ * outside a request.
54
+ *
55
+ * @example
56
+ * await fetchApi("/serving-endpoints");
57
+ *
58
+ * await fetchApi(["/serving-endpoints", endpointName, "invocations"], {
59
+ * body: JSON.stringify({ inputs: [...] }),
60
+ * headers: { "Content-Type": "application/json" },
61
+ * });
62
+ *
63
+ * await fetchApi("/serving-endpoints", undefined,
64
+ * { options: { ttl: 300 } }
65
+ * );
66
+ */
67
+ export declare function fetchApi<T>(target: URL | string[] | string, init?: ApiRequestInit): Promise<T>;
68
+ export type ContextLike = Context | AbortSignal;
69
+ /** Wrap a `Context` (returned as-is) or `AbortSignal` (adapted) as an SDK `Context`. */
70
+ export declare function toContext(input: ContextLike): Context;
71
+ /**
72
+ * Derive an SDK `Context` from `controller.signal`, optionally tying
73
+ * `input` into the controller so the controller becomes the single
74
+ * cancellation source for downstream SDK calls:
75
+ *
76
+ * - `AbortSignal`: aborting it propagates into `controller` (and from
77
+ * there into every SDK call you pass the returned context to).
78
+ * - `Context`: its `cancellationToken` is tied into `controller`, and
79
+ * its other fields (`logger`, `opName`, `rootClassName`,
80
+ * `rootFnName`, `opId`) are preserved in the returned `Context`.
81
+ * The returned context's `cancellationToken` is replaced with one
82
+ * backed by `controller.signal`.
83
+ *
84
+ * The tie is one-way (parent -> child): aborting `controller`
85
+ * directly does NOT cancel `input`. So a request-level cancel (your
86
+ * loop's `try/finally { controller.abort() }`) won't tear down a
87
+ * caller-supplied AbortSignal it didn't own.
88
+ */
89
+ export declare function toContext(controller: AbortController, input?: ContextLike): Context;
90
+ export {};
@@ -0,0 +1,165 @@
1
+ import { Context } from "@databricks/sdk-experimental";
2
+ import { CacheManager, getExecutionContext } from "@databricks/appkit";
3
+ // Direct imports (not via the barrel). The package's NodeNext
4
+ // module resolution wants explicit `.js` extensions on relative
5
+ // imports, and reaching for `commonUtils` / `netUtils` through
6
+ // `../index.client` confused the `noEmit` typecheck with a
7
+ // missing-extension error. Direct sibling imports stay typed and
8
+ // don't risk a future cycle.
9
+ import { fnvHash, tieAbortSignal } from "./common.js";
10
+ import { joinUrl, parseUrl } from "./net.browser.js";
11
+ // ────────────────────────────────────────────────────────────────
12
+ // Constants
13
+ // ────────────────────────────────────────────────────────────────
14
+ const API_PREFIX = "/api/2.0";
15
+ /**
16
+ * Build the absolute `URL` for a Databricks workspace REST endpoint
17
+ * without issuing a request. Mirrors {@link fetchApi}'s path handling
18
+ * (single string or array of segments, leading `/api/2.0` stripped)
19
+ * so callers can construct request URLs that match what `fetchApi`
20
+ * would have used. Resolves the host from the supplied
21
+ * `WorkspaceClient` or, when omitted, from the active
22
+ * `getExecutionContext().client`.
23
+ */
24
+ export async function apiUrl(path, workspaceClient) {
25
+ let joinedPath = joinUrl(path);
26
+ if (joinedPath === API_PREFIX || joinedPath.startsWith(API_PREFIX + "/")) {
27
+ joinedPath = joinedPath.slice(API_PREFIX.length);
28
+ }
29
+ if (!joinedPath) {
30
+ throw new Error(`Invalid path: ${path}`);
31
+ }
32
+ const client = workspaceClient ?? getExecutionContext().client;
33
+ const config = client.config;
34
+ const host = await config.getHost();
35
+ const url = parseUrl(host, API_PREFIX, joinedPath);
36
+ return url;
37
+ }
38
+ /**
39
+ * Issue an authenticated request against a Databricks workspace REST
40
+ * endpoint, resolving the host from the supplied `WorkspaceClient`
41
+ * and stamping the OAuth/PAT auth header in for you. The response
42
+ * body is returned parsed as JSON.
43
+ *
44
+ * `path` may be a single string or an array of segments. A leading
45
+ * `/api/2.0` is auto-stripped so callers can pass either style
46
+ * (`"/api/2.0/serving-endpoints"` or `"/serving-endpoints"`) without
47
+ * doubling it in the final URL.
48
+ *
49
+ * `init` is an optional WHATWG `RequestInit`. Useful fields:
50
+ *
51
+ * - `body`: request payload. Strings / `Buffer` / `FormData` /
52
+ * `URLSearchParams` pass through; for JSON, stringify the object
53
+ * yourself and set `headers["Content-Type"] = "application/json"`.
54
+ * - `headers`: extra request headers, merged in **before** the auth
55
+ * header is applied so the workspace's `Authorization` always
56
+ * wins on conflict.
57
+ * - `method`: HTTP verb. If omitted, defaults to `POST` when
58
+ * `init.body` is present and `GET` otherwise.
59
+ *
60
+ * `cache` is an optional handle to `CacheManager.getOrExecute`: pass
61
+ * `{ options: { ttl: 300 } }` for a per-user, time-boxed cache; the
62
+ * `userId` from the active execution context becomes part of the
63
+ * cache key by default.
64
+ *
65
+ * `workspaceClient` is optional; when omitted the request uses the
66
+ * caller's `getExecutionContext().client` (i.e. the per-request
67
+ * OBO client). Pass an explicit client for service-account work
68
+ * outside a request.
69
+ *
70
+ * @example
71
+ * await fetchApi("/serving-endpoints");
72
+ *
73
+ * await fetchApi(["/serving-endpoints", endpointName, "invocations"], {
74
+ * body: JSON.stringify({ inputs: [...] }),
75
+ * headers: { "Content-Type": "application/json" },
76
+ * });
77
+ *
78
+ * await fetchApi("/serving-endpoints", undefined,
79
+ * { options: { ttl: 300 } }
80
+ * );
81
+ */
82
+ export async function fetchApi(target, init) {
83
+ const client = init?.workspaceClient ?? getExecutionContext().client;
84
+ const config = client.config;
85
+ const url = target instanceof URL ? target : await apiUrl(target, client);
86
+ if (init?.cache) {
87
+ const { cache, ...executeInit } = init;
88
+ const executionContext = getExecutionContext();
89
+ const userId = "userId" in executionContext
90
+ ? executionContext.userId
91
+ : executionContext.serviceUserId;
92
+ const cacheInstance = await CacheManager.getInstance();
93
+ return cacheInstance.getOrExecute(cache.key ?? ["fetchApi", userId], async () => {
94
+ return fetchApi(url, { ...executeInit, workspaceClient: client });
95
+ }, cache.userKey ?? fnvHash(url.toString()), cache.options);
96
+ }
97
+ const headers = new Headers(init?.headers);
98
+ await config.authenticate(headers);
99
+ const method = init?.method?.toUpperCase() ?? (init?.body ? "POST" : "GET");
100
+ const response = await fetch(url.toString(), {
101
+ ...init,
102
+ method,
103
+ headers,
104
+ });
105
+ return response.json();
106
+ }
107
+ export function toContext(source, input) {
108
+ if (!(source instanceof AbortController)) {
109
+ if (source instanceof Context)
110
+ return source;
111
+ return new Context({ cancellationToken: signalToCancellationToken(source) });
112
+ }
113
+ if (input instanceof AbortSignal) {
114
+ tieAbortSignal(source, input);
115
+ }
116
+ else if (input instanceof Context) {
117
+ const token = input.cancellationToken;
118
+ if (token)
119
+ tieCancellationToken(source, token);
120
+ const merged = input.copy();
121
+ merged.setItems({ cancellationToken: signalToCancellationToken(source.signal) });
122
+ return merged;
123
+ }
124
+ return new Context({ cancellationToken: signalToCancellationToken(source.signal) });
125
+ }
126
+ /**
127
+ * Adapt a WHATWG `AbortSignal` to the Databricks SDK's
128
+ * `CancellationToken` interface. The SDK's `api-client.ts`
129
+ * internally creates an `AbortController` and wires
130
+ * `cancellationToken.onCancellationRequested` to it, so this
131
+ * adapter is the one-line bridge from "platform-standard
132
+ * cancellation" to "the SDK aborts the fetch on your behalf".
133
+ *
134
+ * Kept private for now: the genie package is the only consumer in
135
+ * the workspace. Lift to `@dbx-tools/shared` (`apiUtils`) the
136
+ * moment a second package needs SDK-call cancellation.
137
+ */
138
+ function signalToCancellationToken(signal) {
139
+ return {
140
+ get isCancellationRequested() {
141
+ return signal.aborted;
142
+ },
143
+ onCancellationRequested(cb) {
144
+ if (signal.aborted) {
145
+ cb(signal.reason);
146
+ return;
147
+ }
148
+ signal.addEventListener("abort", () => cb(signal.reason), { once: true });
149
+ },
150
+ };
151
+ }
152
+ /**
153
+ * Tie the SDK's `CancellationToken` interface back into an
154
+ * `AbortController`. Mirrors {@link tieAbortSignal} but for the
155
+ * SDK's cancellation shape, used when a caller hands us a
156
+ * pre-built `Context` whose token we want to fold into our own
157
+ * controller.
158
+ */
159
+ function tieCancellationToken(controller, token) {
160
+ if (token.isCancellationRequested) {
161
+ controller.abort();
162
+ return;
163
+ }
164
+ token.onCancellationRequested((reason) => controller.abort(reason));
165
+ }
@@ -0,0 +1,59 @@
1
+ import type { NameLike } from "./common.js";
2
+ export interface PluginContextLike {
3
+ getPlugins(): ReadonlyMap<string, unknown>;
4
+ }
5
+ type PluginData = {
6
+ plugin: abstract new (...args: never[]) => unknown;
7
+ name: string;
8
+ };
9
+ type PluginDataFactory = (...args: never[]) => PluginData;
10
+ type PluginInstanceOf<F extends PluginDataFactory> = InstanceType<ReturnType<F>["plugin"]>;
11
+ /**
12
+ * Returns the static `{ plugin, name }` descriptor for an AppKit plugin
13
+ * factory, caching per factory so repeated lookups do not allocate.
14
+ */
15
+ export declare function data<F extends PluginDataFactory, D extends ReturnType<F>>(factory: F): D;
16
+ /**
17
+ * Look up a sibling plugin instance from the AppKit plugin context,
18
+ * keyed off the factory's registered name and typed via its plugin
19
+ * class.
20
+ *
21
+ * Returns `undefined` when the context is missing or the plugin is not
22
+ * registered. For required siblings prefer {@link require}.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { lakebase } from "@databricks/appkit";
27
+ * import { appkitUtils } from "@dbx-tools/shared";
28
+ *
29
+ * const lake = appkitUtils.instance(this.context, lakebase);
30
+ * // ^^ inferred as LakebasePlugin | undefined
31
+ * lake?.exports().pool;
32
+ * ```
33
+ */
34
+ export declare function instance<F extends PluginDataFactory>(ctx: PluginContextLike | undefined, factory: F): PluginInstanceOf<F> | undefined;
35
+ /**
36
+ * Like {@link instance} but throws when the plugin is not registered.
37
+ * Use for siblings whose absence is a wiring bug rather than a runtime
38
+ * condition (e.g. requiring `lakebase` when the caller has `storage` /
39
+ * `memory` enabled).
40
+ *
41
+ * `caller` is prepended to the error message so cross-plugin failures
42
+ * are easy to attribute in logs.
43
+ *
44
+ * Always accessed through the namespace as `appkitUtils.require(...)`;
45
+ * the bare identifier is legal here because this package is pure ESM.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { lakebase } from "@databricks/appkit";
50
+ * import { appkitUtils } from "@dbx-tools/shared";
51
+ *
52
+ * const pool = appkitUtils.require(this.context, lakebase, "mastra")
53
+ * .exports().pool;
54
+ * ```
55
+ */
56
+ export declare function require<F extends PluginDataFactory>(ctx: PluginContextLike | undefined, factory: F, caller?: NameLike | string): PluginInstanceOf<F>;
57
+ export declare function isInitialized(): boolean;
58
+ export declare function ensureInitialized(): Promise<void>;
59
+ export {};