@jskit-ai/database-runtime 0.1.244 → 0.1.246

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 CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@jskit-ai/database-runtime",
3
- "version": "0.1.244",
3
+ "version": "0.1.246",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
7
7
  },
8
8
  "exports": {
9
- "./client": "./src/client/index.js",
10
9
  "./server/providers/DatabaseProvider": "./src/server/providers/DatabaseProvider.js",
11
10
  "./server/databaseSetup": "./src/server/databaseSetup.js",
12
11
  "./server/knexMigrationConfig": "./src/server/knexMigrationConfig.js",
@@ -15,15 +14,8 @@
15
14
  "./shared/databaseClient": "./src/shared/databaseClient.js",
16
15
  "./shared/dialect": "./src/shared/dialect.js",
17
16
  "./shared/duplicateEntry": "./src/shared/duplicateEntry.js",
18
- "./shared/json": "./src/shared/json.js",
19
- "./shared/repository": "./src/shared/repository.js",
20
- "./shared/repositoryScope": "./src/shared/repositoryScope.js",
21
17
  "./shared/repositoryOptions": "./src/shared/repositoryOptions.js",
22
- "./shared/visibility": "./src/shared/visibility.js",
23
- "./shared/retention": "./src/shared/retention.js",
24
- "./shared/runtimeErrors": "./src/shared/runtimeErrors.js",
25
- "./shared/transactionManager": "./src/shared/transactionManager.js",
26
- "./shared/transactions": "./src/shared/transactions.js"
18
+ "./shared/visibility": "./src/shared/visibility.js"
27
19
  },
28
20
  "dependencies": {
29
21
  "knex": "^3.1.0"
@@ -55,8 +47,8 @@
55
47
  "apiSummary": {
56
48
  "surfaces": [
57
49
  {
58
- "subpath": "./server",
59
- "summary": "Provides the cohesive runtime.database capability with Knex, transactions, the selected driver, and repository utilities."
50
+ "subpath": "./server/providers/DatabaseProvider",
51
+ "summary": "Provides the runtime.database capability: the selected driver, its Knex client and a transaction manager."
60
52
  },
61
53
  {
62
54
  "subpath": "./server/databaseSetup",
@@ -68,17 +60,13 @@
68
60
  },
69
61
  {
70
62
  "subpath": "./shared",
71
- "summary": "Exports shared Knex runtime utilities (transaction manager, repository helpers, retention/json/date/dialect helpers)."
72
- },
73
- {
74
- "subpath": "./client",
75
- "summary": "Exports no runtime API today (reserved client entrypoint)."
63
+ "summary": "Exports shared repository helpers (transactions, record ids, JSON columns, visibility, duplicate-entry detection and date/time serialization)."
76
64
  }
77
65
  ]
78
66
  }
79
67
  }
80
68
  },
81
69
  "peerDependencies": {
82
- "@jskit-ai/kernel": "0.1.244"
70
+ "@jskit-ai/kernel": "0.1.246"
83
71
  }
84
72
  }
@@ -1,5 +1,4 @@
1
1
  import { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
2
- import * as databaseUtilities from "../../shared/index.js";
3
2
  import { normalizeDatabaseClient, toKnexClientId } from "../../shared/databaseClient.js";
4
3
  import { resolveKnexConnectionFromEnvironment } from "../../shared/databaseConnection.js";
5
4
  import { createTransactionManager } from "../../shared/transactionManager.js";
@@ -34,7 +33,6 @@ function createDatabase({ driver, env }) {
34
33
  const transactionManager = createTransactionManager({ knex });
35
34
 
36
35
  return Object.freeze({
37
- ...databaseUtilities,
38
36
  driver,
39
37
  knex,
40
38
  transactionManager
@@ -25,7 +25,6 @@ function toKnexClientId(databaseClient) {
25
25
  }
26
26
 
27
27
  export {
28
- normalizeText,
29
28
  normalizeDatabaseClient,
30
29
  toKnexClientId
31
30
  };
@@ -1,4 +1,5 @@
1
- import { normalizeDatabaseClient, normalizeText } from "./databaseClient.js";
1
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import { normalizeDatabaseClient } from "./databaseClient.js";
2
3
 
3
4
  const DATABASE_URL_PROTOCOL_TO_CLIENT = Object.freeze({
4
5
  "mysql:": "mysql2",
@@ -203,14 +203,6 @@ function toInsertDateTime(dateLike, fallback = new Date()) {
203
203
  return toDatabaseDateTimeUtc(normalized);
204
204
  }
205
205
 
206
- function toNullableDateTime(value) {
207
- const normalized = normalizeDateInput(value);
208
- if (!normalized) {
209
- return null;
210
- }
211
- return toDatabaseDateTimeUtc(normalized);
212
- }
213
-
214
206
  function toDatabaseDateTimeUtc(value) {
215
207
  if (value == null) {
216
208
  return null;
@@ -222,6 +214,9 @@ function toDatabaseDateTimeUtc(value) {
222
214
 
223
215
  const date = toDateOrThrow(value);
224
216
  const year = date.getUTCFullYear();
217
+ if (year < 0 || year > 9999) {
218
+ throw new TypeError("Invalid date-time value.");
219
+ }
225
220
  const month = pad(date.getUTCMonth() + 1);
226
221
  const day = pad(date.getUTCDate());
227
222
  const hours = pad(date.getUTCHours());
@@ -229,14 +224,12 @@ function toDatabaseDateTimeUtc(value) {
229
224
  const seconds = pad(date.getUTCSeconds());
230
225
  const milliseconds = pad(date.getUTCMilliseconds(), 3);
231
226
 
232
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
227
+ return `${pad(year, 4)}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
233
228
  }
234
229
 
235
230
  export {
236
- normalizeDateInput,
237
231
  toIsoString,
238
232
  toInsertDateTime,
239
- toNullableDateTime,
240
233
  toDatabaseDateTimeUtc,
241
234
  toJsonDate,
242
235
  toJsonTime,
@@ -1,58 +1,25 @@
1
1
  export {
2
- DatabaseRuntimeError,
3
- TransactionManagerError,
4
- RepositoryError
5
- } from "./runtimeErrors.js";
6
- export { TransactionManager, createTransactionManager } from "./transactionManager.js";
7
- export { BaseRepository, buildPaginationMeta } from "./repository.js";
8
- export {
9
- normalizeDateInput,
10
2
  toIsoString,
11
3
  toInsertDateTime,
12
- toNullableDateTime,
13
4
  toDatabaseDateTimeUtc,
14
5
  toJsonDate,
15
6
  toJsonTime,
16
7
  toJsonDateTime
17
8
  } from "./dateUtils.js";
18
9
  export { normalizeDialect, detectDialectFromClient } from "./dialect.js";
19
- export { normalizeText, normalizeDatabaseClient, toKnexClientId } from "./databaseClient.js";
20
- export {
21
- parseDatabaseUrl,
22
- resolveDatabaseClientFromEnvironment,
23
- resolveDatabaseConnectionFromEnvironment,
24
- resolveKnexConnectionFromEnvironment
25
- } from "./databaseConnection.js";
10
+ export { normalizeDatabaseClient, toKnexClientId } from "./databaseClient.js";
26
11
  export { findDuplicateEntryError, isDuplicateEntryError } from "./duplicateEntry.js";
27
- export { normalizePath, jsonTextExpression, whereJsonTextEquals } from "./json.js";
28
12
  export { DEFAULT_VISIBILITY_COLUMNS, applyVisibility, applyVisibilityOwners } from "./visibility.js";
29
- export { createRepositoryScope } from "./repositoryScope.js";
30
13
  export {
31
14
  resolveQueryOptions,
32
15
  resolveRepoClient,
33
16
  applyForUpdate,
34
- mapRowNullable,
35
17
  parseJsonObject,
36
18
  stringifyJsonObject,
37
- parseMetadataJson,
38
- stringifyMetadataJson,
39
- normalizeMetadataJsonInput,
40
19
  normalizeNullableString,
41
20
  normalizeDbRecordId,
42
21
  resolveInsertedRecordId,
43
- normalizeIdList,
44
- normalizeCountRow,
45
- parseJsonValue,
46
22
  toDbJson,
47
23
  runInTransaction,
48
- createWithTransaction,
49
- lockScopedRecordId
24
+ createWithTransaction
50
25
  } from "./repositoryOptions.js";
51
- export {
52
- normalizeBatchSize,
53
- normalizeCutoffDateOrThrow,
54
- normalizeDeletedRowCount,
55
- deleteRowsOlderThan,
56
- __testables as retentionTestables
57
- } from "./retention.js";
58
- export { createRepoTransaction } from "./transactions.js";
@@ -28,19 +28,6 @@ function applyForUpdate(query, options = {}) {
28
28
  return query;
29
29
  }
30
30
 
31
- function mapRowNullable(mapper) {
32
- if (typeof mapper !== "function") {
33
- throw new TypeError("mapRowNullable requires a mapper function.");
34
- }
35
-
36
- return function mapNullableRow(row) {
37
- if (!row) {
38
- return null;
39
- }
40
- return mapper(row);
41
- };
42
- }
43
-
44
31
  function parseJsonObject(value, fallback = {}) {
45
32
  const source = String(value || "").trim();
46
33
  if (!source) {
@@ -71,47 +58,6 @@ function stringifyJsonObject(value, fallback = "{}") {
71
58
  }
72
59
  }
73
60
 
74
- function parseMetadataJson(value, fallback = {}) {
75
- const source = String(value || "").trim();
76
- if (!source) {
77
- return fallback;
78
- }
79
-
80
- try {
81
- const parsed = JSON.parse(source);
82
- return parsed && typeof parsed === "object" ? parsed : fallback;
83
- } catch {
84
- return fallback;
85
- }
86
- }
87
-
88
- function stringifyMetadataJson(metadata, fallback = "{}") {
89
- if (!metadata || typeof metadata !== "object") {
90
- return fallback;
91
- }
92
-
93
- try {
94
- return JSON.stringify(metadata);
95
- } catch {
96
- return fallback;
97
- }
98
- }
99
-
100
- function normalizeMetadataJsonInput(value, fallback = null) {
101
- if (value == null) {
102
- return fallback;
103
- }
104
- if (typeof value === "string") {
105
- return value;
106
- }
107
-
108
- try {
109
- return JSON.stringify(value);
110
- } catch {
111
- return fallback;
112
- }
113
- }
114
-
115
61
  function normalizeNullableString(value, { trim = true } = {}) {
116
62
  if (value == null) {
117
63
  return null;
@@ -160,58 +106,6 @@ function resolveInsertedRecordId(insertResult, { fallback = null } = {}) {
160
106
  return normalizeDbRecordId(insertResult, { fallback });
161
107
  }
162
108
 
163
- function normalizeIdList(values, { parseValue } = {}) {
164
- const source = Array.isArray(values) ? values : [];
165
- const parser = typeof parseValue === "function" ? parseValue : (value) => value;
166
- const normalized = source.map((value) => parser(value)).filter(Boolean);
167
- return Array.from(new Set(normalized));
168
- }
169
-
170
- function normalizeCountRow(row) {
171
- const values = Object.values(row || {});
172
- if (values.length < 1) {
173
- return 0;
174
- }
175
-
176
- const parsed = Number(values[0]);
177
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
178
- }
179
-
180
- function parseJsonValue(value, fallback = null, options = {}) {
181
- const source = options && typeof options === "object" ? options : {};
182
- const effectiveFallback = Object.hasOwn(source, "fallback") ? source.fallback : fallback;
183
- const allowNull = source.allowNull === true;
184
- const allowObject = source.allowObject !== false;
185
- const trim = source.trim !== false;
186
-
187
- if (value == null) {
188
- return effectiveFallback;
189
- }
190
-
191
- if (allowObject && typeof value === "object") {
192
- return value;
193
- }
194
-
195
- let text = String(value || "");
196
- if (trim) {
197
- text = text.trim();
198
- }
199
-
200
- if (!text) {
201
- return effectiveFallback;
202
- }
203
-
204
- try {
205
- const parsed = JSON.parse(text);
206
- if (parsed == null && !allowNull) {
207
- return effectiveFallback;
208
- }
209
- return parsed;
210
- } catch {
211
- return effectiveFallback;
212
- }
213
- }
214
-
215
109
  function toDbJson(value) {
216
110
  if (value == null) {
217
111
  return JSON.stringify({});
@@ -242,60 +136,16 @@ function createWithTransaction(knex) {
242
136
  };
243
137
  }
244
138
 
245
- function requireSqlIdentifier(value, label) {
246
- const identifier = String(value || "").trim();
247
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(identifier)) {
248
- throw new TypeError(`${label} must be a safe SQL identifier.`);
249
- }
250
- return identifier;
251
- }
252
-
253
- async function lockScopedRecordId({
254
- trx,
255
- tableName,
256
- recordId,
257
- scopeId,
258
- idColumn = "id",
259
- scopeColumn = "workspace_id"
260
- } = {}) {
261
- if (typeof trx !== "function") {
262
- throw new TypeError("lockScopedRecordId requires a transaction.");
263
- }
264
- const table = requireSqlIdentifier(tableName, "lockScopedRecordId tableName");
265
- const recordKey = requireSqlIdentifier(idColumn, "lockScopedRecordId idColumn");
266
- const scopeKey = requireSqlIdentifier(scopeColumn, "lockScopedRecordId scopeColumn");
267
- const normalizedRecordId = normalizeDbRecordId(recordId);
268
- const normalizedScopeId = normalizeDbRecordId(scopeId);
269
- if (!normalizedRecordId || !normalizedScopeId) {
270
- return null;
271
- }
272
- const row = await trx(table)
273
- .select(recordKey)
274
- .where(recordKey, normalizedRecordId)
275
- .andWhere(scopeKey, normalizedScopeId)
276
- .forUpdate()
277
- .first();
278
- return normalizeDbRecordId(row?.[recordKey]);
279
- }
280
-
281
139
  export {
282
140
  resolveQueryOptions,
283
141
  resolveRepoClient,
284
142
  applyForUpdate,
285
- mapRowNullable,
286
143
  parseJsonObject,
287
144
  stringifyJsonObject,
288
- parseMetadataJson,
289
- stringifyMetadataJson,
290
- normalizeMetadataJsonInput,
291
145
  normalizeNullableString,
292
146
  normalizeDbRecordId,
293
147
  resolveInsertedRecordId,
294
- normalizeIdList,
295
- normalizeCountRow,
296
- parseJsonValue,
297
148
  toDbJson,
298
149
  runInTransaction,
299
- createWithTransaction,
300
- lockScopedRecordId
150
+ createWithTransaction
301
151
  };
@@ -1,10 +1,9 @@
1
1
  import { normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
2
- import { TransactionManagerError } from "./runtimeErrors.js";
3
2
 
4
3
  class TransactionManager {
5
4
  constructor({ knex } = {}) {
6
5
  if (!knex || typeof knex.transaction !== "function") {
7
- throw new TransactionManagerError("TransactionManager requires knex with transaction().");
6
+ throw new TypeError("TransactionManager requires knex with transaction().");
8
7
  }
9
8
 
10
9
  this.knex = knex;
@@ -12,7 +11,7 @@ class TransactionManager {
12
11
 
13
12
  async inTransaction(work, { trx = null } = {}) {
14
13
  if (typeof work !== "function") {
15
- throw new TransactionManagerError("inTransaction requires a callback function.");
14
+ throw new TypeError("inTransaction requires a callback function.");
16
15
  }
17
16
 
18
17
  if (trx) {
@@ -61,10 +61,10 @@ function applyVisibilityOwners(payload = {}, visibilityContext = {}) {
61
61
  };
62
62
 
63
63
  if (context.scopeOwnerId) {
64
- ownedPayload.workspace_id = context.scopeOwnerId;
64
+ ownedPayload[DEFAULT_VISIBILITY_COLUMNS.scopeOwnerId] = context.scopeOwnerId;
65
65
  }
66
66
  if (context.userId) {
67
- ownedPayload.user_id = context.userId;
67
+ ownedPayload[DEFAULT_VISIBILITY_COLUMNS.userId] = context.userId;
68
68
  }
69
69
 
70
70
  return ownedPayload;
@@ -18,6 +18,13 @@ test("toDatabaseDateTimeUtc formats DATETIME(3) UTC string", () => {
18
18
  assert.equal(toDatabaseDateTimeUtc(new Date("2024-01-01T01:02:03.045Z")), "2024-01-01 01:02:03.045");
19
19
  });
20
20
 
21
+ test("toDatabaseDateTimeUtc pads years below 1000 and rejects years outside 0..9999", () => {
22
+ assert.equal(toDatabaseDateTimeUtc("0999-01-01T00:00:00.000Z"), "0999-01-01 00:00:00.000");
23
+ assert.equal(toDatabaseDateTimeUtc(new Date("0005-01-01T00:00:00.000Z")), "0005-01-01 00:00:00.000");
24
+ assert.throws(() => toDatabaseDateTimeUtc(new Date("+010000-01-01T00:00:00.000Z")), /Invalid date-time value/);
25
+ assert.throws(() => toDatabaseDateTimeUtc(new Date("-000001-01-01T00:00:00.000Z")), /Invalid date-time value/);
26
+ });
27
+
21
28
  test("toDatabaseDateTimeUtc preserves nullable empty values", () => {
22
29
  assert.equal(toDatabaseDateTimeUtc(null), null);
23
30
  assert.equal(toDatabaseDateTimeUtc(undefined), null);
@@ -2,7 +2,6 @@ import assert from "node:assert/strict";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import test from "node:test";
4
4
 
5
- import * as clientApi from "../src/client/index.js";
6
5
  import * as sharedApi from "../src/shared/index.js";
7
6
  import { DatabaseProvider } from "../src/server/providers/DatabaseProvider.js";
8
7
 
@@ -10,15 +9,15 @@ test("package exports include explicit shared and provider entrypoints", async (
10
9
  const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
11
10
  const exportsMap = packageJson && typeof packageJson === "object" ? packageJson.exports : {};
12
11
  assert.equal(exportsMap["./server"], undefined);
12
+ assert.equal(exportsMap["./client"], undefined);
13
13
  assert.equal(
14
14
  exportsMap["./server/providers/DatabaseProvider"],
15
15
  "./src/server/providers/DatabaseProvider.js"
16
16
  );
17
17
  assert.equal(exportsMap["./shared"], "./src/shared/index.js");
18
- });
19
-
20
- test("client entrypoint exports no database runtime api", () => {
21
- assert.deepEqual(Object.keys(clientApi), []);
18
+ for (const surface of packageJson.jskit.metadata.apiSummary.surfaces) {
19
+ assert.ok(exportsMap[surface.subpath], `apiSummary names a real export: ${surface.subpath}`);
20
+ }
22
21
  });
23
22
 
24
23
  test("server provider module exports one declarative provider", () => {
@@ -27,8 +26,11 @@ test("server provider module exports one declarative provider", () => {
27
26
  });
28
27
 
29
28
  test("shared entrypoint exports shared database utilities only", () => {
30
- assert.equal(typeof sharedApi.createTransactionManager, "function");
29
+ assert.equal(typeof sharedApi.createWithTransaction, "function");
31
30
  assert.equal(typeof sharedApi.isDuplicateEntryError, "function");
32
- assert.equal(typeof sharedApi.createRepositoryScope, "function");
31
+ assert.equal(typeof sharedApi.toDatabaseDateTimeUtc, "function");
33
32
  assert.equal(typeof sharedApi.DatabaseProvider, "undefined");
33
+ for (const removed of ["normalizeText", "retentionTestables", "createTransactionManager", "createRepositoryScope", "BaseRepository"]) {
34
+ assert.equal(typeof sharedApi[removed], "undefined", removed);
35
+ }
34
36
  });
@@ -110,7 +110,7 @@ test("DatabaseProvider exposes one cohesive database capability", async () => {
110
110
  timezone: "Z"
111
111
  });
112
112
  assert.equal(typeof database.transactionManager.inTransaction, "function");
113
- assert.equal(typeof database.resolveRepoClient, "function");
113
+ assert.deepEqual(Object.keys(database).sort(), ["driver", "knex", "transactionManager"]);
114
114
 
115
115
  await runtime.shutdown();
116
116
  assert.equal(database.knex.destroyCalls, 1);
@@ -1,48 +1,10 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import {
4
- lockScopedRecordId,
5
4
  normalizeDbRecordId,
6
- parseMetadataJson,
7
- resolveInsertedRecordId,
8
- stringifyMetadataJson
5
+ resolveInsertedRecordId
9
6
  } from "../src/shared/repositoryOptions.js";
10
7
 
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
-
27
- test("parseMetadataJson parses object-like metadata payloads", () => {
28
- assert.deepEqual(parseMetadataJson(""), {});
29
- assert.deepEqual(parseMetadataJson("{"), {});
30
- assert.deepEqual(parseMetadataJson("{\"source\":\"console\"}"), { source: "console" });
31
- assert.deepEqual(parseMetadataJson("[1,2,3]"), [1, 2, 3]);
32
- });
33
-
34
- test("stringifyMetadataJson serializes metadata payloads", () => {
35
- assert.equal(stringifyMetadataJson(null), "{}");
36
- assert.equal(stringifyMetadataJson({ source: "console" }), "{\"source\":\"console\"}");
37
- assert.equal(stringifyMetadataJson([1, 2, 3]), "[1,2,3]");
38
- });
39
-
40
- test("stringifyMetadataJson returns fallback for non-serializable values", () => {
41
- const circular = {};
42
- circular.self = circular;
43
- assert.equal(stringifyMetadataJson(circular), "{}");
44
- });
45
-
46
8
  test("normalizeDbRecordId preserves canonical DB ids and rejects unsafe JS numbers", () => {
47
9
  const unsafeNumericId = Number(9007199254740993n);
48
10
  assert.equal(normalizeDbRecordId("9007199254740993"), "9007199254740993");
@@ -57,33 +19,3 @@ test("resolveInsertedRecordId normalizes insert ids without accepting unsafe JS
57
19
  assert.equal(resolveInsertedRecordId([42]), "42");
58
20
  assert.equal(resolveInsertedRecordId([unsafeNumericId]), null);
59
21
  });
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
- });
@@ -1,12 +1,8 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
 
4
- import {
5
- BaseRepository,
6
- buildPaginationMeta,
7
- createTransactionManager,
8
- createWithTransaction
9
- } from "../src/shared/index.js";
4
+ import { createWithTransaction } from "../src/shared/index.js";
5
+ import { createTransactionManager } from "../src/shared/transactionManager.js";
10
6
 
11
7
  function createKnexStub() {
12
8
  return {
@@ -23,15 +19,6 @@ test("transaction manager wraps callback in knex transaction", async () => {
23
19
  assert.equal(result.trx.trxId, "trx-1");
24
20
  });
25
21
 
26
- test("base repository withTransaction delegates to transaction manager", async () => {
27
- const knex = createKnexStub();
28
- const transactionManager = createTransactionManager({ knex });
29
- const repo = new BaseRepository({ knex, transactionManager });
30
-
31
- const result = await repo.withTransaction(async (trx) => ({ id: trx.trxId }));
32
- assert.deepEqual(result, { id: "trx-1" });
33
- });
34
-
35
22
  test("createWithTransaction creates a reusable withTransaction function", async () => {
36
23
  const knex = Object.assign(() => {
37
24
  throw new Error("query execution not expected");
@@ -40,15 +27,3 @@ test("createWithTransaction creates a reusable withTransaction function", async
40
27
  const result = await withTransaction(async (trx) => ({ id: trx.trxId }));
41
28
  assert.deepEqual(result, { id: "trx-1" });
42
29
  });
43
-
44
- test("pagination helpers generate stable metadata", () => {
45
- const meta = buildPaginationMeta({ total: 51, page: 2, pageSize: 25 });
46
- assert.deepEqual(meta, {
47
- total: 51,
48
- page: 2,
49
- pageSize: 25,
50
- pageCount: 3,
51
- hasPrev: true,
52
- hasNext: true
53
- });
54
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,41 +0,0 @@
1
- import { detectDialectFromClient } from "./dialect.js";
2
-
3
- function normalizePath(path) {
4
- if (Array.isArray(path)) {
5
- return path.map((part) => String(part || "").trim()).filter(Boolean);
6
- }
7
-
8
- const source = String(path || "").trim();
9
- if (!source) {
10
- return [];
11
- }
12
-
13
- return source
14
- .split(".")
15
- .map((part) => String(part || "").trim())
16
- .filter(Boolean);
17
- }
18
-
19
- function jsonTextExpression({ client, column, path }) {
20
- const normalizedPath = normalizePath(path);
21
- if (!column || normalizedPath.length < 1) {
22
- throw new TypeError("jsonTextExpression requires column and path.");
23
- }
24
-
25
- const dialect = detectDialectFromClient(client);
26
- if (dialect === "postgres") {
27
- const pgPath = normalizedPath.map((part) => part.replace(/[{}]/g, "")).join(",");
28
- return client.raw("?? #>> ?", [column, `{${pgPath}}`]);
29
- }
30
-
31
- const mysqlPath = `$.${normalizedPath.join(".")}`;
32
- return client.raw("JSON_UNQUOTE(JSON_EXTRACT(??, ?))", [column, mysqlPath]);
33
- }
34
-
35
- function whereJsonTextEquals(query, { column, path, value }) {
36
- const expression = jsonTextExpression({ client: query.client, column, path });
37
- query.whereRaw(`${expression.sql} = ?`, [...expression.bindings, String(value || "")]);
38
- return query;
39
- }
40
-
41
- export { normalizePath, jsonTextExpression, whereJsonTextEquals };
@@ -1,61 +0,0 @@
1
- import { normalizeInteger, normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
2
- import { RepositoryError } from "./runtimeErrors.js";
3
-
4
- function buildPaginationMeta({ total, page, pageSize } = {}) {
5
- const normalizedTotal = Math.max(0, normalizeInteger(total, { fallback: 0 }));
6
- const normalizedPageSize = Math.max(1, normalizeInteger(pageSize, { fallback: 25 }));
7
- const normalizedPage = Math.max(1, normalizeInteger(page, { fallback: 1 }));
8
- const pageCount = Math.max(1, Math.ceil(normalizedTotal / normalizedPageSize));
9
-
10
- return Object.freeze({
11
- total: normalizedTotal,
12
- page: Math.min(normalizedPage, pageCount),
13
- pageSize: normalizedPageSize,
14
- pageCount,
15
- hasPrev: normalizedPage > 1,
16
- hasNext: normalizedPage < pageCount
17
- });
18
- }
19
-
20
- class BaseRepository {
21
- constructor({ knex, transactionManager } = {}) {
22
- if (!knex) {
23
- throw new RepositoryError("BaseRepository requires knex.");
24
- }
25
- if (!transactionManager || typeof transactionManager.inTransaction !== "function") {
26
- throw new RepositoryError("BaseRepository requires transactionManager.inTransaction().");
27
- }
28
-
29
- this.knex = knex;
30
- this.transactionManager = transactionManager;
31
- }
32
-
33
- async withTransaction(work, options = {}) {
34
- if (typeof work !== "function") {
35
- throw new RepositoryError("withTransaction requires a callback.");
36
- }
37
-
38
- return this.transactionManager.inTransaction(work, normalizeObject(options));
39
- }
40
-
41
- paginateRows(rows = [], options = {}) {
42
- const sourceRows = Array.isArray(rows) ? rows : [];
43
- const pageSize = Math.max(1, normalizeInteger(options.pageSize, { fallback: 25 }));
44
- const page = Math.max(1, normalizeInteger(options.page, { fallback: 1 }));
45
- const meta = buildPaginationMeta({
46
- total: sourceRows.length,
47
- page,
48
- pageSize
49
- });
50
-
51
- const start = (meta.page - 1) * meta.pageSize;
52
- const end = start + meta.pageSize;
53
-
54
- return Object.freeze({
55
- rows: Object.freeze(sourceRows.slice(start, end)),
56
- meta
57
- });
58
- }
59
- }
60
-
61
- export { BaseRepository, buildPaginationMeta };
@@ -1,62 +0,0 @@
1
- import { resolveRepoClient } from "./repositoryOptions.js";
2
- import { applyVisibility, applyVisibilityOwners } from "./visibility.js";
3
-
4
- function normalizeTableName(value) {
5
- const normalized = String(value || "").trim();
6
- if (!normalized) {
7
- throw new TypeError("createRepositoryScope requires tableName.");
8
- }
9
- return normalized;
10
- }
11
-
12
- function normalizeIdColumn(value) {
13
- const normalized = String(value || "").trim();
14
- return normalized || "id";
15
- }
16
-
17
- function createRepositoryScope(knex, tableName, options = {}) {
18
- if (typeof knex !== "function") {
19
- throw new TypeError("createRepositoryScope requires knex.");
20
- }
21
-
22
- const source = options && typeof options === "object" && !Array.isArray(options) ? options : {};
23
- const resolvedTableName = normalizeTableName(tableName);
24
- const resolvedIdColumn = normalizeIdColumn(source.idColumn);
25
-
26
- function clientOf(queryOptions = {}) {
27
- return resolveRepoClient(knex, queryOptions);
28
- }
29
-
30
- function table(queryOptions = {}) {
31
- return clientOf(queryOptions)(resolvedTableName);
32
- }
33
-
34
- function applyToQuery(queryBuilder, queryOptions = {}) {
35
- return applyVisibility(queryBuilder, queryOptions.visibilityContext);
36
- }
37
-
38
- function scoped(queryOptions = {}) {
39
- return applyToQuery(table(queryOptions), queryOptions);
40
- }
41
-
42
- function scopedById(recordId, queryOptions = {}) {
43
- return scoped(queryOptions).where(resolvedIdColumn, recordId);
44
- }
45
-
46
- function withOwners(payload = {}, queryOptions = {}) {
47
- return applyVisibilityOwners(payload, queryOptions.visibilityContext);
48
- }
49
-
50
- return Object.freeze({
51
- tableName: resolvedTableName,
52
- idColumn: resolvedIdColumn,
53
- clientOf,
54
- table,
55
- applyToQuery,
56
- scoped,
57
- scopedById,
58
- withOwners
59
- });
60
- }
61
-
62
- export { createRepositoryScope };
@@ -1,59 +0,0 @@
1
- import { toDatabaseDateTimeUtc } from "./dateUtils.js";
2
- import { normalizeDbRecordId } from "./repositoryOptions.js";
3
-
4
- function normalizeBatchSize(value, { fallback = 1000, max = 10_000 } = {}) {
5
- const parsed = Number(value);
6
- if (!Number.isInteger(parsed) || parsed < 1) {
7
- return fallback;
8
- }
9
-
10
- return Math.min(parsed, max);
11
- }
12
-
13
- function normalizeCutoffDateOrThrow(value) {
14
- const parsed = value instanceof Date ? value : new Date(value);
15
- if (Number.isNaN(parsed.getTime())) {
16
- throw new TypeError("Invalid cutoff date.");
17
- }
18
-
19
- return parsed;
20
- }
21
-
22
- function normalizeDeletedRowCount(value) {
23
- const parsed = Number(value);
24
- if (!Number.isFinite(parsed) || parsed < 0) {
25
- return 0;
26
- }
27
-
28
- return parsed;
29
- }
30
-
31
- async function deleteRowsOlderThan({ client, tableName, dateColumn, cutoffDate, batchSize, applyFilters }) {
32
- const normalizedCutoff = toDatabaseDateTimeUtc(normalizeCutoffDateOrThrow(cutoffDate));
33
- const normalizedBatchSize = normalizeBatchSize(batchSize);
34
- let query = client(tableName).where(dateColumn, "<", normalizedCutoff);
35
- if (typeof applyFilters === "function") {
36
- query = applyFilters(query);
37
- }
38
-
39
- const ids = await query.orderBy("id", "asc").limit(normalizedBatchSize).select("id");
40
- if (!Array.isArray(ids) || ids.length < 1) {
41
- return 0;
42
- }
43
-
44
- const recordIds = ids.map((entry) => normalizeDbRecordId(entry?.id)).filter(Boolean);
45
- if (recordIds.length < 1) {
46
- return 0;
47
- }
48
-
49
- const deleted = await client(tableName).whereIn("id", recordIds).del();
50
- return normalizeDeletedRowCount(deleted);
51
- }
52
-
53
- const __testables = {
54
- normalizeBatchSize,
55
- normalizeCutoffDateOrThrow,
56
- normalizeDeletedRowCount
57
- };
58
-
59
- export { normalizeBatchSize, normalizeCutoffDateOrThrow, normalizeDeletedRowCount, deleteRowsOlderThan, __testables };
@@ -1,12 +0,0 @@
1
- class DatabaseRuntimeError extends Error {
2
- constructor(message, details = {}) {
3
- super(String(message || "Database runtime error."));
4
- this.name = this.constructor.name;
5
- this.details = details && typeof details === "object" ? { ...details } : {};
6
- }
7
- }
8
-
9
- class TransactionManagerError extends DatabaseRuntimeError {}
10
- class RepositoryError extends DatabaseRuntimeError {}
11
-
12
- export { DatabaseRuntimeError, TransactionManagerError, RepositoryError };
@@ -1,11 +0,0 @@
1
- function createRepoTransaction(dbClient) {
2
- return async function repoTransaction(callback) {
3
- if (typeof dbClient?.transaction === "function") {
4
- return dbClient.transaction(callback);
5
- }
6
-
7
- return callback(dbClient);
8
- };
9
- }
10
-
11
- export { createRepoTransaction };
package/test/json.test.js DELETED
@@ -1,9 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { normalizePath } from "../src/shared/json.js";
4
-
5
- test("normalizePath accepts string and array forms", () => {
6
- assert.deepEqual(normalizePath("a.b.c"), ["a", "b", "c"]);
7
- assert.deepEqual(normalizePath(["a", "b", "c"]), ["a", "b", "c"]);
8
- assert.deepEqual(normalizePath(""), []);
9
- });
@@ -1,103 +0,0 @@
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_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
- userId: 7
53
- }
54
- });
55
-
56
- assert.deepEqual(calls, [["table", "contacts"], ["where", "user_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
- userId: 9
69
- }
70
- });
71
-
72
- assert.deepEqual(calls, [["where", "workspace_id", "4"], ["where", "user_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
- userId: 9
84
- }
85
- }
86
- ),
87
- {
88
- name: "Ada",
89
- workspace_id: "4",
90
- user_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
- });
@@ -1,21 +0,0 @@
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
- });