@dbx-tools/appkit-shared
v0.1.13
Published
Shared utilities used by the other `@dbx-tools/appkit-*` plugins. The package has zero AppKit runtime dependency (it lists `@databricks/appkit` as a peer) so it's safe to consume from any plugin or app code.
Readme
@dbx-tools/appkit-shared
Shared utilities used by the other @dbx-tools/appkit-* plugins. The
package has zero AppKit runtime dependency (it lists @databricks/appkit
as a peer) so it's safe to consume from any plugin or app code.
Each utility module is exported as a namespace so call sites read naturally and never collide with similarly named helpers from other libraries:
import {
apiUtils,
commonUtils,
httpUtils,
logUtils,
pluginUtils,
projectUtils,
stringUtils,
} from "@dbx-tools/appkit-shared";
apiUtilsandprojectUtilsimport Node-only modules (@databricks/appkit,node:fs) and are intentionally not re-exported from the browser entry. Vite / Webpack / esbuild builds that honor thebrowsercondition will resolve@dbx-tools/appkit-sharedto a barrel that omits both - import them only from server-side code.
pluginUtils - typed sibling-plugin lookup
AppKit's this.context.getPlugins() returns ReadonlyMap<string, BasePlugin>,
so every cross-plugin call ends up writing the same
as InstanceType<ReturnType<typeof someFactory>["plugin"]> cast.
pluginUtils.instance / pluginUtils.require absorb that boilerplate:
import { lakebase } from "@databricks/appkit";
import { pluginUtils } from "@dbx-tools/appkit-shared";
const lake = pluginUtils.instance(this.context, lakebase);
// ^^ inferred as LakebasePlugin | undefined
const pool = lake?.exports().pool;
// Throws "<caller>: required plugin not registered: lakebase" when missing.
const pool2 = pluginUtils
.require(this.context, lakebase, "mastra")
.exports().pool;pluginUtils.data(factory) caches the static { plugin, name } descriptor
per factory so repeated lookups don't allocate. Use it directly when you
need the registered name for a manifest dependency.
httpUtils - framework-neutral request helpers
Public surface: joinUrlSegments, toURL, forEachHeaderValue,
parseCookies. The header-shaped helpers work uniformly against any of:
- Express
req(Node-stylereq.headers) - Web Fetch
Request(Headersinstance) - Hono
Context.req(c.req.raw.headers) node:httpIncomingMessage- Plain
Record<string, string | string[] | undefined>
import { httpUtils } from "@dbx-tools/appkit-shared";
app.use((req, res, next) => {
const session = httpUtils.parseCookies(req).session;
// Walk every value of a (possibly repeated) header without committing
// to a specific framework's accessor shape.
let bearer: string | undefined;
httpUtils.forEachHeaderValue(req, "authorization", (value) => {
if (value.startsWith("Bearer ")) bearer = value.slice(7);
});
});
// Tolerant URL coercion - bare hostnames, partial inputs, or
// objects with a `.url` field all round-trip through:
const url = httpUtils.toURL("example.com"); // https://example.com/
// Path-segment join: nullish/blank inputs are skipped, leading /
// trailing slashes are normalized, nested arrays recurse.
httpUtils.joinUrlSegments("/api/", ["v2", "items"], null); // "/api/v2/items"apiUtils - authenticated Databricks REST calls (server only)
Wraps fetch against https://<workspace-host>/api/2.0/<path> with the
auth header your AppKit execution context already carries, plus an
optional CacheManager.getOrExecute hook so per-user TTL'd reads are a
single positional arg:
import { apiUtils } from "@dbx-tools/appkit-shared";
// Bare GET against the workspace /api/2.0 namespace - leading /api/2.0
// is auto-stripped so you can pass either form.
const data = await apiUtils.fetchApi<{ endpoints?: unknown[] }>(
"serving-endpoints",
);
// With a per-user cache (useful for "list everything" calls):
const cached = await apiUtils.fetchApi<{ endpoints?: unknown[] }>(
"serving-endpoints",
undefined,
{ userKey: req.userId, options: { ttl: 300 } },
);
// POST + custom client (e.g. service-account script outside a request).
await apiUtils.fetchApi<{ id: string }>(
["serving-endpoints", endpointName, "invocations"],
{
body: JSON.stringify({ inputs }),
headers: { "Content-Type": "application/json" },
},
undefined,
serviceClient,
);Defaults to POST when init.body is set, GET otherwise. The wrapper
needs an active getExecutionContext() to resolve the workspace client
unless a WorkspaceClient is passed in explicitly, so it's server-only.
stringUtils - identifier + slug helpers
toIdentifier / toSlug are deterministic, length-bounded, and always
lower-case at the type level (the lowerCase option literal is fixed to
true so an explicit false is a compile error):
import { stringUtils } from "@dbx-tools/appkit-shared";
stringUtils.toIdentifier("My Cool Project!"); // "my_cool_project"
stringUtils.toSlug("My Cool Project!"); // "my-cool-project"
stringUtils.toIdentifierWithOptions({ maxLength: 12 }, "very long project name");
// "very_long_43c1" <- hash suffix when truncatedprojectUtils - project name + git-remote parsing
import { projectUtils } from "@dbx-tools/appkit-shared";
// Discovers a stable name for the current project. Order:
// 1. `package.json` name (root of an npm/bun workspace if applicable)
// 2. Closest `git remote origin` repo name
// 3. Process `cwd` basename
const name = await projectUtils.name();
// Strip "owner/" + ".git" from a remote URL.
projectUtils.parseGitRemote("[email protected]:org/my-repo.git"); // "my-repo"commonUtils - memoize + hashing
import { commonUtils } from "@dbx-tools/appkit-shared";
// Memoize by all-args; sync results cache forever, async failures bust.
const fetchUser = commonUtils.memoize(async (id: string) => loadUser(id));
// Short, deterministic hash for cache keys / slug suffixes / etc.
// Pure-JS FNV-1a in Crockford-style base-32 (digits + lowercase
// alphabet minus i/l/o/u). Browser-safe.
commonUtils.fnvHash("databricks-claude-sonnet-4-6"); // e.g. "k3p9q7"
commonUtils.fnvHashWithOptions({ length: 4 }, "[email protected]");@memoized is a TC39 stage-1 method decorator built on the same
memoize (requires experimentalDecorators: true in tsconfig.json).
fnvHash is intentionally not cryptographically secure - use it for
keys and slugs, never for tokens or signatures.
logUtils - tagged console logger
logger(plugin) returns a leveled { debug, info, warn, error } interface
that auto-tags every line with the plugin's name:
import { logUtils } from "@dbx-tools/appkit-shared";
class MyPlugin extends Plugin<MyConfig> {
private log = logUtils.logger(this); // tags as "[my-plugin]"
override async setup() {
this.log.info("setup", { mode: this.config.mode });
}
}The logger is intentionally console-backed (no extra deps). For richer
sinks pass your own { debug, info, warn, error } object - the plugins
in this repo accept any matching shape.
LOG_LEVEL filtering
Each call checks process.env.LOG_LEVEL (case-insensitive, default
info) and drops anything below the threshold before string
formatting, so leaving log.debug({...heavy details}) calls in
production code costs nothing as long as LOG_LEVEL isn't debug.
LOG_LEVEL=debug bun dev # full verbosity
LOG_LEVEL=warn bun start # production: hide info chatterThe lookup is per-call (not module-load), so test runners can flip
the threshold after the module has been imported. In browser bundles
where process.env.LOG_LEVEL is undefined, the default info
threshold applies.
License
Apache-2.0
