@dbx-tools/appkit 0.3.37 → 0.3.39
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/package.json +7 -3
- package/test/config.test.ts +0 -63
- package/test/lakebase-resolver.test.ts +0 -82
- package/test/pgaddress.test.ts +0 -76
- package/test/plugin.test.ts +0 -64
- package/test/provision.test.ts +0 -35
- package/test/tsconfig.json +0 -14
- package/tsconfig.json +0 -41
package/package.json
CHANGED
|
@@ -18,21 +18,25 @@
|
|
|
18
18
|
"@databricks/sdk-experimental": "^0.17.0",
|
|
19
19
|
"yaml": "^2.9.0",
|
|
20
20
|
"zod": "4.3.6",
|
|
21
|
-
"@dbx-tools/core": "0.3.
|
|
22
|
-
"@dbx-tools/shared-core": "0.3.
|
|
21
|
+
"@dbx-tools/core": "0.3.39",
|
|
22
|
+
"@dbx-tools/shared-core": "0.3.39"
|
|
23
23
|
},
|
|
24
24
|
"main": "index.ts",
|
|
25
25
|
"license": "UNLICENSED",
|
|
26
26
|
"publishConfig": {
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
|
-
"version": "0.3.
|
|
29
|
+
"version": "0.3.39",
|
|
30
30
|
"types": "index.ts",
|
|
31
31
|
"type": "module",
|
|
32
32
|
"exports": {
|
|
33
33
|
".": "./index.ts",
|
|
34
34
|
"./package.json": "./package.json"
|
|
35
35
|
},
|
|
36
|
+
"files": [
|
|
37
|
+
"index.ts",
|
|
38
|
+
"src"
|
|
39
|
+
],
|
|
36
40
|
"peerDependenciesMeta": {
|
|
37
41
|
"@databricks/appkit": {
|
|
38
42
|
"optional": true
|
package/test/config.test.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, describe, it } from "node:test";
|
|
3
|
-
import { ValidationError } from "@databricks/appkit";
|
|
4
|
-
import { resolveConfigValue, withCliSources, type ConfigSource } from "../src/config";
|
|
5
|
-
|
|
6
|
-
const ENV_KEY = "DBX_TOOLS_APPKIT_TEST_VALUE";
|
|
7
|
-
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
delete process.env[ENV_KEY];
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
describe("config precedence", () => {
|
|
13
|
-
it("prefers explicit config over the environment", async () => {
|
|
14
|
-
process.env[ENV_KEY] = "from-env";
|
|
15
|
-
const value = await resolveConfigValue(ENV_KEY, {
|
|
16
|
-
explicit: { [ENV_KEY]: "from-explicit" },
|
|
17
|
-
});
|
|
18
|
-
assert.equal(value, "from-explicit");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("falls back to the environment when explicit config omits the key", async () => {
|
|
22
|
-
process.env[ENV_KEY] = "from-env";
|
|
23
|
-
const value = await resolveConfigValue(ENV_KEY, { explicit: { OTHER_KEY: "x" } });
|
|
24
|
-
assert.equal(value, "from-env");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("prepends explicit even when the caller passes its own source list", async () => {
|
|
28
|
-
process.env[ENV_KEY] = "from-env";
|
|
29
|
-
const value = await resolveConfigValue(ENV_KEY, {
|
|
30
|
-
sources: ["env"],
|
|
31
|
-
explicit: { [ENV_KEY]: "from-explicit" },
|
|
32
|
-
});
|
|
33
|
-
assert.equal(value, "from-explicit");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("lets a cli flag win when cli sources are requested", async () => {
|
|
37
|
-
process.env[ENV_KEY] = "from-env";
|
|
38
|
-
const value = await resolveConfigValue(ENV_KEY, {
|
|
39
|
-
sources: withCliSources(),
|
|
40
|
-
cli: { [ENV_KEY]: "from-cli" },
|
|
41
|
-
explicit: { [ENV_KEY]: "from-explicit" },
|
|
42
|
-
});
|
|
43
|
-
assert.equal(value, "from-cli");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("orders cli sources ahead of explicit and the default sources", () => {
|
|
47
|
-
assert.deepEqual(withCliSources(), ["cli", "explicit", "env", "bundle"]);
|
|
48
|
-
assert.deepEqual(withCliSources(["explicit", "env"]), ["cli", "explicit", "env"]);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("skips blank values instead of treating them as resolved", async () => {
|
|
52
|
-
process.env[ENV_KEY] = "from-env";
|
|
53
|
-
const value = await resolveConfigValue(ENV_KEY, { explicit: { [ENV_KEY]: " " } });
|
|
54
|
-
assert.equal(value, "from-env");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("rejects an unknown source", async () => {
|
|
58
|
-
await assert.rejects(
|
|
59
|
-
() => resolveConfigValue(ENV_KEY, { sources: ["nope" as ConfigSource] }),
|
|
60
|
-
ValidationError,
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { ValidationError } from "@databricks/appkit";
|
|
4
|
-
import { nextPollDelay, parsePort, parseSslMode, pollDelay } from "../src/lakebase-resolver";
|
|
5
|
-
|
|
6
|
-
describe("PGPORT validation", () => {
|
|
7
|
-
it("accepts a numeric string and a number", () => {
|
|
8
|
-
assert.equal(parsePort("5433"), 5433);
|
|
9
|
-
assert.equal(parsePort(5432), 5432);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("treats absent and empty values as unset", () => {
|
|
13
|
-
assert.equal(parsePort(undefined), undefined);
|
|
14
|
-
assert.equal(parsePort(""), undefined);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("rejects anything that is not a TCP port instead of yielding NaN", () => {
|
|
18
|
-
for (const bad of ["abc", "0", "70000", "5432.5"]) {
|
|
19
|
-
assert.throws(
|
|
20
|
-
() => parsePort(bad),
|
|
21
|
-
(err) => {
|
|
22
|
-
assert.ok(err instanceof ValidationError);
|
|
23
|
-
assert.match(err.message, /PGPORT/);
|
|
24
|
-
return true;
|
|
25
|
-
},
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe("PGSSLMODE validation", () => {
|
|
32
|
-
it("normalizes case and surrounding space", () => {
|
|
33
|
-
assert.equal(parseSslMode(" Require "), "require");
|
|
34
|
-
assert.equal(parseSslMode("disable"), "disable");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("treats absent and empty values as unset", () => {
|
|
38
|
-
assert.equal(parseSslMode(undefined), undefined);
|
|
39
|
-
assert.equal(parseSslMode(""), undefined);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("rejects a mode pg does not accept", () => {
|
|
43
|
-
assert.throws(
|
|
44
|
-
() => parseSslMode("verify-full"),
|
|
45
|
-
(err) => {
|
|
46
|
-
assert.ok(err instanceof ValidationError);
|
|
47
|
-
assert.match(err.message, /PGSSLMODE/);
|
|
48
|
-
return true;
|
|
49
|
-
},
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe("poll backoff", () => {
|
|
55
|
-
it("grows with the attempt and stays inside the jitter band", () => {
|
|
56
|
-
const base = 2_000;
|
|
57
|
-
for (let attempt = 0; attempt < 10; attempt++) {
|
|
58
|
-
const delay = nextPollDelay(attempt, base);
|
|
59
|
-
assert.ok(delay >= 0);
|
|
60
|
-
assert.ok(delay <= 15_000 * 1.2 + 1, `attempt ${attempt} produced ${delay}`);
|
|
61
|
-
}
|
|
62
|
-
const first = Array.from({ length: 20 }, () => nextPollDelay(0, base));
|
|
63
|
-
const later = Array.from({ length: 20 }, () => nextPollDelay(4, base));
|
|
64
|
-
assert.ok(Math.max(...first) < Math.min(...later));
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("caps the delay so a long wait keeps polling", () => {
|
|
68
|
-
assert.ok(nextPollDelay(50, 2_000) <= 15_000 * 1.2 + 1);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("rejects with the abort reason when cancelled mid-wait", async () => {
|
|
72
|
-
const controller = new AbortController();
|
|
73
|
-
const waiting = pollDelay(5, 10_000, controller.signal);
|
|
74
|
-
const reason = new Error("boot cancelled");
|
|
75
|
-
controller.abort(reason);
|
|
76
|
-
await assert.rejects(waiting, reason);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("rejects immediately for an already-aborted signal", async () => {
|
|
80
|
-
await assert.rejects(pollDelay(0, 10_000, AbortSignal.abort(new Error("gone"))), /gone/);
|
|
81
|
-
});
|
|
82
|
-
});
|
package/test/pgaddress.test.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { parseAddress, parseResourcePath, SSL_MODES } from "../src/pgaddress";
|
|
4
|
-
|
|
5
|
-
describe("pgaddress parseAddress", () => {
|
|
6
|
-
it("returns nothing for empty and unrecognized input", () => {
|
|
7
|
-
assert.deepEqual(parseAddress(undefined), {});
|
|
8
|
-
assert.deepEqual(parseAddress(""), {});
|
|
9
|
-
assert.deepEqual(parseAddress(" "), {});
|
|
10
|
-
assert.deepEqual(parseAddress("Not An Address"), {});
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("splits a Postgres URI into host, user, database, port and ssl mode", () => {
|
|
14
|
-
const parsed = parseAddress(
|
|
15
|
-
"postgresql://me%40acme.com@ep-1.database.eastus2.azuredatabricks.net:5433/app?sslmode=disable",
|
|
16
|
-
);
|
|
17
|
-
assert.equal(parsed.host, "ep-1.database.eastus2.azuredatabricks.net");
|
|
18
|
-
assert.equal(parsed.user, "me@acme.com");
|
|
19
|
-
assert.equal(parsed.database, "app");
|
|
20
|
-
assert.equal(parsed.port, 5433);
|
|
21
|
-
assert.equal(parsed.sslMode, "disable");
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("ignores an ssl mode the driver does not accept", () => {
|
|
25
|
-
assert.equal(
|
|
26
|
-
parseAddress("postgres://h.example.com/db?sslmode=verify-full").sslMode,
|
|
27
|
-
undefined,
|
|
28
|
-
);
|
|
29
|
-
assert.deepEqual([...SSL_MODES], ["require", "disable", "prefer"]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("recovers project, branch and endpoint from a canonical endpoint path", () => {
|
|
33
|
-
const path = "projects/demo/branches/production/endpoints/ep-1";
|
|
34
|
-
assert.deepEqual(parseAddress(path), {
|
|
35
|
-
project: "demo",
|
|
36
|
-
branch: "production",
|
|
37
|
-
endpointId: "ep-1",
|
|
38
|
-
endpoint: path,
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("keeps a database resource id separate from PGDATABASE", () => {
|
|
43
|
-
assert.deepEqual(
|
|
44
|
-
parseAddress("projects/demo/branches/production/databases/databricks-postgres"),
|
|
45
|
-
{
|
|
46
|
-
project: "demo",
|
|
47
|
-
branch: "production",
|
|
48
|
-
databaseResourceId: "databricks-postgres",
|
|
49
|
-
},
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("reads shorter resource paths", () => {
|
|
54
|
-
assert.deepEqual(parseAddress("projects/demo"), { project: "demo" });
|
|
55
|
-
assert.deepEqual(parseAddress("projects/demo/branches/main"), {
|
|
56
|
-
project: "demo",
|
|
57
|
-
branch: "main",
|
|
58
|
-
});
|
|
59
|
-
assert.deepEqual(parseAddress("projects/demo/branches"), {});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("treats a dotted value as a hostname and a bare slug as a project id", () => {
|
|
63
|
-
assert.deepEqual(parseAddress("ep-1.database.azuredatabricks.net"), {
|
|
64
|
-
host: "ep-1.database.azuredatabricks.net",
|
|
65
|
-
});
|
|
66
|
-
assert.deepEqual(parseAddress("dbx-tools-demo"), { project: "dbx-tools-demo" });
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe("pgaddress parseResourcePath", () => {
|
|
71
|
-
it("only accepts `projects/` paths so a bare branch id is not read as a project", () => {
|
|
72
|
-
assert.deepEqual(parseResourcePath("production"), {});
|
|
73
|
-
assert.deepEqual(parseResourcePath(undefined), {});
|
|
74
|
-
assert.equal(parseResourcePath("projects/demo/branches/main").branch, "main");
|
|
75
|
-
});
|
|
76
|
-
});
|
package/test/plugin.test.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { ConfigurationError } from "@databricks/appkit";
|
|
4
|
-
import { data, instance, require as requirePlugin, type PluginContextLike } from "../src/plugin";
|
|
5
|
-
|
|
6
|
-
class FakeLakebasePlugin {
|
|
7
|
-
exports() {
|
|
8
|
-
return { pool: "pool" };
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function fakeFactory(name: string, calls: { count: number }) {
|
|
13
|
-
return () => {
|
|
14
|
-
calls.count += 1;
|
|
15
|
-
return { plugin: FakeLakebasePlugin, name };
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function fakeContext(entries: Record<string, unknown>): PluginContextLike {
|
|
20
|
-
const plugins = new Map(Object.entries(entries));
|
|
21
|
-
return { getPlugins: () => plugins };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describe("plugin lookup", () => {
|
|
25
|
-
it("caches the factory descriptor per factory", () => {
|
|
26
|
-
const calls = { count: 0 };
|
|
27
|
-
const factory = fakeFactory("lakebase", calls);
|
|
28
|
-
assert.equal(data(factory).name, "lakebase");
|
|
29
|
-
assert.equal(data(factory).name, "lakebase");
|
|
30
|
-
assert.equal(calls.count, 1);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("returns the registered instance, or undefined without a context", () => {
|
|
34
|
-
const factory = fakeFactory("lakebase", { count: 0 });
|
|
35
|
-
const plugin = new FakeLakebasePlugin();
|
|
36
|
-
assert.equal(instance(fakeContext({ lakebase: plugin }), factory), plugin);
|
|
37
|
-
assert.equal(instance(fakeContext({}), factory), undefined);
|
|
38
|
-
assert.equal(instance(undefined, factory), undefined);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("require returns the instance when registered", () => {
|
|
42
|
-
const factory = fakeFactory("lakebase", { count: 0 });
|
|
43
|
-
const plugin = new FakeLakebasePlugin();
|
|
44
|
-
assert.equal(requirePlugin(fakeContext({ lakebase: plugin }), factory), plugin);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("require throws a ConfigurationError naming the plugin and the caller", () => {
|
|
48
|
-
const factory = fakeFactory("lakebase", { count: 0 });
|
|
49
|
-
assert.throws(
|
|
50
|
-
() => requirePlugin(fakeContext({ server: {} }), factory, "mastra"),
|
|
51
|
-
(err) => {
|
|
52
|
-
assert.ok(err instanceof ConfigurationError);
|
|
53
|
-
assert.match(err.message, /mastra/);
|
|
54
|
-
assert.match(err.message, /lakebase/);
|
|
55
|
-
return true;
|
|
56
|
-
},
|
|
57
|
-
);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("require throws without a context at all", () => {
|
|
61
|
-
const factory = fakeFactory("lakebase", { count: 0 });
|
|
62
|
-
assert.throws(() => requirePlugin(undefined, factory), ConfigurationError);
|
|
63
|
-
});
|
|
64
|
-
});
|
package/test/provision.test.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { ValidationError } from "@databricks/appkit";
|
|
4
|
-
import { cacheGrantStatements } from "../src/provision";
|
|
5
|
-
|
|
6
|
-
describe("cache schema grants", () => {
|
|
7
|
-
it("quotes the schema and an email-shaped role", () => {
|
|
8
|
-
const statements = cacheGrantStatements("me@acme.com");
|
|
9
|
-
assert.equal(statements.length, 5);
|
|
10
|
-
for (const sql of statements) {
|
|
11
|
-
assert.match(sql, /"appkit"/);
|
|
12
|
-
assert.match(sql, /"me@acme\.com"/);
|
|
13
|
-
}
|
|
14
|
-
assert.equal(statements[0], 'GRANT USAGE, CREATE ON SCHEMA "appkit" TO "me@acme.com"');
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("rejects a role name that could not be an identifier", () => {
|
|
18
|
-
for (const bad of ['ev"il', "role; DROP SCHEMA appkit", "", "a b"]) {
|
|
19
|
-
assert.throws(
|
|
20
|
-
() => cacheGrantStatements(bad),
|
|
21
|
-
(err) => {
|
|
22
|
-
assert.ok(err instanceof ValidationError);
|
|
23
|
-
assert.match(err.message, /role/);
|
|
24
|
-
return true;
|
|
25
|
-
},
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("keeps the grants idempotent and scoped to the cache schema", () => {
|
|
31
|
-
const statements = cacheGrantStatements("sp-1234").join("\n");
|
|
32
|
-
assert.match(statements, /ALTER DEFAULT PRIVILEGES IN SCHEMA "appkit"/);
|
|
33
|
-
assert.doesNotMatch(statements, /public/);
|
|
34
|
-
});
|
|
35
|
-
});
|
package/test/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// ~~ Generated by projen. To modify, edit .projenrc.js and run "pnpm exec projen".
|
|
2
|
-
{
|
|
3
|
-
"extends": "../tsconfig.json",
|
|
4
|
-
"compilerOptions": {
|
|
5
|
-
"noEmit": true,
|
|
6
|
-
"rootDir": ".."
|
|
7
|
-
},
|
|
8
|
-
"include": [
|
|
9
|
-
"**/*.ts"
|
|
10
|
-
],
|
|
11
|
-
"exclude": [
|
|
12
|
-
"node_modules"
|
|
13
|
-
]
|
|
14
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
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
|
-
}
|