@jskit-ai/database-runtime 0.1.159 → 0.1.161

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.
@@ -1,51 +1,16 @@
1
1
  import assert from "node:assert/strict";
2
- import { readFile } from "node:fs/promises";
3
- import path from "node:path";
4
2
  import test from "node:test";
5
- import { fileURLToPath } from "node:url";
6
3
  import packageJson from "../package.json" with { type: "json" };
7
4
 
8
- const packageMetadata = packageJson.jskit;
9
-
10
- const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
-
12
- test("database-runtime db migrate scripts sync JSKIT-managed migrations before Knex reads them", () => {
13
- const scripts = packageMetadata.mutations.packageJson.scripts;
14
-
15
- assert.equal(scripts["db:migrations:sync"], "jskit migrations sync");
16
- assert.equal(
17
- scripts["db:migrate"],
18
- "npm run db:migrations:sync && knex --knexfile ./knexfile.js migrate:latest"
19
- );
20
- assert.equal(
21
- scripts["db:migrate:status"],
22
- "npm run db:migrations:sync && knex --knexfile ./knexfile.js migrate:list"
23
- );
24
- assert.equal(
25
- scripts["db:migrate:rollback"],
26
- "knex --knexfile ./knexfile.js migrate:rollback"
27
- );
28
- assert.deepEqual(packageMetadata.ci.steps, [
29
- {
30
- id: "database-migrations",
31
- phase: "before-verify",
32
- label: "Apply database migrations",
33
- command: "npm run db:migrate"
34
- }
35
- ]);
36
- });
37
-
38
- test("database-runtime runs deferred constraints after all ordinary migrations", async () => {
39
- const knexfile = await readFile(path.join(PACKAGE_ROOT, "templates/knexfile.js"), "utf8");
40
- const deferredDirectoryMutation = packageMetadata.mutations.files.find(
41
- (mutation) => mutation.id === "database-runtime-constraint-migrations-dir"
42
- );
43
-
44
- assert.match(
45
- knexfile,
46
- /directory:\s*\[migrationsDirectory,\s*deferredConstraintsDirectory\]/
47
- );
48
- assert.match(knexfile, /sortDirsSeparately:\s*true/);
49
- assert.match(knexfile, /path\.join\(migrationsDirectory,\s*"constraints"\)/);
50
- assert.equal(deferredDirectoryMutation?.to, "migrations/constraints/.gitkeep");
5
+ test("database-runtime exposes migration configuration without authoring mutations", () => {
6
+ assert.equal(packageJson.exports?.["./server/knexMigrationConfig"], "./src/server/knexMigrationConfig.js");
7
+ assert.equal(packageJson.exports?.["./server/databaseSetup"], "./src/server/databaseSetup.js");
8
+ assert.equal(Object.hasOwn(packageJson.jskit, "mutations"), false);
9
+ assert.equal(Object.hasOwn(packageJson.jskit, "ci"), false);
10
+ assert.equal(Object.hasOwn(packageJson.jskit, "options"), false);
11
+ assert.equal(Object.hasOwn(packageJson.jskit.metadata.apiSummary, "containerTokens"), false);
12
+ assert.deepEqual(packageJson.jskit.capabilities, {
13
+ provides: ["runtime.database"],
14
+ requires: ["runtime.database.driver"]
15
+ });
51
16
  });
@@ -1,63 +1,36 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
3
- import path from "node:path";
2
+ import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
4
3
  import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
5
  import test from "node:test";
6
6
 
7
- import { DatabaseRuntimeServiceProvider } from "../src/server/providers/DatabaseRuntimeServiceProvider.js";
8
-
9
- function createSingletonApp() {
10
- const singletons = new Map();
11
- const instances = new Map();
12
-
13
- return {
14
- has(token) {
15
- return singletons.has(token) || instances.has(token);
16
- },
17
- singleton(token, factory) {
18
- if (this.has(token)) {
19
- throw new Error(`Token ${String(token)} is already registered.`);
20
- }
21
- singletons.set(token, {
22
- factory,
23
- resolved: false,
24
- value: undefined
25
- });
26
- },
27
- instance(token, value) {
28
- if (this.has(token)) {
29
- throw new Error(`Token ${String(token)} is already registered.`);
30
- }
31
- instances.set(token, value);
32
- },
33
- make(token) {
34
- if (instances.has(token)) {
35
- return instances.get(token);
36
- }
37
- if (!singletons.has(token)) {
38
- throw new Error(`Token ${String(token)} is not registered.`);
39
- }
40
- const entry = singletons.get(token);
41
- if (!entry.resolved) {
42
- entry.value = entry.factory(this);
43
- entry.resolved = true;
44
- instances.set(token, entry.value);
45
- }
46
- return entry.value;
7
+ import { defineProvider, createCapabilityRuntime } from "@jskit-ai/kernel/shared/capabilities";
8
+ import { DatabaseProvider } from "../src/server/providers/DatabaseProvider.js";
9
+
10
+ function driverProvider(dialectId = "mysql2") {
11
+ return defineProvider({
12
+ id: `test.database.driver.${dialectId}`,
13
+ provides: { driver: "runtime.database.driver" },
14
+ setup() {
15
+ return {
16
+ driver: Object.freeze({
17
+ DIALECT_ID: dialectId,
18
+ getDialectId: () => dialectId
19
+ })
20
+ };
47
21
  }
48
- };
22
+ });
49
23
  }
50
24
 
51
- function createKnexStub() {
52
- return {
53
- destroyCalls: 0,
54
- async transaction(callback) {
55
- return callback({ trxId: "trx-1" });
56
- },
57
- async destroy() {
58
- this.destroyCalls += 1;
25
+ function databaseConsumer(onDatabase) {
26
+ return defineProvider({
27
+ id: "test.database.consumer",
28
+ requires: { database: "runtime.database" },
29
+ setup({ database }) {
30
+ onDatabase(database);
31
+ return {};
59
32
  }
60
- };
33
+ });
61
34
  }
62
35
 
63
36
  async function withAppRootKnexStub(callback) {
@@ -67,12 +40,7 @@ async function withAppRootKnexStub(callback) {
67
40
  await writeFile(path.join(appRoot, "package.json"), JSON.stringify({ name: "runtime-app", private: true }), "utf8");
68
41
  await writeFile(
69
42
  path.join(knexPackageDir, "package.json"),
70
- JSON.stringify({
71
- name: "knex",
72
- version: "0.0.0-test",
73
- main: "index.js",
74
- type: "commonjs"
75
- }),
43
+ JSON.stringify({ name: "knex", version: "0.0.0-test", main: "index.js", type: "commonjs" }),
76
44
  "utf8"
77
45
  );
78
46
  await writeFile(
@@ -82,9 +50,11 @@ async function withAppRootKnexStub(callback) {
82
50
  " return {",
83
51
  " __source: 'app-root-knex',",
84
52
  " __config: config,",
53
+ " destroyCalls: 0,",
85
54
  " transaction: async function transaction(callback) {",
86
55
  " return callback({ trxId: 'trx-app-root' });",
87
- " }",
56
+ " },",
57
+ " destroy: async function destroy() { this.destroyCalls += 1; }",
88
58
  " };",
89
59
  "};",
90
60
  ""
@@ -101,120 +71,92 @@ async function withAppRootKnexStub(callback) {
101
71
  }
102
72
  }
103
73
 
104
- test("DatabaseRuntimeServiceProvider registers runtime api", () => {
105
- const app = createSingletonApp();
106
- const provider = new DatabaseRuntimeServiceProvider();
107
- provider.register(app);
108
-
109
- assert.equal(app.has("runtime.database"), true);
110
- const api = app.make("runtime.database");
111
- assert.equal(typeof api.createTransactionManager, "function");
112
- assert.equal(typeof api.resolveRepoClient, "function");
113
- });
114
-
115
- test("DatabaseRuntimeServiceProvider registers transaction manager when Knex is pre-bound", async () => {
116
- const app = createSingletonApp();
117
- app.instance("jskit.database.knex", createKnexStub());
118
-
119
- const provider = new DatabaseRuntimeServiceProvider();
120
- provider.register(app);
121
-
122
- assert.equal(app.has("jskit.database.transactionManager"), true);
123
- const transactionManager = app.make("jskit.database.transactionManager");
124
- const result = await transactionManager.inTransaction(async (trx) => trx.trxId);
125
- assert.equal(result, "trx-1");
126
- });
127
-
128
- test("DatabaseRuntimeServiceProvider driver token resolves to registered mysql driver", () => {
129
- const app = createSingletonApp();
130
- app.instance("runtime.database.driver.mysql", Object.freeze({ DIALECT_ID: "mysql2" }));
131
-
132
- const provider = new DatabaseRuntimeServiceProvider();
133
- provider.register(app);
134
-
135
- const driver = app.make("runtime.database.driver");
136
- assert.deepEqual(driver, { DIALECT_ID: "mysql2" });
137
- });
138
-
139
- test("DatabaseRuntimeServiceProvider driver token throws when no driver registered", () => {
140
- const app = createSingletonApp();
141
- const provider = new DatabaseRuntimeServiceProvider();
142
- provider.register(app);
143
-
144
- assert.throws(() => app.make("runtime.database.driver"), /No database driver is registered\./);
145
- });
146
-
147
- test("DatabaseRuntimeServiceProvider driver token throws when multiple drivers are registered", () => {
148
- const app = createSingletonApp();
149
- app.instance("runtime.database.driver.mysql", Object.freeze({ DIALECT_ID: "mysql2" }));
150
- app.instance("runtime.database.driver.postgres", Object.freeze({ DIALECT_ID: "pg" }));
151
-
152
- const provider = new DatabaseRuntimeServiceProvider();
153
- provider.register(app);
154
-
155
- assert.throws(() => app.make("runtime.database.driver"), /Multiple database drivers are registered\./);
156
- });
157
-
158
- test("DatabaseRuntimeServiceProvider resolves knex from app root package context", async () => {
74
+ test("DatabaseProvider exposes one cohesive database capability", async () => {
159
75
  await withAppRootKnexStub(async () => {
160
- const app = createSingletonApp();
161
- app.instance("runtime.database.driver.mysql", Object.freeze({ DIALECT_ID: "mysql2" }));
162
- app.instance("jskit.env", {
163
- DB_HOST: "db.local",
164
- DB_PORT: "3307",
165
- DB_NAME: "appdb",
166
- DB_USER: "appuser",
167
- DB_PASSWORD: "apppass"
76
+ let database;
77
+ const runtime = createCapabilityRuntime({
78
+ inputs: {
79
+ "runtime.env": {
80
+ DB_HOST: "db.local",
81
+ DB_PORT: "3307",
82
+ DB_NAME: "appdb",
83
+ DB_USER: "appuser",
84
+ DB_PASSWORD: "apppass"
85
+ }
86
+ },
87
+ providers: [
88
+ driverProvider(),
89
+ DatabaseProvider,
90
+ databaseConsumer((value) => {
91
+ database = value;
92
+ })
93
+ ]
168
94
  });
169
95
 
170
- const provider = new DatabaseRuntimeServiceProvider();
171
- provider.register(app);
96
+ await runtime.start();
97
+
98
+ assert.equal(database.driver.DIALECT_ID, "mysql2");
99
+ assert.equal(database.knex.__source, "app-root-knex");
100
+ assert.equal(database.knex.__config.client, "mysql2");
101
+ assert.deepEqual(database.knex.__config.connection, {
102
+ host: "db.local",
103
+ port: 3307,
104
+ database: "appdb",
105
+ user: "appuser",
106
+ password: "apppass",
107
+ supportBigNumbers: true,
108
+ bigNumberStrings: true,
109
+ dateStrings: ["DATE"],
110
+ timezone: "Z"
111
+ });
112
+ assert.equal(typeof database.transactionManager.inTransaction, "function");
113
+ assert.equal(typeof database.resolveRepoClient, "function");
172
114
 
173
- const knex = app.make("jskit.database.knex");
174
- assert.equal(knex.__source, "app-root-knex");
175
- assert.equal(knex.__config.client, "mysql2");
176
- assert.equal(knex.__config.connection.host, "db.local");
177
- assert.equal(knex.__config.connection.port, 3307);
178
- assert.equal(knex.__config.connection.database, "appdb");
179
- assert.equal(knex.__config.connection.user, "appuser");
180
- assert.equal(knex.__config.connection.password, "apppass");
181
- assert.deepEqual(knex.__config.connection.dateStrings, ["DATE"]);
115
+ await runtime.shutdown();
116
+ assert.equal(database.knex.destroyCalls, 1);
117
+ assert.deepEqual(await runtime.shutdown(), []);
118
+ assert.equal(database.knex.destroyCalls, 1);
182
119
  });
183
120
  });
184
121
 
185
- test("DatabaseRuntimeServiceProvider resolves knex config from DATABASE_URL when DB_* vars are omitted", async () => {
122
+ test("DatabaseProvider accepts DATABASE_URL without redundant DB_CLIENT", async () => {
186
123
  await withAppRootKnexStub(async () => {
187
- const app = createSingletonApp();
188
- app.instance("runtime.database.driver.mysql", Object.freeze({ DIALECT_ID: "mysql2" }));
189
- app.instance("jskit.env", {
190
- DATABASE_URL: "mysql://urluser:urlpass@db.url.local:3308/url_db_name"
124
+ let database;
125
+ const runtime = createCapabilityRuntime({
126
+ inputs: {
127
+ "runtime.env": {
128
+ DATABASE_URL: "mysql://urluser:urlpass@db.url.local:3308/url_db_name"
129
+ }
130
+ },
131
+ providers: [driverProvider(), DatabaseProvider, databaseConsumer((value) => { database = value; })]
191
132
  });
192
133
 
193
- const provider = new DatabaseRuntimeServiceProvider();
194
- provider.register(app);
195
-
196
- const knex = app.make("jskit.database.knex");
197
- assert.equal(knex.__source, "app-root-knex");
198
- assert.equal(knex.__config.client, "mysql2");
199
- assert.equal(knex.__config.connection.host, "db.url.local");
200
- assert.equal(knex.__config.connection.port, 3308);
201
- assert.equal(knex.__config.connection.database, "url_db_name");
202
- assert.equal(knex.__config.connection.user, "urluser");
203
- assert.equal(knex.__config.connection.password, "urlpass");
134
+ await runtime.start();
135
+ assert.equal(database.knex.__config.connection.host, "db.url.local");
136
+ assert.equal(database.knex.__config.connection.port, 3308);
137
+ assert.equal(database.knex.__config.connection.database, "url_db_name");
138
+ assert.equal(database.knex.__config.connection.user, "urluser");
139
+ assert.equal(database.knex.__config.connection.password, "urlpass");
140
+ await runtime.shutdown();
204
141
  });
205
142
  });
206
143
 
207
- test("DatabaseRuntimeServiceProvider destroys resolved knex during shutdown", async () => {
208
- const app = createSingletonApp();
209
- const knex = createKnexStub();
210
- app.instance("jskit.database.knex", knex);
211
-
212
- const provider = new DatabaseRuntimeServiceProvider();
213
- provider.register(app);
214
-
215
- await provider.shutdown(app);
216
- assert.equal(knex.destroyCalls, 1);
144
+ test("DatabaseProvider rejects environment dialect mismatch", async () => {
145
+ await withAppRootKnexStub(async () => {
146
+ const runtime = createCapabilityRuntime({
147
+ inputs: {
148
+ "runtime.env": {
149
+ DB_CLIENT: "pg",
150
+ DB_NAME: "appdb",
151
+ DB_USER: "appuser"
152
+ }
153
+ },
154
+ providers: [driverProvider(), DatabaseProvider]
155
+ });
217
156
 
218
- await provider.shutdown(app);
219
- assert.equal(knex.destroyCalls, 1);
157
+ await assert.rejects(
158
+ runtime.start(),
159
+ /DB_CLIENT="pg" does not match installed database driver "mysql2"\./
160
+ );
161
+ });
220
162
  });
@@ -1,12 +1,29 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import {
4
+ lockScopedRecordId,
4
5
  normalizeDbRecordId,
5
6
  parseMetadataJson,
6
7
  resolveInsertedRecordId,
7
8
  stringifyMetadataJson
8
9
  } from "../src/shared/repositoryOptions.js";
9
10
 
11
+ function fakeLockTransaction(row = null) {
12
+ const calls = [];
13
+ const query = {
14
+ select(value) { calls.push(["select", value]); return this; },
15
+ where(key, value) { calls.push(["where", key, value]); return this; },
16
+ andWhere(key, value) { calls.push(["andWhere", key, value]); return this; },
17
+ forUpdate() { calls.push(["forUpdate"]); return this; },
18
+ async first() { calls.push(["first"]); return row; }
19
+ };
20
+ const trx = (table) => {
21
+ calls.push(["table", table]);
22
+ return query;
23
+ };
24
+ return { calls, trx };
25
+ }
26
+
10
27
  test("parseMetadataJson parses object-like metadata payloads", () => {
11
28
  assert.deepEqual(parseMetadataJson(""), {});
12
29
  assert.deepEqual(parseMetadataJson("{"), {});
@@ -40,3 +57,33 @@ test("resolveInsertedRecordId normalizes insert ids without accepting unsafe JS
40
57
  assert.equal(resolveInsertedRecordId([42]), "42");
41
58
  assert.equal(resolveInsertedRecordId([unsafeNumericId]), null);
42
59
  });
60
+
61
+ test("lockScopedRecordId locks an exact record inside its owner scope", async () => {
62
+ const { calls, trx } = fakeLockTransaction({ id: "42" });
63
+ assert.equal(await lockScopedRecordId({
64
+ trx,
65
+ tableName: "daycare_events",
66
+ recordId: 42,
67
+ scopeId: 7
68
+ }), "42");
69
+ assert.deepEqual(calls, [
70
+ ["table", "daycare_events"],
71
+ ["select", "id"],
72
+ ["where", "id", "42"],
73
+ ["andWhere", "workspace_id", "7"],
74
+ ["forUpdate"],
75
+ ["first"]
76
+ ]);
77
+ });
78
+
79
+ test("lockScopedRecordId rejects missing transactions and unsafe identifiers", async () => {
80
+ await assert.rejects(
81
+ () => lockScopedRecordId({ tableName: "records", recordId: 1, scopeId: 1 }),
82
+ /requires a transaction/u
83
+ );
84
+ const { trx } = fakeLockTransaction();
85
+ await assert.rejects(
86
+ () => lockScopedRecordId({ trx, tableName: "records; drop table", recordId: 1, scopeId: 1 }),
87
+ /safe SQL identifier/u
88
+ );
89
+ });
@@ -5,8 +5,7 @@ import {
5
5
  BaseRepository,
6
6
  buildPaginationMeta,
7
7
  createTransactionManager,
8
- createWithTransaction,
9
- registerDatabaseRuntime,
8
+ createWithTransaction
10
9
  } from "../src/shared/index.js";
11
10
 
12
11
  function createKnexStub() {
@@ -17,48 +16,6 @@ function createKnexStub() {
17
16
  };
18
17
  }
19
18
 
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
19
  test("transaction manager wraps callback in knex transaction", async () => {
63
20
  const manager = createTransactionManager({ knex: createKnexStub() });
64
21
  const result = await manager.inTransaction(async (trx) => ({ ok: true, trx }));
@@ -95,13 +52,3 @@ test("pagination helpers generate stable metadata", () => {
95
52
  hasNext: true
96
53
  });
97
54
  });
98
-
99
- test("registerDatabaseRuntime binds knex and transaction manager tokens", () => {
100
- const app = createSingletonApp();
101
- const knex = createKnexStub();
102
-
103
- const runtime = registerDatabaseRuntime(app, { knex });
104
- assert.strictEqual(runtime.knex, knex);
105
- assert.equal(typeof runtime.transactionManager.inTransaction, "function");
106
- assert.strictEqual(app.make("jskit.database.knex"), knex);
107
- });