@jskit-ai/database-runtime 0.1.32 → 0.1.34

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,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/database-runtime",
4
- version: "0.1.32",
4
+ version: "0.1.34",
5
5
  kind: "runtime",
6
6
  dependsOn: [
7
7
  "@jskit-ai/kernel"
@@ -58,7 +58,7 @@ export default Object.freeze({
58
58
  mutations: {
59
59
  dependencies: {
60
60
  runtime: {
61
- "@jskit-ai/kernel": "0.1.32",
61
+ "@jskit-ai/kernel": "0.1.34",
62
62
  "dotenv": "^16.4.5",
63
63
  "knex": "^3.1.0"
64
64
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/database-runtime",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -25,6 +25,6 @@
25
25
  "./shared/transactions": "./src/shared/transactions.js"
26
26
  },
27
27
  "dependencies": {
28
- "@jskit-ai/kernel": "0.1.32"
28
+ "@jskit-ai/kernel": "0.1.34"
29
29
  }
30
30
  }
@@ -5,7 +5,7 @@ import {
5
5
  normalizeDatabaseClient,
6
6
  toKnexClientId
7
7
  } from "../../shared/databaseClient.js";
8
- import { resolveDatabaseConnectionFromEnvironment } from "../../shared/databaseConnection.js";
8
+ import { resolveKnexConnectionFromEnvironment } from "../../shared/databaseConnection.js";
9
9
 
10
10
  const DATABASE_RUNTIME_SERVER_API = Object.freeze({
11
11
  ...databaseRuntime
@@ -120,7 +120,8 @@ function createKnexConfig(scope) {
120
120
 
121
121
  const client = toKnexClientId(dialectId);
122
122
  const defaultPort = dialectId === "pg" ? 5432 : 3306;
123
- const connection = resolveDatabaseConnectionFromEnvironment(env, {
123
+ const connection = resolveKnexConnectionFromEnvironment(env, {
124
+ client: dialectId,
124
125
  defaultPort,
125
126
  context: "database runtime"
126
127
  });
@@ -100,7 +100,6 @@ function resolveDatabaseConnectionFromEnvironment(
100
100
  const hasDbPassword = Object.prototype.hasOwnProperty.call(source, "DB_PASSWORD");
101
101
  const password = hasDbPassword ? String(source.DB_PASSWORD ?? "") : String(parsedUrl?.password || "");
102
102
 
103
- // Knex may redefine connection.password as a hidden property; keep this mutable.
104
103
  return {
105
104
  host,
106
105
  port,
@@ -110,8 +109,38 @@ function resolveDatabaseConnectionFromEnvironment(
110
109
  };
111
110
  }
112
111
 
112
+ function resolveKnexConnectionFromEnvironment(
113
+ env = {},
114
+ {
115
+ client = "",
116
+ defaultHost = "localhost",
117
+ defaultPort = 3306,
118
+ context = "database runtime"
119
+ } = {}
120
+ ) {
121
+ const resolvedClient = client
122
+ ? normalizeDatabaseClient(client, { allowEmpty: true })
123
+ : resolveDatabaseClientFromEnvironment(env, { allowEmpty: true });
124
+ const connection = resolveDatabaseConnectionFromEnvironment(env, {
125
+ defaultHost,
126
+ defaultPort,
127
+ context
128
+ });
129
+
130
+ if (resolvedClient === "mysql2") {
131
+ return {
132
+ ...connection,
133
+ supportBigNumbers: true,
134
+ bigNumberStrings: true
135
+ };
136
+ }
137
+
138
+ return connection;
139
+ }
140
+
113
141
  export {
114
142
  parseDatabaseUrl,
115
143
  resolveDatabaseClientFromEnvironment,
116
- resolveDatabaseConnectionFromEnvironment
144
+ resolveDatabaseConnectionFromEnvironment,
145
+ resolveKnexConnectionFromEnvironment
117
146
  };
@@ -18,7 +18,8 @@ export { normalizeText, normalizeDatabaseClient, toKnexClientId } from "./databa
18
18
  export {
19
19
  parseDatabaseUrl,
20
20
  resolveDatabaseClientFromEnvironment,
21
- resolveDatabaseConnectionFromEnvironment
21
+ resolveDatabaseConnectionFromEnvironment,
22
+ resolveKnexConnectionFromEnvironment
22
23
  } from "./databaseConnection.js";
23
24
  export { isDuplicateEntryError } from "./duplicateEntry.js";
24
25
  export { normalizePath, jsonTextExpression, whereJsonTextEquals } from "./json.js";
@@ -35,11 +36,14 @@ export {
35
36
  stringifyMetadataJson,
36
37
  normalizeMetadataJsonInput,
37
38
  normalizeNullableString,
39
+ normalizeDbRecordId,
40
+ resolveInsertedRecordId,
38
41
  normalizeIdList,
39
42
  normalizeCountRow,
40
43
  parseJsonValue,
41
44
  toDbJson,
42
- runInTransaction
45
+ runInTransaction,
46
+ createWithTransaction
43
47
  } from "./repositoryOptions.js";
44
48
  export {
45
49
  normalizeBatchSize,
@@ -1,3 +1,5 @@
1
+ import { normalizeCanonicalRecordIdText } from "@jskit-ai/kernel/shared/support/normalize";
2
+
1
3
  function resolveQueryOptions(options = {}) {
2
4
  if (!options || typeof options !== "object") {
3
5
  return {
@@ -120,6 +122,44 @@ function normalizeNullableString(value, { trim = true } = {}) {
120
122
  return normalized || null;
121
123
  }
122
124
 
125
+ function normalizeDbRecordId(value, { fallback = null } = {}) {
126
+ if (value == null) {
127
+ return fallback;
128
+ }
129
+
130
+ if (typeof value === "string") {
131
+ return normalizeCanonicalRecordIdText(value, { fallback });
132
+ }
133
+
134
+ if (typeof value === "number") {
135
+ if (!Number.isSafeInteger(value) || value < 1) {
136
+ return fallback;
137
+ }
138
+ return normalizeCanonicalRecordIdText(value, { fallback });
139
+ }
140
+
141
+ if (typeof value === "bigint") {
142
+ if (value < 1n) {
143
+ return fallback;
144
+ }
145
+ return normalizeCanonicalRecordIdText(value, { fallback });
146
+ }
147
+
148
+ if (typeof value === "object" && !Array.isArray(value) && Object.hasOwn(value, "id")) {
149
+ return normalizeDbRecordId(value.id, { fallback });
150
+ }
151
+
152
+ return normalizeCanonicalRecordIdText(value, { fallback });
153
+ }
154
+
155
+ function resolveInsertedRecordId(insertResult, { fallback = null } = {}) {
156
+ if (Array.isArray(insertResult) && insertResult.length > 0) {
157
+ return normalizeDbRecordId(insertResult[0], { fallback });
158
+ }
159
+
160
+ return normalizeDbRecordId(insertResult, { fallback });
161
+ }
162
+
123
163
  function normalizeIdList(values, { parseValue } = {}) {
124
164
  const source = Array.isArray(values) ? values : [];
125
165
  const parser = typeof parseValue === "function" ? parseValue : (value) => value;
@@ -196,6 +236,12 @@ async function runInTransaction(knex, callback) {
196
236
  return knex.transaction(callback);
197
237
  }
198
238
 
239
+ function createWithTransaction(knex) {
240
+ return function withTransaction(work) {
241
+ return runInTransaction(knex, work);
242
+ };
243
+ }
244
+
199
245
  export {
200
246
  resolveQueryOptions,
201
247
  resolveRepoClient,
@@ -207,9 +253,12 @@ export {
207
253
  stringifyMetadataJson,
208
254
  normalizeMetadataJsonInput,
209
255
  normalizeNullableString,
256
+ normalizeDbRecordId,
257
+ resolveInsertedRecordId,
210
258
  normalizeIdList,
211
259
  normalizeCountRow,
212
260
  parseJsonValue,
213
261
  toDbJson,
214
- runInTransaction
262
+ runInTransaction,
263
+ createWithTransaction
215
264
  };
@@ -1,4 +1,5 @@
1
1
  import { toDatabaseDateTimeUtc } from "./dateUtils.js";
2
+ import { normalizeDbRecordId } from "./repositoryOptions.js";
2
3
 
3
4
  function normalizeBatchSize(value, { fallback = 1000, max = 10_000 } = {}) {
4
5
  const parsed = Number(value);
@@ -40,12 +41,12 @@ async function deleteRowsOlderThan({ client, tableName, dateColumn, cutoffDate,
40
41
  return 0;
41
42
  }
42
43
 
43
- const numericIds = ids.map((entry) => Number(entry.id)).filter((id) => Number.isInteger(id) && id > 0);
44
- if (numericIds.length < 1) {
44
+ const recordIds = ids.map((entry) => normalizeDbRecordId(entry?.id)).filter(Boolean);
45
+ if (recordIds.length < 1) {
45
46
  return 0;
46
47
  }
47
48
 
48
- const deleted = await client(tableName).whereIn("id", numericIds).del();
49
+ const deleted = await client(tableName).whereIn("id", recordIds).del();
49
50
  return normalizeDeletedRowCount(deleted);
50
51
  }
51
52
 
@@ -2,14 +2,14 @@ import { normalizeVisibilityContext } from "@jskit-ai/kernel/shared/support/visi
2
2
 
3
3
  const ALWAYS_FALSE_SQL = "1 = 0";
4
4
  const DEFAULT_VISIBILITY_COLUMNS = Object.freeze({
5
- scopeOwnerId: "workspace_owner_id",
6
- userOwnerId: "user_owner_id"
5
+ scopeOwnerId: "workspace_id",
6
+ userId: "user_id"
7
7
  });
8
8
 
9
9
  function applyVisibility(queryBuilder, visibilityContext = {}) {
10
10
  const context = normalizeVisibilityContext(visibilityContext);
11
11
  const workspaceColumn = DEFAULT_VISIBILITY_COLUMNS.scopeOwnerId;
12
- const userColumn = DEFAULT_VISIBILITY_COLUMNS.userOwnerId;
12
+ const userColumn = DEFAULT_VISIBILITY_COLUMNS.userId;
13
13
 
14
14
  if (context.visibility === "public") {
15
15
  return queryBuilder;
@@ -23,17 +23,17 @@ function applyVisibility(queryBuilder, visibilityContext = {}) {
23
23
  }
24
24
 
25
25
  if (context.visibility === "user") {
26
- if (!context.userOwnerId) {
26
+ if (!context.userId) {
27
27
  return queryBuilder.whereRaw(ALWAYS_FALSE_SQL);
28
28
  }
29
- return queryBuilder.where(userColumn, context.userOwnerId);
29
+ return queryBuilder.where(userColumn, context.userId);
30
30
  }
31
31
 
32
- if (!context.scopeOwnerId || !context.userOwnerId) {
32
+ if (!context.scopeOwnerId || !context.userId) {
33
33
  return queryBuilder.whereRaw(ALWAYS_FALSE_SQL);
34
34
  }
35
35
 
36
- return queryBuilder.where(workspaceColumn, context.scopeOwnerId).where(userColumn, context.userOwnerId);
36
+ return queryBuilder.where(workspaceColumn, context.scopeOwnerId).where(userColumn, context.userId);
37
37
  }
38
38
 
39
39
  function applyVisibilityOwners(payload = {}, visibilityContext = {}) {
@@ -49,11 +49,11 @@ function applyVisibilityOwners(payload = {}, visibilityContext = {}) {
49
49
  if (context.visibility === "workspace" && !context.scopeOwnerId) {
50
50
  throw new Error("Visibility context requires scopeOwnerId.");
51
51
  }
52
- if (context.visibility === "user" && !context.userOwnerId) {
53
- throw new Error("Visibility context requires userOwnerId.");
52
+ if (context.visibility === "user" && !context.userId) {
53
+ throw new Error("Visibility context requires userId.");
54
54
  }
55
- if (context.visibility === "workspace_user" && (!context.scopeOwnerId || !context.userOwnerId)) {
56
- throw new Error("Visibility context requires scopeOwnerId and userOwnerId.");
55
+ if (context.visibility === "workspace_user" && (!context.scopeOwnerId || !context.userId)) {
56
+ throw new Error("Visibility context requires scopeOwnerId and userId.");
57
57
  }
58
58
 
59
59
  const ownedPayload = {
@@ -61,10 +61,10 @@ function applyVisibilityOwners(payload = {}, visibilityContext = {}) {
61
61
  };
62
62
 
63
63
  if (context.scopeOwnerId) {
64
- ownedPayload.workspace_owner_id = context.scopeOwnerId;
64
+ ownedPayload.workspace_id = context.scopeOwnerId;
65
65
  }
66
- if (context.userOwnerId) {
67
- ownedPayload.user_owner_id = context.userOwnerId;
66
+ if (context.userId) {
67
+ ownedPayload.user_id = context.userId;
68
68
  }
69
69
 
70
70
  return ownedPayload;
@@ -4,7 +4,7 @@ import {
4
4
  normalizeText,
5
5
  toKnexClientId,
6
6
  resolveDatabaseClientFromEnvironment,
7
- resolveDatabaseConnectionFromEnvironment
7
+ resolveKnexConnectionFromEnvironment
8
8
  } from "@jskit-ai/database-runtime/shared";
9
9
 
10
10
  const appRoot = process.cwd();
@@ -20,7 +20,8 @@ const migrationsDirectory = path.resolve(appRoot, normalizeText(process.env.DB_M
20
20
 
21
21
  export default {
22
22
  client,
23
- connection: resolveDatabaseConnectionFromEnvironment(process.env, {
23
+ connection: resolveKnexConnectionFromEnvironment(process.env, {
24
+ client: dialectId,
24
25
  defaultPort,
25
26
  context: "knex migrations"
26
27
  }),
@@ -1,6 +1,11 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { parseMetadataJson, stringifyMetadataJson } from "../src/shared/repositoryOptions.js";
3
+ import {
4
+ normalizeDbRecordId,
5
+ parseMetadataJson,
6
+ resolveInsertedRecordId,
7
+ stringifyMetadataJson
8
+ } from "../src/shared/repositoryOptions.js";
4
9
 
5
10
  test("parseMetadataJson parses object-like metadata payloads", () => {
6
11
  assert.deepEqual(parseMetadataJson(""), {});
@@ -20,3 +25,18 @@ test("stringifyMetadataJson returns fallback for non-serializable values", () =>
20
25
  circular.self = circular;
21
26
  assert.equal(stringifyMetadataJson(circular), "{}");
22
27
  });
28
+
29
+ test("normalizeDbRecordId preserves canonical DB ids and rejects unsafe JS numbers", () => {
30
+ const unsafeNumericId = Number(9007199254740993n);
31
+ assert.equal(normalizeDbRecordId("9007199254740993"), "9007199254740993");
32
+ assert.equal(normalizeDbRecordId(42), "42");
33
+ assert.equal(normalizeDbRecordId(42n), "42");
34
+ assert.equal(normalizeDbRecordId(unsafeNumericId), null);
35
+ });
36
+
37
+ test("resolveInsertedRecordId normalizes insert ids without accepting unsafe JS numbers", () => {
38
+ const unsafeNumericId = Number(9007199254740993n);
39
+ assert.equal(resolveInsertedRecordId(["9007199254740993"]), "9007199254740993");
40
+ assert.equal(resolveInsertedRecordId([42]), "42");
41
+ assert.equal(resolveInsertedRecordId([unsafeNumericId]), null);
42
+ });
@@ -37,7 +37,7 @@ test("createRepositoryScope builds explicit scoped query helpers", () => {
37
37
  }
38
38
  });
39
39
 
40
- assert.deepEqual(calls, [["table", "contacts"], ["where", "workspace_owner_id", 12]]);
40
+ assert.deepEqual(calls, [["table", "contacts"], ["where", "workspace_id", "12"]]);
41
41
  });
42
42
 
43
43
  test("createRepositoryScope supports scopedById with custom id column", () => {
@@ -49,11 +49,11 @@ test("createRepositoryScope supports scopedById with custom id column", () => {
49
49
  scope.scopedById(33, {
50
50
  visibilityContext: {
51
51
  visibility: "user",
52
- userOwnerId: 7
52
+ userId: 7
53
53
  }
54
54
  });
55
55
 
56
- assert.deepEqual(calls, [["table", "contacts"], ["where", "user_owner_id", 7], ["where", "contact_id", 33]]);
56
+ assert.deepEqual(calls, [["table", "contacts"], ["where", "user_id", "7"], ["where", "contact_id", 33]]);
57
57
  });
58
58
 
59
59
  test("createRepositoryScope exposes applyToQuery and owner stamping", () => {
@@ -65,11 +65,11 @@ test("createRepositoryScope exposes applyToQuery and owner stamping", () => {
65
65
  visibilityContext: {
66
66
  visibility: "workspace_user",
67
67
  scopeOwnerId: 4,
68
- userOwnerId: 9
68
+ userId: 9
69
69
  }
70
70
  });
71
71
 
72
- assert.deepEqual(calls, [["where", "workspace_owner_id", 4], ["where", "user_owner_id", 9]]);
72
+ assert.deepEqual(calls, [["where", "workspace_id", "4"], ["where", "user_id", "9"]]);
73
73
 
74
74
  assert.deepEqual(
75
75
  scope.withOwners(
@@ -80,14 +80,14 @@ test("createRepositoryScope exposes applyToQuery and owner stamping", () => {
80
80
  visibilityContext: {
81
81
  visibility: "workspace_user",
82
82
  scopeOwnerId: 4,
83
- userOwnerId: 9
83
+ userId: 9
84
84
  }
85
85
  }
86
86
  ),
87
87
  {
88
88
  name: "Ada",
89
- workspace_owner_id: 4,
90
- user_owner_id: 9
89
+ workspace_id: "4",
90
+ user_id: "9"
91
91
  }
92
92
  );
93
93
 
@@ -5,7 +5,8 @@ import {
5
5
  BaseRepository,
6
6
  buildPaginationMeta,
7
7
  createTransactionManager,
8
- registerDatabaseRuntime
8
+ createWithTransaction,
9
+ registerDatabaseRuntime,
9
10
  } from "../src/shared/index.js";
10
11
 
11
12
  function createKnexStub() {
@@ -74,6 +75,15 @@ test("base repository withTransaction delegates to transaction manager", async (
74
75
  assert.deepEqual(result, { id: "trx-1" });
75
76
  });
76
77
 
78
+ test("createWithTransaction creates a reusable withTransaction function", async () => {
79
+ const knex = Object.assign(() => {
80
+ throw new Error("query execution not expected");
81
+ }, createKnexStub());
82
+ const withTransaction = createWithTransaction(knex);
83
+ const result = await withTransaction(async (trx) => ({ id: trx.trxId }));
84
+ assert.deepEqual(result, { id: "trx-1" });
85
+ });
86
+
77
87
  test("pagination helpers generate stable metadata", () => {
78
88
  const meta = buildPaginationMeta({ total: 51, page: 2, pageSize: 25 });
79
89
  assert.deepEqual(meta, {
@@ -28,14 +28,14 @@ test("applyVisibility appends scope filters to query builders", () => {
28
28
  visibility: "workspace",
29
29
  scopeOwnerId: 12
30
30
  });
31
- assert.deepEqual(workspaceQuery.calls, [["where", "workspace_owner_id", 12]]);
31
+ assert.deepEqual(workspaceQuery.calls, [["where", "workspace_id", "12"]]);
32
32
 
33
33
  const userQuery = createQueryBuilderStub();
34
34
  applyVisibility(userQuery, {
35
35
  visibility: "user",
36
- userOwnerId: 7
36
+ userId: 7
37
37
  });
38
- assert.deepEqual(userQuery.calls, [["where", "user_owner_id", 7]]);
38
+ assert.deepEqual(userQuery.calls, [["where", "user_id", "7"]]);
39
39
 
40
40
  const deniedQuery = createQueryBuilderStub();
41
41
  applyVisibility(deniedQuery, {
@@ -68,13 +68,13 @@ test("applyVisibilityOwners injects owner columns for write payloads", () => {
68
68
  {
69
69
  visibility: "workspace_user",
70
70
  scopeOwnerId: 4,
71
- userOwnerId: 9
71
+ userId: 9
72
72
  }
73
73
  ),
74
74
  {
75
75
  name: "Alice",
76
- workspace_owner_id: 4,
77
- user_owner_id: 9
76
+ workspace_id: "4",
77
+ user_id: "9"
78
78
  }
79
79
  );
80
80
 
@@ -88,6 +88,6 @@ test("applyVisibilityOwners injects owner columns for write payloads", () => {
88
88
  visibility: "user"
89
89
  }
90
90
  ),
91
- /requires userOwnerId/
91
+ /requires userId/
92
92
  );
93
93
  });