@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 +234 -0
- package/dist/index.client.d.ts +32 -0
- package/dist/index.client.js +32 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +24 -0
- package/dist/src/api.d.ts +90 -0
- package/dist/src/api.js +165 -0
- package/dist/src/appkit.d.ts +59 -0
- package/dist/src/appkit.js +109 -0
- package/dist/src/common.d.ts +185 -0
- package/dist/src/common.js +277 -0
- package/dist/src/http.d.ts +77 -0
- package/dist/src/http.js +166 -0
- package/dist/src/log.d.ts +47 -0
- package/dist/src/log.js +80 -0
- package/dist/src/net.browser.d.ts +98 -0
- package/dist/src/net.browser.js +146 -0
- package/dist/src/net.d.ts +14 -0
- package/dist/src/net.js +29 -0
- package/dist/src/project.d.ts +33 -0
- package/dist/src/project.js +215 -0
- package/dist/src/string.d.ts +105 -0
- package/dist/src/string.js +220 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/index.client.ts +32 -0
- package/index.ts +26 -0
- package/package.json +54 -0
- package/src/api.ts +222 -0
- package/src/appkit.ts +161 -0
- package/src/common.ts +422 -0
- package/src/http.ts +203 -0
- package/src/log.ts +116 -0
- package/src/net.browser.ts +174 -0
- package/src/net.ts +32 -0
- package/src/project.ts +264 -0
- package/src/string.ts +276 -0
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";
|
package/dist/index.d.ts
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";
|
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 {};
|
package/dist/src/api.js
ADDED
|
@@ -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 {};
|