@jskit-ai/database-runtime 0.1.4

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.
@@ -0,0 +1,174 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import test from "node:test";
6
+
7
+ import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
8
+ import { DatabaseRuntimeServiceProvider } from "../src/server/providers/DatabaseRuntimeServiceProvider.js";
9
+
10
+ function createSingletonApp() {
11
+ const singletons = new Map();
12
+ const instances = new Map();
13
+
14
+ return {
15
+ has(token) {
16
+ return singletons.has(token) || instances.has(token);
17
+ },
18
+ singleton(token, factory) {
19
+ if (this.has(token)) {
20
+ throw new Error(`Token ${String(token)} is already registered.`);
21
+ }
22
+ singletons.set(token, {
23
+ factory,
24
+ resolved: false,
25
+ value: undefined
26
+ });
27
+ },
28
+ instance(token, value) {
29
+ if (this.has(token)) {
30
+ throw new Error(`Token ${String(token)} is already registered.`);
31
+ }
32
+ instances.set(token, value);
33
+ },
34
+ make(token) {
35
+ if (instances.has(token)) {
36
+ return instances.get(token);
37
+ }
38
+ if (!singletons.has(token)) {
39
+ throw new Error(`Token ${String(token)} is not registered.`);
40
+ }
41
+ const entry = singletons.get(token);
42
+ if (!entry.resolved) {
43
+ entry.value = entry.factory(this);
44
+ entry.resolved = true;
45
+ instances.set(token, entry.value);
46
+ }
47
+ return entry.value;
48
+ }
49
+ };
50
+ }
51
+
52
+ function createKnexStub() {
53
+ return {
54
+ async transaction(callback) {
55
+ return callback({ trxId: "trx-1" });
56
+ }
57
+ };
58
+ }
59
+
60
+ test("DatabaseRuntimeServiceProvider registers runtime api", () => {
61
+ const app = createSingletonApp();
62
+ const provider = new DatabaseRuntimeServiceProvider();
63
+ provider.register(app);
64
+
65
+ assert.equal(app.has("runtime.database"), true);
66
+ const api = app.make("runtime.database");
67
+ assert.equal(typeof api.createTransactionManager, "function");
68
+ assert.equal(typeof api.resolveRepoClient, "function");
69
+ });
70
+
71
+ test("DatabaseRuntimeServiceProvider registers transaction manager when Knex is pre-bound", async () => {
72
+ const app = createSingletonApp();
73
+ app.instance(KERNEL_TOKENS.Knex, createKnexStub());
74
+
75
+ const provider = new DatabaseRuntimeServiceProvider();
76
+ provider.register(app);
77
+
78
+ assert.equal(app.has(KERNEL_TOKENS.TransactionManager), true);
79
+ const transactionManager = app.make(KERNEL_TOKENS.TransactionManager);
80
+ const result = await transactionManager.inTransaction(async (trx) => trx.trxId);
81
+ assert.equal(result, "trx-1");
82
+ });
83
+
84
+ test("DatabaseRuntimeServiceProvider driver token resolves to registered mysql driver", () => {
85
+ const app = createSingletonApp();
86
+ app.instance("runtime.database.driver.mysql", Object.freeze({ DIALECT_ID: "mysql2" }));
87
+
88
+ const provider = new DatabaseRuntimeServiceProvider();
89
+ provider.register(app);
90
+
91
+ const driver = app.make("runtime.database.driver");
92
+ assert.deepEqual(driver, { DIALECT_ID: "mysql2" });
93
+ });
94
+
95
+ test("DatabaseRuntimeServiceProvider driver token throws when no driver registered", () => {
96
+ const app = createSingletonApp();
97
+ const provider = new DatabaseRuntimeServiceProvider();
98
+ provider.register(app);
99
+
100
+ assert.throws(() => app.make("runtime.database.driver"), /No database driver is registered\./);
101
+ });
102
+
103
+ test("DatabaseRuntimeServiceProvider driver token throws when multiple drivers are registered", () => {
104
+ const app = createSingletonApp();
105
+ app.instance("runtime.database.driver.mysql", Object.freeze({ DIALECT_ID: "mysql2" }));
106
+ app.instance("runtime.database.driver.postgres", Object.freeze({ DIALECT_ID: "pg" }));
107
+
108
+ const provider = new DatabaseRuntimeServiceProvider();
109
+ provider.register(app);
110
+
111
+ assert.throws(() => app.make("runtime.database.driver"), /Multiple database drivers are registered\./);
112
+ });
113
+
114
+ test("DatabaseRuntimeServiceProvider resolves knex from app root package context", async () => {
115
+ const appRoot = await mkdtemp(path.join(tmpdir(), "jskit-db-runtime-test-"));
116
+ const knexPackageDir = path.join(appRoot, "node_modules", "knex");
117
+ await mkdir(knexPackageDir, { recursive: true });
118
+ await writeFile(path.join(appRoot, "package.json"), JSON.stringify({ name: "runtime-app", private: true }), "utf8");
119
+ await writeFile(
120
+ path.join(knexPackageDir, "package.json"),
121
+ JSON.stringify({
122
+ name: "knex",
123
+ version: "0.0.0-test",
124
+ main: "index.js",
125
+ type: "commonjs"
126
+ }),
127
+ "utf8"
128
+ );
129
+ await writeFile(
130
+ path.join(knexPackageDir, "index.js"),
131
+ [
132
+ "module.exports = function knex(config) {",
133
+ " return {",
134
+ " __source: 'app-root-knex',",
135
+ " __config: config,",
136
+ " transaction: async function transaction(callback) {",
137
+ " return callback({ trxId: 'trx-app-root' });",
138
+ " }",
139
+ " };",
140
+ "};",
141
+ ""
142
+ ].join("\n"),
143
+ "utf8"
144
+ );
145
+
146
+ const previousCwd = process.cwd();
147
+ try {
148
+ process.chdir(appRoot);
149
+
150
+ const app = createSingletonApp();
151
+ app.instance("runtime.database.driver.mysql", Object.freeze({ DIALECT_ID: "mysql2" }));
152
+ app.instance(KERNEL_TOKENS.Env, {
153
+ DB_HOST: "db.local",
154
+ DB_PORT: "3307",
155
+ DB_NAME: "appdb",
156
+ DB_USER: "appuser",
157
+ DB_PASSWORD: "apppass"
158
+ });
159
+
160
+ const provider = new DatabaseRuntimeServiceProvider();
161
+ provider.register(app);
162
+
163
+ const knex = app.make(KERNEL_TOKENS.Knex);
164
+ assert.equal(knex.__source, "app-root-knex");
165
+ assert.equal(knex.__config.client, "mysql2");
166
+ assert.equal(knex.__config.connection.host, "db.local");
167
+ assert.equal(knex.__config.connection.port, 3307);
168
+ assert.equal(knex.__config.connection.database, "appdb");
169
+ assert.equal(knex.__config.connection.user, "appuser");
170
+ assert.equal(knex.__config.connection.password, "apppass");
171
+ } finally {
172
+ process.chdir(previousCwd);
173
+ }
174
+ });
@@ -0,0 +1,22 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { parseMetadataJson, stringifyMetadataJson } from "../src/shared/repositoryOptions.js";
4
+
5
+ test("parseMetadataJson parses object-like metadata payloads", () => {
6
+ assert.deepEqual(parseMetadataJson(""), {});
7
+ assert.deepEqual(parseMetadataJson("{"), {});
8
+ assert.deepEqual(parseMetadataJson("{\"source\":\"console\"}"), { source: "console" });
9
+ assert.deepEqual(parseMetadataJson("[1,2,3]"), [1, 2, 3]);
10
+ });
11
+
12
+ test("stringifyMetadataJson serializes metadata payloads", () => {
13
+ assert.equal(stringifyMetadataJson(null), "{}");
14
+ assert.equal(stringifyMetadataJson({ source: "console" }), "{\"source\":\"console\"}");
15
+ assert.equal(stringifyMetadataJson([1, 2, 3]), "[1,2,3]");
16
+ });
17
+
18
+ test("stringifyMetadataJson returns fallback for non-serializable values", () => {
19
+ const circular = {};
20
+ circular.self = circular;
21
+ assert.equal(stringifyMetadataJson(circular), "{}");
22
+ });
@@ -0,0 +1,103 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createRepositoryScope } from "../src/shared/repositoryScope.js";
4
+
5
+ function createQueryBuilder(calls) {
6
+ return {
7
+ where(...args) {
8
+ calls.push(["where", ...args]);
9
+ return this;
10
+ },
11
+ whereRaw(sql) {
12
+ calls.push(["whereRaw", sql]);
13
+ return this;
14
+ }
15
+ };
16
+ }
17
+
18
+ function createKnexStub() {
19
+ const calls = [];
20
+
21
+ function knex(tableName) {
22
+ calls.push(["table", tableName]);
23
+ return createQueryBuilder(calls);
24
+ }
25
+
26
+ return { knex, calls };
27
+ }
28
+
29
+ test("createRepositoryScope builds explicit scoped query helpers", () => {
30
+ const { knex, calls } = createKnexStub();
31
+ const scope = createRepositoryScope(knex, "contacts");
32
+
33
+ scope.scoped({
34
+ visibilityContext: {
35
+ visibility: "workspace",
36
+ scopeOwnerId: 12
37
+ }
38
+ });
39
+
40
+ assert.deepEqual(calls, [["table", "contacts"], ["where", "workspace_owner_id", 12]]);
41
+ });
42
+
43
+ test("createRepositoryScope supports scopedById with custom id column", () => {
44
+ const { knex, calls } = createKnexStub();
45
+ const scope = createRepositoryScope(knex, "contacts", {
46
+ idColumn: "contact_id"
47
+ });
48
+
49
+ scope.scopedById(33, {
50
+ visibilityContext: {
51
+ visibility: "user",
52
+ userOwnerId: 7
53
+ }
54
+ });
55
+
56
+ assert.deepEqual(calls, [["table", "contacts"], ["where", "user_owner_id", 7], ["where", "contact_id", 33]]);
57
+ });
58
+
59
+ test("createRepositoryScope exposes applyToQuery and owner stamping", () => {
60
+ const { knex, calls } = createKnexStub();
61
+ const scope = createRepositoryScope(knex, "contacts");
62
+ const queryBuilder = createQueryBuilder(calls);
63
+
64
+ scope.applyToQuery(queryBuilder, {
65
+ visibilityContext: {
66
+ visibility: "workspace_user",
67
+ scopeOwnerId: 4,
68
+ userOwnerId: 9
69
+ }
70
+ });
71
+
72
+ assert.deepEqual(calls, [["where", "workspace_owner_id", 4], ["where", "user_owner_id", 9]]);
73
+
74
+ assert.deepEqual(
75
+ scope.withOwners(
76
+ {
77
+ name: "Ada"
78
+ },
79
+ {
80
+ visibilityContext: {
81
+ visibility: "workspace_user",
82
+ scopeOwnerId: 4,
83
+ userOwnerId: 9
84
+ }
85
+ }
86
+ ),
87
+ {
88
+ name: "Ada",
89
+ workspace_owner_id: 4,
90
+ user_owner_id: 9
91
+ }
92
+ );
93
+
94
+ assert.equal(scope.clientOf(), knex);
95
+ assert.equal(typeof scope.table, "function");
96
+ });
97
+
98
+ test("createRepositoryScope validates required inputs", () => {
99
+ const { knex } = createKnexStub();
100
+
101
+ assert.throws(() => createRepositoryScope(null, "contacts"), /requires knex/);
102
+ assert.throws(() => createRepositoryScope(knex, ""), /requires tableName/);
103
+ });
@@ -0,0 +1,21 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { normalizeBatchSize, normalizeCutoffDateOrThrow, normalizeDeletedRowCount } from "../src/shared/retention.js";
4
+
5
+ test("normalizeBatchSize clamps to configured max", () => {
6
+ assert.equal(normalizeBatchSize(undefined, { fallback: 100, max: 500 }), 100);
7
+ assert.equal(normalizeBatchSize(0, { fallback: 100, max: 500 }), 100);
8
+ assert.equal(normalizeBatchSize(250, { fallback: 100, max: 500 }), 250);
9
+ assert.equal(normalizeBatchSize(700, { fallback: 100, max: 500 }), 500);
10
+ });
11
+
12
+ test("normalizeCutoffDateOrThrow validates date", () => {
13
+ assert.ok(normalizeCutoffDateOrThrow("2024-01-01T00:00:00.000Z") instanceof Date);
14
+ assert.throws(() => normalizeCutoffDateOrThrow("invalid"), /Invalid cutoff date\./);
15
+ });
16
+
17
+ test("normalizeDeletedRowCount returns bounded non-negative number", () => {
18
+ assert.equal(normalizeDeletedRowCount(3), 3);
19
+ assert.equal(normalizeDeletedRowCount(-1), 0);
20
+ assert.equal(normalizeDeletedRowCount("x"), 0);
21
+ });
@@ -0,0 +1,98 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
5
+ import {
6
+ BaseRepository,
7
+ buildPaginationMeta,
8
+ createTransactionManager,
9
+ registerDatabaseRuntime
10
+ } from "../src/shared/index.js";
11
+
12
+ function createKnexStub() {
13
+ return {
14
+ async transaction(callback) {
15
+ return callback({ trxId: "trx-1" });
16
+ }
17
+ };
18
+ }
19
+
20
+ function createSingletonApp() {
21
+ const singletons = new Map();
22
+ const instances = new Map();
23
+
24
+ return {
25
+ has(token) {
26
+ return singletons.has(token) || instances.has(token);
27
+ },
28
+ instance(token, value) {
29
+ if (this.has(token)) {
30
+ throw new Error(`Token ${String(token)} is already registered.`);
31
+ }
32
+ instances.set(token, value);
33
+ },
34
+ singleton(token, factory) {
35
+ if (this.has(token)) {
36
+ throw new Error(`Token ${String(token)} is already registered.`);
37
+ }
38
+ singletons.set(token, {
39
+ factory,
40
+ resolved: false,
41
+ value: undefined
42
+ });
43
+ },
44
+ make(token) {
45
+ if (instances.has(token)) {
46
+ return instances.get(token);
47
+ }
48
+ if (!singletons.has(token)) {
49
+ throw new Error(`Token ${String(token)} is not registered.`);
50
+ }
51
+ const entry = singletons.get(token);
52
+ if (!entry.resolved) {
53
+ entry.value = entry.factory(this);
54
+ entry.resolved = true;
55
+ instances.set(token, entry.value);
56
+ }
57
+ return entry.value;
58
+ }
59
+ };
60
+ }
61
+
62
+ test("transaction manager wraps callback in knex transaction", async () => {
63
+ const manager = createTransactionManager({ knex: createKnexStub() });
64
+ const result = await manager.inTransaction(async (trx) => ({ ok: true, trx }));
65
+ assert.equal(result.ok, true);
66
+ assert.equal(result.trx.trxId, "trx-1");
67
+ });
68
+
69
+ test("base repository withTransaction delegates to transaction manager", async () => {
70
+ const knex = createKnexStub();
71
+ const transactionManager = createTransactionManager({ knex });
72
+ const repo = new BaseRepository({ knex, transactionManager });
73
+
74
+ const result = await repo.withTransaction(async (trx) => ({ id: trx.trxId }));
75
+ assert.deepEqual(result, { id: "trx-1" });
76
+ });
77
+
78
+ test("pagination helpers generate stable metadata", () => {
79
+ const meta = buildPaginationMeta({ total: 51, page: 2, pageSize: 25 });
80
+ assert.deepEqual(meta, {
81
+ total: 51,
82
+ page: 2,
83
+ pageSize: 25,
84
+ pageCount: 3,
85
+ hasPrev: true,
86
+ hasNext: true
87
+ });
88
+ });
89
+
90
+ test("registerDatabaseRuntime binds knex and transaction manager tokens", () => {
91
+ const app = createSingletonApp();
92
+ const knex = createKnexStub();
93
+
94
+ const runtime = registerDatabaseRuntime(app, { knex });
95
+ assert.strictEqual(runtime.knex, knex);
96
+ assert.equal(typeof runtime.transactionManager.inTransaction, "function");
97
+ assert.strictEqual(app.make(KERNEL_TOKENS.Knex), knex);
98
+ });
@@ -0,0 +1,93 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { applyVisibility, applyVisibilityOwners } from "../src/shared/visibility.js";
4
+
5
+ function createQueryBuilderStub() {
6
+ return {
7
+ calls: [],
8
+ where(...args) {
9
+ this.calls.push(["where", ...args]);
10
+ return this;
11
+ },
12
+ whereRaw(sql) {
13
+ this.calls.push(["whereRaw", sql]);
14
+ return this;
15
+ }
16
+ };
17
+ }
18
+
19
+ test("applyVisibility appends scope filters to query builders", () => {
20
+ const publicQuery = createQueryBuilderStub();
21
+ applyVisibility(publicQuery, {
22
+ visibility: "public"
23
+ });
24
+ assert.deepEqual(publicQuery.calls, []);
25
+
26
+ const workspaceQuery = createQueryBuilderStub();
27
+ applyVisibility(workspaceQuery, {
28
+ visibility: "workspace",
29
+ scopeOwnerId: 12
30
+ });
31
+ assert.deepEqual(workspaceQuery.calls, [["where", "workspace_owner_id", 12]]);
32
+
33
+ const userQuery = createQueryBuilderStub();
34
+ applyVisibility(userQuery, {
35
+ visibility: "user",
36
+ userOwnerId: 7
37
+ });
38
+ assert.deepEqual(userQuery.calls, [["where", "user_owner_id", 7]]);
39
+
40
+ const deniedQuery = createQueryBuilderStub();
41
+ applyVisibility(deniedQuery, {
42
+ visibility: "workspace_user",
43
+ scopeOwnerId: 3
44
+ });
45
+ assert.deepEqual(deniedQuery.calls, [["whereRaw", "1 = 0"]]);
46
+ });
47
+
48
+ test("applyVisibilityOwners injects owner columns for write payloads", () => {
49
+ assert.deepEqual(
50
+ applyVisibilityOwners(
51
+ {
52
+ name: "Alice"
53
+ },
54
+ {
55
+ visibility: "public"
56
+ }
57
+ ),
58
+ {
59
+ name: "Alice"
60
+ }
61
+ );
62
+
63
+ assert.deepEqual(
64
+ applyVisibilityOwners(
65
+ {
66
+ name: "Alice"
67
+ },
68
+ {
69
+ visibility: "workspace_user",
70
+ scopeOwnerId: 4,
71
+ userOwnerId: 9
72
+ }
73
+ ),
74
+ {
75
+ name: "Alice",
76
+ workspace_owner_id: 4,
77
+ user_owner_id: 9
78
+ }
79
+ );
80
+
81
+ assert.throws(
82
+ () =>
83
+ applyVisibilityOwners(
84
+ {
85
+ name: "Alice"
86
+ },
87
+ {
88
+ visibility: "user"
89
+ }
90
+ ),
91
+ /requires userOwnerId/
92
+ );
93
+ });