@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,89 @@
|
|
|
1
|
+
export default Object.freeze({
|
|
2
|
+
packageVersion: 1,
|
|
3
|
+
packageId: "@jskit-ai/database-runtime",
|
|
4
|
+
version: "0.1.4",
|
|
5
|
+
dependsOn: [
|
|
6
|
+
"@jskit-ai/kernel"
|
|
7
|
+
],
|
|
8
|
+
capabilities: {
|
|
9
|
+
provides: [
|
|
10
|
+
"runtime.database"
|
|
11
|
+
],
|
|
12
|
+
requires: [
|
|
13
|
+
"runtime.database.driver"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
runtime: {
|
|
17
|
+
server: {
|
|
18
|
+
providerEntrypoint: "src/server/providers/DatabaseRuntimeServiceProvider.js",
|
|
19
|
+
providers: [
|
|
20
|
+
{
|
|
21
|
+
entrypoint: "src/server/providers/DatabaseRuntimeServiceProvider.js",
|
|
22
|
+
export: "DatabaseRuntimeServiceProvider"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
client: {
|
|
27
|
+
providers: []
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
metadata: {
|
|
31
|
+
apiSummary: {
|
|
32
|
+
surfaces: [
|
|
33
|
+
{
|
|
34
|
+
subpath: "./server",
|
|
35
|
+
summary: "Exports DatabaseRuntimeServiceProvider plus registerDatabaseRuntime for server container wiring."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
subpath: "./shared",
|
|
39
|
+
summary: "Exports shared Knex runtime utilities (transaction manager, repository helpers, retention/json/date/dialect helpers)."
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
subpath: "./client",
|
|
43
|
+
summary: "Exports no runtime API today (reserved client entrypoint)."
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
containerTokens: {
|
|
47
|
+
server: [
|
|
48
|
+
"runtime.database",
|
|
49
|
+
"runtime.database.driver"
|
|
50
|
+
],
|
|
51
|
+
client: []
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
mutations: {
|
|
56
|
+
dependencies: {
|
|
57
|
+
runtime: {
|
|
58
|
+
"@jskit-ai/kernel": "0.1.4",
|
|
59
|
+
"dotenv": "^16.4.5",
|
|
60
|
+
"knex": "^3.1.0"
|
|
61
|
+
},
|
|
62
|
+
dev: {}
|
|
63
|
+
},
|
|
64
|
+
packageJson: {
|
|
65
|
+
scripts: {
|
|
66
|
+
"db:migrate": "knex --knexfile ./knexfile.js migrate:latest",
|
|
67
|
+
"db:migrate:rollback": "knex --knexfile ./knexfile.js migrate:rollback",
|
|
68
|
+
"db:migrate:status": "knex --knexfile ./knexfile.js migrate:list"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
procfile: {},
|
|
72
|
+
files: [
|
|
73
|
+
{
|
|
74
|
+
from: "templates/knexfile.js",
|
|
75
|
+
to: "knexfile.js",
|
|
76
|
+
reason: "Install root Knex configuration so app scripts can run migrations through Knex CLI.",
|
|
77
|
+
category: "database-runtime",
|
|
78
|
+
id: "database-runtime-knexfile"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
from: "templates/migrations/.gitkeep",
|
|
82
|
+
to: "migrations/.gitkeep",
|
|
83
|
+
reason: "Ensure migrations directory exists so Knex migration commands can run before any module installs migrations.",
|
|
84
|
+
category: "database-runtime",
|
|
85
|
+
id: "database-runtime-migrations-dir"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jskit-ai/database-runtime",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "node --test"
|
|
7
|
+
},
|
|
8
|
+
"exports": {
|
|
9
|
+
"./client": "./src/client/index.js",
|
|
10
|
+
"./server/providers/DatabaseRuntimeServiceProvider": "./src/server/providers/DatabaseRuntimeServiceProvider.js",
|
|
11
|
+
"./shared": "./src/shared/index.js",
|
|
12
|
+
"./shared/dateUtils": "./src/shared/dateUtils.js",
|
|
13
|
+
"./shared/databaseClient": "./src/shared/databaseClient.js",
|
|
14
|
+
"./shared/dialect": "./src/shared/dialect.js",
|
|
15
|
+
"./shared/duplicateEntry": "./src/shared/duplicateEntry.js",
|
|
16
|
+
"./shared/json": "./src/shared/json.js",
|
|
17
|
+
"./shared/repository": "./src/shared/repository.js",
|
|
18
|
+
"./shared/repositoryScope": "./src/shared/repositoryScope.js",
|
|
19
|
+
"./shared/repositoryOptions": "./src/shared/repositoryOptions.js",
|
|
20
|
+
"./shared/visibility": "./src/shared/visibility.js",
|
|
21
|
+
"./shared/retention": "./src/shared/retention.js",
|
|
22
|
+
"./shared/runtime": "./src/shared/runtime.js",
|
|
23
|
+
"./shared/runtimeErrors": "./src/shared/runtimeErrors.js",
|
|
24
|
+
"./shared/transactionManager": "./src/shared/transactionManager.js",
|
|
25
|
+
"./shared/transactions": "./src/shared/transactions.js"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@jskit-ai/kernel": "0.1.4"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
2
|
+
import { symlinkSafeRequire } from "@jskit-ai/kernel/server/support";
|
|
3
|
+
import * as databaseRuntime from "../../shared/index.js";
|
|
4
|
+
import { createTransactionManager } from "../../shared/transactionManager.js";
|
|
5
|
+
import {
|
|
6
|
+
normalizeText,
|
|
7
|
+
normalizeDatabaseClient,
|
|
8
|
+
toKnexClientId
|
|
9
|
+
} from "../../shared/databaseClient.js";
|
|
10
|
+
|
|
11
|
+
const DATABASE_RUNTIME_TOKEN = "runtime.database";
|
|
12
|
+
const DATABASE_DRIVER_TOKEN = "runtime.database.driver";
|
|
13
|
+
const MYSQL_DRIVER_TOKEN = "runtime.database.driver.mysql";
|
|
14
|
+
const POSTGRES_DRIVER_TOKEN = "runtime.database.driver.postgres";
|
|
15
|
+
|
|
16
|
+
const DATABASE_RUNTIME_SERVER_API = Object.freeze({
|
|
17
|
+
...databaseRuntime
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function resolveDatabaseEnv(scope) {
|
|
21
|
+
const envFromScope = scope?.has?.(KERNEL_TOKENS.Env) ? scope.make(KERNEL_TOKENS.Env) : {};
|
|
22
|
+
const source = envFromScope && typeof envFromScope === "object" ? envFromScope : {};
|
|
23
|
+
return {
|
|
24
|
+
...process.env,
|
|
25
|
+
...source
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveDriverDialectId(driver) {
|
|
30
|
+
const source = driver && typeof driver === "object" ? driver : {};
|
|
31
|
+
const fromConstant = normalizeDatabaseClient(source.DIALECT_ID || source.dialectId || source.dialect, {
|
|
32
|
+
allowEmpty: true
|
|
33
|
+
});
|
|
34
|
+
if (fromConstant) {
|
|
35
|
+
return fromConstant;
|
|
36
|
+
}
|
|
37
|
+
if (typeof source.getDialectId === "function") {
|
|
38
|
+
return normalizeDatabaseClient(source.getDialectId(), {
|
|
39
|
+
allowEmpty: true
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveRequiredEnvString(env, key) {
|
|
46
|
+
const value = normalizeText(env?.[key]);
|
|
47
|
+
if (!value) {
|
|
48
|
+
throw new Error(`${key} is required for database runtime.`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolvePort(value, fallbackPort) {
|
|
54
|
+
const parsed = Number.parseInt(normalizeText(value), 10);
|
|
55
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
return fallbackPort;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadKnexFactory() {
|
|
62
|
+
let moduleValue;
|
|
63
|
+
try {
|
|
64
|
+
moduleValue = symlinkSafeRequire("knex");
|
|
65
|
+
} catch {
|
|
66
|
+
throw new Error(
|
|
67
|
+
"Knex package is not installed. Re-run `npx jskit update package database-runtime` to apply runtime dependencies."
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const knexFactory =
|
|
72
|
+
typeof moduleValue === "function"
|
|
73
|
+
? moduleValue
|
|
74
|
+
: typeof moduleValue?.default === "function"
|
|
75
|
+
? moduleValue.default
|
|
76
|
+
: null;
|
|
77
|
+
if (!knexFactory) {
|
|
78
|
+
throw new Error("Knex package resolved but did not expose a callable factory.");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return knexFactory;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveRegisteredDriver(scope) {
|
|
85
|
+
const drivers = resolveRegisteredDrivers(scope);
|
|
86
|
+
if (drivers.length === 1) {
|
|
87
|
+
return drivers[0];
|
|
88
|
+
}
|
|
89
|
+
if (drivers.length < 1) {
|
|
90
|
+
throw new Error("No database driver is registered. Install @jskit-ai/database-runtime-mysql or @jskit-ai/database-runtime-postgres.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const env = resolveDatabaseEnv(scope);
|
|
94
|
+
const preferredClient = normalizeDatabaseClient(env.DB_CLIENT, {
|
|
95
|
+
allowEmpty: true
|
|
96
|
+
});
|
|
97
|
+
if (!preferredClient) {
|
|
98
|
+
throw new Error("Multiple database drivers are registered. Set DB_CLIENT to mysql2 or pg, or keep exactly one database runtime driver package installed.");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const matched = drivers.find((driver) => resolveDriverDialectId(driver) === preferredClient) || null;
|
|
102
|
+
if (!matched) {
|
|
103
|
+
throw new Error(`Multiple database drivers are registered, but DB_CLIENT="${preferredClient}" did not match any registered driver.`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return matched;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveRegisteredDrivers(scope) {
|
|
110
|
+
const drivers = [];
|
|
111
|
+
|
|
112
|
+
if (scope.has(MYSQL_DRIVER_TOKEN)) {
|
|
113
|
+
drivers.push(scope.make(MYSQL_DRIVER_TOKEN));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (scope.has(POSTGRES_DRIVER_TOKEN)) {
|
|
117
|
+
drivers.push(scope.make(POSTGRES_DRIVER_TOKEN));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return drivers;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function resolveSingleRegisteredDriver(scope) {
|
|
124
|
+
return resolveRegisteredDriver(scope);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function createKnexConfig(scope) {
|
|
128
|
+
const env = resolveDatabaseEnv(scope);
|
|
129
|
+
const configuredClient = normalizeDatabaseClient(env.DB_CLIENT, {
|
|
130
|
+
allowEmpty: true
|
|
131
|
+
});
|
|
132
|
+
const driver = resolveRegisteredDriver(scope);
|
|
133
|
+
const dialectId = resolveDriverDialectId(driver);
|
|
134
|
+
|
|
135
|
+
if (!dialectId) {
|
|
136
|
+
throw new Error("Selected database driver did not expose a valid dialect id.");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (configuredClient && configuredClient !== dialectId) {
|
|
140
|
+
throw new Error(`DB_CLIENT="${configuredClient}" does not match installed database driver "${dialectId}".`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const client = toKnexClientId(dialectId);
|
|
144
|
+
const defaultPort = dialectId === "pg" ? 5432 : 3306;
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
client,
|
|
148
|
+
connection: {
|
|
149
|
+
host: normalizeText(env.DB_HOST) || "localhost",
|
|
150
|
+
port: resolvePort(env.DB_PORT, defaultPort),
|
|
151
|
+
database: resolveRequiredEnvString(env, "DB_NAME"),
|
|
152
|
+
user: resolveRequiredEnvString(env, "DB_USER"),
|
|
153
|
+
password: String(env.DB_PASSWORD ?? "")
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function createKnexInstance(scope) {
|
|
159
|
+
const knexFactory = loadKnexFactory();
|
|
160
|
+
const config = createKnexConfig(scope);
|
|
161
|
+
return knexFactory(config);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
class DatabaseRuntimeServiceProvider {
|
|
165
|
+
static id = DATABASE_RUNTIME_TOKEN;
|
|
166
|
+
|
|
167
|
+
register(app) {
|
|
168
|
+
if (!app || typeof app.singleton !== "function" || typeof app.has !== "function") {
|
|
169
|
+
throw new Error("DatabaseRuntimeServiceProvider requires application singleton()/has().");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
app.singleton(DATABASE_RUNTIME_TOKEN, () => DATABASE_RUNTIME_SERVER_API);
|
|
173
|
+
|
|
174
|
+
if (!app.has(DATABASE_DRIVER_TOKEN)) {
|
|
175
|
+
app.singleton(DATABASE_DRIVER_TOKEN, (scope) => resolveSingleRegisteredDriver(scope));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!app.has(KERNEL_TOKENS.Knex)) {
|
|
179
|
+
app.singleton(KERNEL_TOKENS.Knex, (scope) => createKnexInstance(scope));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!app.has(KERNEL_TOKENS.TransactionManager)) {
|
|
183
|
+
app.singleton(KERNEL_TOKENS.TransactionManager, (scope) => {
|
|
184
|
+
const knex = scope.make(KERNEL_TOKENS.Knex);
|
|
185
|
+
return createTransactionManager({ knex });
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
boot() {}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export { DatabaseRuntimeServiceProvider };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
|
|
3
|
+
function normalizeDatabaseClient(value, { allowEmpty = false } = {}) {
|
|
4
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
5
|
+
if (!normalized) {
|
|
6
|
+
if (allowEmpty) {
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
throw new Error("DB_CLIENT is required. Use mysql2 or pg.");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (normalized === "pg") {
|
|
13
|
+
return "pg";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (normalized === "mysql2") {
|
|
17
|
+
return "mysql2";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
throw new Error(`Unsupported DB_CLIENT "${normalized}". Use one of: mysql2, pg.`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toKnexClientId(databaseClient) {
|
|
24
|
+
return normalizeDatabaseClient(databaseClient);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
normalizeText,
|
|
29
|
+
normalizeDatabaseClient,
|
|
30
|
+
toKnexClientId
|
|
31
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
function normalizeDateInput(value) {
|
|
2
|
+
if (!value) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
7
|
+
if (Number.isNaN(date.getTime())) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toDateOrThrow(value) {
|
|
15
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
16
|
+
if (Number.isNaN(date.getTime())) {
|
|
17
|
+
throw new TypeError("Invalid date value.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return date;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pad(value, size = 2) {
|
|
24
|
+
return String(value).padStart(size, "0");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toIsoString(value) {
|
|
28
|
+
return toDateOrThrow(value).toISOString();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toInsertDateTime(dateLike, fallback = new Date()) {
|
|
32
|
+
const normalized = normalizeDateInput(dateLike) || normalizeDateInput(fallback) || new Date();
|
|
33
|
+
return toDatabaseDateTimeUtc(normalized);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toNullableDateTime(value) {
|
|
37
|
+
const normalized = normalizeDateInput(value);
|
|
38
|
+
if (!normalized) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return toDatabaseDateTimeUtc(normalized);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function toDatabaseDateTimeUtc(value) {
|
|
45
|
+
const date = toDateOrThrow(value);
|
|
46
|
+
const year = date.getUTCFullYear();
|
|
47
|
+
const month = pad(date.getUTCMonth() + 1);
|
|
48
|
+
const day = pad(date.getUTCDate());
|
|
49
|
+
const hours = pad(date.getUTCHours());
|
|
50
|
+
const minutes = pad(date.getUTCMinutes());
|
|
51
|
+
const seconds = pad(date.getUTCSeconds());
|
|
52
|
+
const milliseconds = pad(date.getUTCMilliseconds(), 3);
|
|
53
|
+
|
|
54
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { normalizeDateInput, toIsoString, toInsertDateTime, toNullableDateTime, toDatabaseDateTimeUtc };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function normalizeDialect(value) {
|
|
2
|
+
return String(value || "").trim().toLowerCase();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function detectDialectFromClient(client) {
|
|
6
|
+
const candidate =
|
|
7
|
+
client?.client?.config?.client ||
|
|
8
|
+
client?.client?.dialect ||
|
|
9
|
+
client?.client?.driverName ||
|
|
10
|
+
client?.client?.constructor?.name ||
|
|
11
|
+
"";
|
|
12
|
+
|
|
13
|
+
const normalized = normalizeDialect(candidate);
|
|
14
|
+
if (normalized.includes("postgres") || normalized === "pg") {
|
|
15
|
+
return "postgres";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (normalized.includes("mysql") || normalized.includes("maria")) {
|
|
19
|
+
return "mysql";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return normalized;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { normalizeDialect, detectDialectFromClient };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { detectDialectFromClient, normalizeDialect } from "./dialect.js";
|
|
2
|
+
|
|
3
|
+
function isMysqlDuplicateEntryError(error) {
|
|
4
|
+
const code = String(error?.code || "").trim().toUpperCase();
|
|
5
|
+
if (code === "ER_DUP_ENTRY") {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const errno = Number(error?.errno || error?.errorno || 0);
|
|
10
|
+
return errno === 1062;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isPostgresDuplicateEntryError(error) {
|
|
14
|
+
const code = String(error?.code || "").trim();
|
|
15
|
+
return code === "23505";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isDuplicateEntryError(error, { dialect = "", client = null } = {}) {
|
|
19
|
+
if (!error) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const resolvedDialect =
|
|
24
|
+
normalizeDialect(dialect) || (client ? normalizeDialect(detectDialectFromClient(client)) : "");
|
|
25
|
+
|
|
26
|
+
if (resolvedDialect === "postgres") {
|
|
27
|
+
return isPostgresDuplicateEntryError(error);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (resolvedDialect === "mysql") {
|
|
31
|
+
return isMysqlDuplicateEntryError(error);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return isMysqlDuplicateEntryError(error) || isPostgresDuplicateEntryError(error);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { isDuplicateEntryError };
|
|
@@ -0,0 +1,45 @@
|
|
|
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 { registerDatabaseRuntime } from "./runtime.js";
|
|
9
|
+
export {
|
|
10
|
+
normalizeDateInput,
|
|
11
|
+
toIsoString,
|
|
12
|
+
toInsertDateTime,
|
|
13
|
+
toNullableDateTime,
|
|
14
|
+
toDatabaseDateTimeUtc
|
|
15
|
+
} from "./dateUtils.js";
|
|
16
|
+
export { normalizeDialect, detectDialectFromClient } from "./dialect.js";
|
|
17
|
+
export { normalizeText, normalizeDatabaseClient, toKnexClientId } from "./databaseClient.js";
|
|
18
|
+
export { isDuplicateEntryError } from "./duplicateEntry.js";
|
|
19
|
+
export { normalizePath, jsonTextExpression, whereJsonTextEquals } from "./json.js";
|
|
20
|
+
export { DEFAULT_VISIBILITY_COLUMNS, applyVisibility, applyVisibilityOwners } from "./visibility.js";
|
|
21
|
+
export { createRepositoryScope } from "./repositoryScope.js";
|
|
22
|
+
export {
|
|
23
|
+
resolveQueryOptions,
|
|
24
|
+
resolveRepoClient,
|
|
25
|
+
applyForUpdate,
|
|
26
|
+
mapRowNullable,
|
|
27
|
+
parseJsonObject,
|
|
28
|
+
stringifyJsonObject,
|
|
29
|
+
parseMetadataJson,
|
|
30
|
+
stringifyMetadataJson,
|
|
31
|
+
normalizeMetadataJsonInput,
|
|
32
|
+
normalizeNullableString,
|
|
33
|
+
normalizeIdList,
|
|
34
|
+
normalizeCountRow,
|
|
35
|
+
parseJsonValue,
|
|
36
|
+
toDbJson
|
|
37
|
+
} from "./repositoryOptions.js";
|
|
38
|
+
export {
|
|
39
|
+
normalizeBatchSize,
|
|
40
|
+
normalizeCutoffDateOrThrow,
|
|
41
|
+
normalizeDeletedRowCount,
|
|
42
|
+
deleteRowsOlderThan,
|
|
43
|
+
__testables as retentionTestables
|
|
44
|
+
} from "./retention.js";
|
|
45
|
+
export { createRepoTransaction } from "./transactions.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
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 };
|
|
@@ -0,0 +1,61 @@
|
|
|
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 };
|