@jskit-ai/database-runtime 0.1.245 → 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 +6 -18
- package/src/server/providers/DatabaseProvider.js +0 -2
- package/src/shared/databaseClient.js +0 -1
- package/src/shared/databaseConnection.js +2 -1
- package/src/shared/dateUtils.js +4 -11
- package/src/shared/index.js +2 -35
- package/src/shared/repositoryOptions.js +1 -151
- package/src/shared/transactionManager.js +2 -3
- package/src/shared/visibility.js +2 -2
- package/test/dateUtils.test.js +7 -0
- package/test/entrypoints.boundary.test.js +9 -7
- package/test/providerRuntime.test.js +1 -1
- package/test/repositoryOptions.test.js +1 -69
- package/test/runtimeCore.test.js +2 -27
- package/src/client/index.js +0 -1
- package/src/shared/json.js +0 -41
- package/src/shared/repository.js +0 -61
- package/src/shared/repositoryScope.js +0 -62
- package/src/shared/retention.js +0 -59
- package/src/shared/runtimeErrors.js +0 -12
- package/src/shared/transactions.js +0 -11
- package/test/json.test.js +0 -9
- package/test/repositoryScope.test.js +0 -103
- package/test/retention.test.js +0 -21
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/database-runtime",
|
|
3
|
-
"version": "0.1.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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",
|
package/src/shared/dateUtils.js
CHANGED
|
@@ -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,
|
package/src/shared/index.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
14
|
+
throw new TypeError("inTransaction requires a callback function.");
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
if (trx) {
|
package/src/shared/visibility.js
CHANGED
|
@@ -61,10 +61,10 @@ function applyVisibilityOwners(payload = {}, visibilityContext = {}) {
|
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
if (context.scopeOwnerId) {
|
|
64
|
-
ownedPayload.
|
|
64
|
+
ownedPayload[DEFAULT_VISIBILITY_COLUMNS.scopeOwnerId] = context.scopeOwnerId;
|
|
65
65
|
}
|
|
66
66
|
if (context.userId) {
|
|
67
|
-
ownedPayload.
|
|
67
|
+
ownedPayload[DEFAULT_VISIBILITY_COLUMNS.userId] = context.userId;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
return ownedPayload;
|
package/test/dateUtils.test.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
29
|
+
assert.equal(typeof sharedApi.createWithTransaction, "function");
|
|
31
30
|
assert.equal(typeof sharedApi.isDuplicateEntryError, "function");
|
|
32
|
-
assert.equal(typeof sharedApi.
|
|
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.
|
|
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
|
-
|
|
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
|
-
});
|
package/test/runtimeCore.test.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
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
|
-
});
|
package/src/client/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/src/shared/json.js
DELETED
|
@@ -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 };
|
package/src/shared/repository.js
DELETED
|
@@ -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 };
|
package/src/shared/retention.js
DELETED
|
@@ -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
|
-
});
|
package/test/retention.test.js
DELETED
|
@@ -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
|
-
});
|