@jskit-ai/database-runtime 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.descriptor.mjs +89 -0
- package/package.json +30 -0
- package/src/client/index.js +1 -0
- package/src/server/providers/DatabaseRuntimeServiceProvider.js +193 -0
- package/src/shared/databaseClient.js +31 -0
- package/src/shared/dateUtils.js +57 -0
- package/src/shared/dialect.js +25 -0
- package/src/shared/duplicateEntry.js +37 -0
- package/src/shared/index.js +45 -0
- package/src/shared/json.js +41 -0
- package/src/shared/repository.js +61 -0
- package/src/shared/repositoryOptions.js +200 -0
- package/src/shared/repositoryScope.js +62 -0
- package/src/shared/retention.js +58 -0
- package/src/shared/runtime.js +59 -0
- package/src/shared/runtimeErrors.js +12 -0
- package/src/shared/transactionManager.js +32 -0
- package/src/shared/transactions.js +11 -0
- package/src/shared/visibility.js +73 -0
- package/templates/knexfile.js +49 -0
- package/templates/migrations/.gitkeep +1 -0
- package/test/dateUtils.test.js +17 -0
- package/test/duplicateEntry.test.js +10 -0
- package/test/entrypoints.boundary.test.js +34 -0
- package/test/json.test.js +9 -0
- package/test/providerRuntime.test.js +174 -0
- package/test/repositoryOptions.test.js +22 -0
- package/test/repositoryScope.test.js +103 -0
- package/test/retention.test.js +21 -0
- package/test/runtimeCore.test.js +98 -0
- package/test/visibility.test.js +93 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
function resolveQueryOptions(options = {}) {
|
|
2
|
+
if (!options || typeof options !== "object") {
|
|
3
|
+
return {
|
|
4
|
+
trx: null,
|
|
5
|
+
forUpdate: false
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
trx: options.trx || null,
|
|
11
|
+
forUpdate: options.forUpdate === true
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolveRepoClient(dbClient, options = {}) {
|
|
16
|
+
const { trx } = resolveQueryOptions(options);
|
|
17
|
+
return trx || dbClient;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function applyForUpdate(query, options = {}) {
|
|
21
|
+
const { forUpdate } = resolveQueryOptions(options);
|
|
22
|
+
if (forUpdate && typeof query?.forUpdate === "function") {
|
|
23
|
+
return query.forUpdate();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return query;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function mapRowNullable(mapper) {
|
|
30
|
+
if (typeof mapper !== "function") {
|
|
31
|
+
throw new TypeError("mapRowNullable requires a mapper function.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return function mapNullableRow(row) {
|
|
35
|
+
if (!row) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return mapper(row);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseJsonObject(value, fallback = {}) {
|
|
43
|
+
const source = String(value || "").trim();
|
|
44
|
+
if (!source) {
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(source);
|
|
50
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function stringifyJsonObject(value, fallback = "{}") {
|
|
61
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
62
|
+
return fallback;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
return JSON.stringify(value);
|
|
67
|
+
} catch {
|
|
68
|
+
return fallback;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseMetadataJson(value, fallback = {}) {
|
|
73
|
+
const source = String(value || "").trim();
|
|
74
|
+
if (!source) {
|
|
75
|
+
return fallback;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(source);
|
|
80
|
+
return parsed && typeof parsed === "object" ? parsed : fallback;
|
|
81
|
+
} catch {
|
|
82
|
+
return fallback;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function stringifyMetadataJson(metadata, fallback = "{}") {
|
|
87
|
+
if (!metadata || typeof metadata !== "object") {
|
|
88
|
+
return fallback;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
return JSON.stringify(metadata);
|
|
93
|
+
} catch {
|
|
94
|
+
return fallback;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeMetadataJsonInput(value, fallback = null) {
|
|
99
|
+
if (value == null) {
|
|
100
|
+
return fallback;
|
|
101
|
+
}
|
|
102
|
+
if (typeof value === "string") {
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
return JSON.stringify(value);
|
|
108
|
+
} catch {
|
|
109
|
+
return fallback;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizeNullableString(value, { trim = true } = {}) {
|
|
114
|
+
if (value == null) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const source = String(value);
|
|
119
|
+
const normalized = trim ? source.trim() : source;
|
|
120
|
+
return normalized || null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeIdList(values, { parseValue } = {}) {
|
|
124
|
+
const source = Array.isArray(values) ? values : [];
|
|
125
|
+
const parser = typeof parseValue === "function" ? parseValue : (value) => value;
|
|
126
|
+
const normalized = source.map((value) => parser(value)).filter(Boolean);
|
|
127
|
+
return Array.from(new Set(normalized));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeCountRow(row) {
|
|
131
|
+
const values = Object.values(row || {});
|
|
132
|
+
if (values.length < 1) {
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const parsed = Number(values[0]);
|
|
137
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseJsonValue(value, fallback = null, options = {}) {
|
|
141
|
+
const source = options && typeof options === "object" ? options : {};
|
|
142
|
+
const effectiveFallback = Object.hasOwn(source, "fallback") ? source.fallback : fallback;
|
|
143
|
+
const allowNull = source.allowNull === true;
|
|
144
|
+
const allowObject = source.allowObject !== false;
|
|
145
|
+
const trim = source.trim !== false;
|
|
146
|
+
|
|
147
|
+
if (value == null) {
|
|
148
|
+
return effectiveFallback;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (allowObject && typeof value === "object") {
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let text = String(value || "");
|
|
156
|
+
if (trim) {
|
|
157
|
+
text = text.trim();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!text) {
|
|
161
|
+
return effectiveFallback;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(text);
|
|
166
|
+
if (parsed == null && !allowNull) {
|
|
167
|
+
return effectiveFallback;
|
|
168
|
+
}
|
|
169
|
+
return parsed;
|
|
170
|
+
} catch {
|
|
171
|
+
return effectiveFallback;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function toDbJson(value) {
|
|
176
|
+
if (value == null) {
|
|
177
|
+
return JSON.stringify({});
|
|
178
|
+
}
|
|
179
|
+
if (typeof value === "string") {
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
return JSON.stringify(value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export {
|
|
186
|
+
resolveQueryOptions,
|
|
187
|
+
resolveRepoClient,
|
|
188
|
+
applyForUpdate,
|
|
189
|
+
mapRowNullable,
|
|
190
|
+
parseJsonObject,
|
|
191
|
+
stringifyJsonObject,
|
|
192
|
+
parseMetadataJson,
|
|
193
|
+
stringifyMetadataJson,
|
|
194
|
+
normalizeMetadataJsonInput,
|
|
195
|
+
normalizeNullableString,
|
|
196
|
+
normalizeIdList,
|
|
197
|
+
normalizeCountRow,
|
|
198
|
+
parseJsonValue,
|
|
199
|
+
toDbJson
|
|
200
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
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 };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { toDatabaseDateTimeUtc } from "./dateUtils.js";
|
|
2
|
+
|
|
3
|
+
function normalizeBatchSize(value, { fallback = 1000, max = 10_000 } = {}) {
|
|
4
|
+
const parsed = Number(value);
|
|
5
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
6
|
+
return fallback;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return Math.min(parsed, max);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeCutoffDateOrThrow(value) {
|
|
13
|
+
const parsed = value instanceof Date ? value : new Date(value);
|
|
14
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
15
|
+
throw new TypeError("Invalid cutoff date.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeDeletedRowCount(value) {
|
|
22
|
+
const parsed = Number(value);
|
|
23
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function deleteRowsOlderThan({ client, tableName, dateColumn, cutoffDate, batchSize, applyFilters }) {
|
|
31
|
+
const normalizedCutoff = toDatabaseDateTimeUtc(normalizeCutoffDateOrThrow(cutoffDate));
|
|
32
|
+
const normalizedBatchSize = normalizeBatchSize(batchSize);
|
|
33
|
+
let query = client(tableName).where(dateColumn, "<", normalizedCutoff);
|
|
34
|
+
if (typeof applyFilters === "function") {
|
|
35
|
+
query = applyFilters(query);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ids = await query.orderBy("id", "asc").limit(normalizedBatchSize).select("id");
|
|
39
|
+
if (!Array.isArray(ids) || ids.length < 1) {
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const numericIds = ids.map((entry) => Number(entry.id)).filter((id) => Number.isInteger(id) && id > 0);
|
|
44
|
+
if (numericIds.length < 1) {
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const deleted = await client(tableName).whereIn("id", numericIds).del();
|
|
49
|
+
return normalizeDeletedRowCount(deleted);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const __testables = {
|
|
53
|
+
normalizeBatchSize,
|
|
54
|
+
normalizeCutoffDateOrThrow,
|
|
55
|
+
normalizeDeletedRowCount
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export { normalizeBatchSize, normalizeCutoffDateOrThrow, normalizeDeletedRowCount, deleteRowsOlderThan, __testables };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
2
|
+
import { createTransactionManager } from "./transactionManager.js";
|
|
3
|
+
import { DatabaseRuntimeError } from "./runtimeErrors.js";
|
|
4
|
+
|
|
5
|
+
function ensureContainerInterface(app) {
|
|
6
|
+
if (
|
|
7
|
+
!app ||
|
|
8
|
+
typeof app.instance !== "function" ||
|
|
9
|
+
typeof app.singleton !== "function" ||
|
|
10
|
+
typeof app.make !== "function" ||
|
|
11
|
+
typeof app.has !== "function"
|
|
12
|
+
) {
|
|
13
|
+
throw new DatabaseRuntimeError("registerDatabaseRuntime requires application instance/singleton/make/has methods.");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureKnexInterface(knex) {
|
|
18
|
+
if (!knex || typeof knex.transaction !== "function") {
|
|
19
|
+
throw new DatabaseRuntimeError("registerDatabaseRuntime requires knex with transaction().");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ensureKnexBinding(app, knex) {
|
|
24
|
+
if (!app.has(KERNEL_TOKENS.Knex)) {
|
|
25
|
+
app.instance(KERNEL_TOKENS.Knex, knex);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const existingKnex = app.make(KERNEL_TOKENS.Knex);
|
|
30
|
+
if (existingKnex !== knex) {
|
|
31
|
+
throw new DatabaseRuntimeError("registerDatabaseRuntime received knex that differs from existing Knex binding.");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ensureTransactionManagerBinding(app) {
|
|
36
|
+
if (app.has(KERNEL_TOKENS.TransactionManager)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
app.singleton(KERNEL_TOKENS.TransactionManager, (scope) => {
|
|
41
|
+
const knex = scope.make(KERNEL_TOKENS.Knex);
|
|
42
|
+
return createTransactionManager({ knex });
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function registerDatabaseRuntime(app, { knex } = {}) {
|
|
47
|
+
ensureContainerInterface(app);
|
|
48
|
+
ensureKnexInterface(knex);
|
|
49
|
+
|
|
50
|
+
ensureKnexBinding(app, knex);
|
|
51
|
+
ensureTransactionManagerBinding(app);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
knex: app.make(KERNEL_TOKENS.Knex),
|
|
55
|
+
transactionManager: app.make(KERNEL_TOKENS.TransactionManager)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { registerDatabaseRuntime };
|
|
@@ -0,0 +1,12 @@
|
|
|
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 };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { TransactionManagerError } from "./runtimeErrors.js";
|
|
3
|
+
|
|
4
|
+
class TransactionManager {
|
|
5
|
+
constructor({ knex } = {}) {
|
|
6
|
+
if (!knex || typeof knex.transaction !== "function") {
|
|
7
|
+
throw new TransactionManagerError("TransactionManager requires knex with transaction().");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
this.knex = knex;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async inTransaction(work, { trx = null } = {}) {
|
|
14
|
+
if (typeof work !== "function") {
|
|
15
|
+
throw new TransactionManagerError("inTransaction requires a callback function.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (trx) {
|
|
19
|
+
return work(trx);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return this.knex.transaction(async (nextTrx) => {
|
|
23
|
+
return work(nextTrx);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createTransactionManager(options = {}) {
|
|
29
|
+
return new TransactionManager(normalizeObject(options));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { TransactionManager, createTransactionManager };
|
|
@@ -0,0 +1,11 @@
|
|
|
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 };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { normalizeVisibilityContext } from "@jskit-ai/kernel/shared/support/visibility";
|
|
2
|
+
|
|
3
|
+
const ALWAYS_FALSE_SQL = "1 = 0";
|
|
4
|
+
const DEFAULT_VISIBILITY_COLUMNS = Object.freeze({
|
|
5
|
+
scopeOwnerId: "workspace_owner_id",
|
|
6
|
+
userOwnerId: "user_owner_id"
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
function applyVisibility(queryBuilder, visibilityContext = {}) {
|
|
10
|
+
const context = normalizeVisibilityContext(visibilityContext);
|
|
11
|
+
const workspaceColumn = DEFAULT_VISIBILITY_COLUMNS.scopeOwnerId;
|
|
12
|
+
const userColumn = DEFAULT_VISIBILITY_COLUMNS.userOwnerId;
|
|
13
|
+
|
|
14
|
+
if (context.visibility === "public") {
|
|
15
|
+
return queryBuilder;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (context.visibility === "workspace") {
|
|
19
|
+
if (!context.scopeOwnerId) {
|
|
20
|
+
return queryBuilder.whereRaw(ALWAYS_FALSE_SQL);
|
|
21
|
+
}
|
|
22
|
+
return queryBuilder.where(workspaceColumn, context.scopeOwnerId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (context.visibility === "user") {
|
|
26
|
+
if (!context.userOwnerId) {
|
|
27
|
+
return queryBuilder.whereRaw(ALWAYS_FALSE_SQL);
|
|
28
|
+
}
|
|
29
|
+
return queryBuilder.where(userColumn, context.userOwnerId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!context.scopeOwnerId || !context.userOwnerId) {
|
|
33
|
+
return queryBuilder.whereRaw(ALWAYS_FALSE_SQL);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return queryBuilder.where(workspaceColumn, context.scopeOwnerId).where(userColumn, context.userOwnerId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function applyVisibilityOwners(payload = {}, visibilityContext = {}) {
|
|
40
|
+
const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
41
|
+
const context = normalizeVisibilityContext(visibilityContext);
|
|
42
|
+
|
|
43
|
+
if (context.visibility === "public") {
|
|
44
|
+
return {
|
|
45
|
+
...source
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (context.visibility === "workspace" && !context.scopeOwnerId) {
|
|
50
|
+
throw new Error("Visibility context requires scopeOwnerId.");
|
|
51
|
+
}
|
|
52
|
+
if (context.visibility === "user" && !context.userOwnerId) {
|
|
53
|
+
throw new Error("Visibility context requires userOwnerId.");
|
|
54
|
+
}
|
|
55
|
+
if (context.visibility === "workspace_user" && (!context.scopeOwnerId || !context.userOwnerId)) {
|
|
56
|
+
throw new Error("Visibility context requires scopeOwnerId and userOwnerId.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ownedPayload = {
|
|
60
|
+
...source
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (context.scopeOwnerId) {
|
|
64
|
+
ownedPayload.workspace_owner_id = context.scopeOwnerId;
|
|
65
|
+
}
|
|
66
|
+
if (context.userOwnerId) {
|
|
67
|
+
ownedPayload.user_owner_id = context.userOwnerId;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return ownedPayload;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { DEFAULT_VISIBILITY_COLUMNS, applyVisibility, applyVisibilityOwners };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
import {
|
|
4
|
+
normalizeText,
|
|
5
|
+
normalizeDatabaseClient,
|
|
6
|
+
toKnexClientId
|
|
7
|
+
} from "@jskit-ai/database-runtime/shared/databaseClient";
|
|
8
|
+
|
|
9
|
+
function resolveRequiredEnvString(env, key) {
|
|
10
|
+
const value = normalizeText(env[key]);
|
|
11
|
+
if (!value) {
|
|
12
|
+
throw new Error(`${key} is required.`);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolvePort(value, fallbackPort) {
|
|
18
|
+
const parsed = Number.parseInt(normalizeText(value), 10);
|
|
19
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
return fallbackPort;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const appRoot = process.cwd();
|
|
26
|
+
dotenv.config({
|
|
27
|
+
path: path.join(appRoot, ".env"),
|
|
28
|
+
quiet: true
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const dialectId = normalizeDatabaseClient(process.env.DB_CLIENT);
|
|
32
|
+
const client = toKnexClientId(dialectId);
|
|
33
|
+
const defaultPort = dialectId === "pg" ? 5432 : 3306;
|
|
34
|
+
const migrationsDirectory = path.resolve(appRoot, normalizeText(process.env.DB_MIGRATIONS_DIR) || "migrations");
|
|
35
|
+
|
|
36
|
+
export default {
|
|
37
|
+
client,
|
|
38
|
+
connection: {
|
|
39
|
+
host: normalizeText(process.env.DB_HOST) || "localhost",
|
|
40
|
+
port: resolvePort(process.env.DB_PORT, defaultPort),
|
|
41
|
+
database: resolveRequiredEnvString(process.env, "DB_NAME"),
|
|
42
|
+
user: resolveRequiredEnvString(process.env, "DB_USER"),
|
|
43
|
+
password: String(process.env.DB_PASSWORD ?? "")
|
|
44
|
+
},
|
|
45
|
+
migrations: {
|
|
46
|
+
directory: migrationsDirectory,
|
|
47
|
+
extension: "cjs"
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { toIsoString, toDatabaseDateTimeUtc } from "../src/shared/dateUtils.js";
|
|
4
|
+
|
|
5
|
+
test("toIsoString normalizes valid date input", () => {
|
|
6
|
+
assert.equal(toIsoString("2024-01-01T00:00:00.000Z"), "2024-01-01T00:00:00.000Z");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("toDatabaseDateTimeUtc formats DATETIME(3) UTC string", () => {
|
|
10
|
+
assert.equal(toDatabaseDateTimeUtc("2024-01-01T00:00:00.000Z"), "2024-01-01 00:00:00.000");
|
|
11
|
+
assert.equal(toDatabaseDateTimeUtc(new Date("2024-01-01T01:02:03.045Z")), "2024-01-01 01:02:03.045");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("date utils throw on invalid date", () => {
|
|
15
|
+
assert.throws(() => toIsoString("not-a-date"), /Invalid date value\./);
|
|
16
|
+
assert.throws(() => toDatabaseDateTimeUtc("not-a-date"), /Invalid date value\./);
|
|
17
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { isDuplicateEntryError } from "../src/shared/duplicateEntry.js";
|
|
4
|
+
|
|
5
|
+
test("isDuplicateEntryError matches mysql and postgres duplicate signatures", () => {
|
|
6
|
+
assert.equal(isDuplicateEntryError({ code: "ER_DUP_ENTRY" }), true);
|
|
7
|
+
assert.equal(isDuplicateEntryError({ errno: 1062 }), true);
|
|
8
|
+
assert.equal(isDuplicateEntryError({ code: "23505" }), true);
|
|
9
|
+
assert.equal(isDuplicateEntryError({ code: "ER_PARSE_ERROR", errno: 1064 }), false);
|
|
10
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
|
|
5
|
+
import * as clientApi from "../src/client/index.js";
|
|
6
|
+
import * as sharedApi from "../src/shared/index.js";
|
|
7
|
+
import { DatabaseRuntimeServiceProvider } from "../src/server/providers/DatabaseRuntimeServiceProvider.js";
|
|
8
|
+
|
|
9
|
+
test("package exports include explicit shared and provider entrypoints", async () => {
|
|
10
|
+
const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
11
|
+
const exportsMap = packageJson && typeof packageJson === "object" ? packageJson.exports : {};
|
|
12
|
+
assert.equal(exportsMap["./server"], undefined);
|
|
13
|
+
assert.equal(
|
|
14
|
+
exportsMap["./server/providers/DatabaseRuntimeServiceProvider"],
|
|
15
|
+
"./src/server/providers/DatabaseRuntimeServiceProvider.js"
|
|
16
|
+
);
|
|
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), []);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("server provider module exports service provider only", () => {
|
|
25
|
+
assert.equal(typeof DatabaseRuntimeServiceProvider, "function");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("shared entrypoint exports shared database utilities only", () => {
|
|
29
|
+
assert.equal(typeof sharedApi.registerDatabaseRuntime, "function");
|
|
30
|
+
assert.equal(typeof sharedApi.createTransactionManager, "function");
|
|
31
|
+
assert.equal(typeof sharedApi.isDuplicateEntryError, "function");
|
|
32
|
+
assert.equal(typeof sharedApi.createRepositoryScope, "function");
|
|
33
|
+
assert.equal(typeof sharedApi.DatabaseRuntimeServiceProvider, "undefined");
|
|
34
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
});
|