@zudojs/database 0.1.0 → 1.1.0
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/LICENSE +21 -0
- package/README.md +225 -17
- package/dist/cache/cache.memory.d.ts +167 -0
- package/dist/cache/cache.memory.js +186 -40
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +2 -2
- package/dist/database/database.core.d.ts +91 -0
- package/dist/database/database.core.js +21 -4
- package/dist/databaseClient/databaseClient.core.d.ts +171 -14
- package/dist/databaseClient/databaseClient.core.js +350 -70
- package/dist/databaseClient/databaseClient.errors.d.ts +112 -0
- package/dist/databaseClient/databaseClient.errors.js +425 -0
- package/dist/databaseClient/index.d.ts +2 -1
- package/dist/databaseClient/index.js +2 -1
- package/dist/databaseConnection/databaseConnection.manager.d.ts +163 -0
- package/dist/databaseConnection/databaseConnection.manager.js +138 -76
- package/dist/databaseConnection/index.d.ts +1 -1
- package/dist/databaseType/databaseType.type.d.ts +224 -0
- package/dist/databaseType/databaseType.type.js +0 -35
- package/dist/health/health.check.d.ts +74 -0
- package/dist/health/health.check.js +78 -47
- package/dist/health/index.d.ts +1 -1
- package/dist/health/index.js +1 -1
- package/dist/index.d.ts +14 -14
- package/dist/index.js +11 -11
- package/dist/locks/index.d.ts +2 -2
- package/dist/locks/index.js +2 -2
- package/dist/locks/locks.core.d.ts +132 -0
- package/dist/locks/locks.core.js +150 -85
- package/dist/migration/index.d.ts +3 -1
- package/dist/migration/index.js +2 -0
- package/dist/migration/migration.dialect.d.ts +65 -0
- package/dist/migration/migration.dialect.js +63 -0
- package/dist/migration/migration.helpers.d.ts +65 -0
- package/dist/migration/migration.helpers.js +60 -18
- package/dist/migration/migration.runner.d.ts +75 -0
- package/dist/migration/migration.runner.js +195 -107
- package/dist/migration/migration.types.d.ts +85 -0
- package/dist/pagination/index.d.ts +2 -1
- package/dist/pagination/index.js +2 -1
- package/dist/pagination/pagination.core.d.ts +163 -0
- package/dist/pagination/pagination.core.js +78 -9
- package/dist/pagination/pagination.keyset.d.ts +55 -0
- package/dist/pagination/pagination.keyset.js +108 -0
- package/dist/queryBuilder/index.d.ts +4 -3
- package/dist/queryBuilder/index.js +3 -2
- package/dist/queryBuilder/queryBuilder.core.d.ts +178 -0
- package/dist/queryBuilder/queryBuilder.core.js +92 -14
- package/dist/queryBuilder/queryBuilder.factory.d.ts +6 -0
- package/dist/queryBuilder/queryBuilder.factory.js +0 -25
- package/dist/queryBuilder/queryBuilder.filter.d.ts +170 -0
- package/dist/queryBuilder/queryBuilder.filter.js +191 -30
- package/dist/queryBuilder/queryBuilder.prisma.d.ts +60 -0
- package/dist/queryBuilder/queryBuilder.prisma.js +233 -0
- package/dist/queryBuilder/queryBuilder.type.d.ts +48 -0
- package/dist/relations/index.d.ts +1 -1
- package/dist/relations/index.js +1 -1
- package/dist/relations/relations.definition.d.ts +184 -0
- package/dist/relations/relations.definition.js +186 -19
- package/dist/repository/index.d.ts +2 -1
- package/dist/repository/index.js +1 -0
- package/dist/repository/repository.base.d.ts +283 -0
- package/dist/repository/repository.base.js +370 -89
- package/dist/repository/repository.errors.d.ts +60 -0
- package/dist/repository/repository.errors.js +235 -0
- package/dist/seed/index.d.ts +2 -2
- package/dist/seed/index.js +1 -1
- package/dist/seed/seed.runner.d.ts +160 -0
- package/dist/seed/seed.runner.js +194 -225
- package/dist/transaction/index.d.ts +1 -1
- package/dist/transaction/index.js +1 -1
- package/dist/transaction/transaction.core.d.ts +117 -0
- package/dist/transaction/transaction.core.js +83 -56
- package/dist/unitOfWork/unitOfWork.core.d.ts +44 -0
- package/dist/unitOfWork/unitOfWork.core.js +9 -20
- package/package.json +44 -24
- package/dist/cache/cache.memory.d.ts.map +0 -1
- package/dist/cache/cache.memory.js.map +0 -1
- package/dist/cache/index.d.ts.map +0 -1
- package/dist/cache/index.js.map +0 -1
- package/dist/database/database.core.d.ts.map +0 -1
- package/dist/database/database.core.js.map +0 -1
- package/dist/database/index.d.ts.map +0 -1
- package/dist/database/index.js.map +0 -1
- package/dist/databaseClient/databaseClient.core.d.ts.map +0 -1
- package/dist/databaseClient/databaseClient.core.js.map +0 -1
- package/dist/databaseClient/databaseClient.logger.d.ts.map +0 -1
- package/dist/databaseClient/databaseClient.logger.js.map +0 -1
- package/dist/databaseClient/index.d.ts.map +0 -1
- package/dist/databaseClient/index.js.map +0 -1
- package/dist/databaseConnection/databaseConnection.manager.d.ts.map +0 -1
- package/dist/databaseConnection/databaseConnection.manager.js.map +0 -1
- package/dist/databaseConnection/index.d.ts.map +0 -1
- package/dist/databaseConnection/index.js.map +0 -1
- package/dist/databaseType/databaseType.type.d.ts.map +0 -1
- package/dist/databaseType/databaseType.type.js.map +0 -1
- package/dist/databaseType/index.d.ts.map +0 -1
- package/dist/databaseType/index.js.map +0 -1
- package/dist/health/health.check.d.ts.map +0 -1
- package/dist/health/health.check.js.map +0 -1
- package/dist/health/index.d.ts.map +0 -1
- package/dist/health/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/locks/index.d.ts.map +0 -1
- package/dist/locks/index.js.map +0 -1
- package/dist/locks/locks.core.d.ts.map +0 -1
- package/dist/locks/locks.core.js.map +0 -1
- package/dist/migration/index.d.ts.map +0 -1
- package/dist/migration/index.js.map +0 -1
- package/dist/migration/migration.helpers.d.ts.map +0 -1
- package/dist/migration/migration.helpers.js.map +0 -1
- package/dist/migration/migration.runner.d.ts.map +0 -1
- package/dist/migration/migration.runner.js.map +0 -1
- package/dist/migration/migration.types.d.ts.map +0 -1
- package/dist/migration/migration.types.js.map +0 -1
- package/dist/pagination/index.d.ts.map +0 -1
- package/dist/pagination/index.js.map +0 -1
- package/dist/pagination/pagination.core.d.ts.map +0 -1
- package/dist/pagination/pagination.core.js.map +0 -1
- package/dist/queryBuilder/index.d.ts.map +0 -1
- package/dist/queryBuilder/index.js.map +0 -1
- package/dist/queryBuilder/queryBuilder.core.d.ts.map +0 -1
- package/dist/queryBuilder/queryBuilder.core.js.map +0 -1
- package/dist/queryBuilder/queryBuilder.factory.d.ts.map +0 -1
- package/dist/queryBuilder/queryBuilder.factory.js.map +0 -1
- package/dist/queryBuilder/queryBuilder.filter.d.ts.map +0 -1
- package/dist/queryBuilder/queryBuilder.filter.js.map +0 -1
- package/dist/queryBuilder/queryBuilder.type.d.ts.map +0 -1
- package/dist/queryBuilder/queryBuilder.type.js.map +0 -1
- package/dist/relations/index.d.ts.map +0 -1
- package/dist/relations/index.js.map +0 -1
- package/dist/relations/relations.definition.d.ts.map +0 -1
- package/dist/relations/relations.definition.js.map +0 -1
- package/dist/repository/index.d.ts.map +0 -1
- package/dist/repository/index.js.map +0 -1
- package/dist/repository/repository.base.d.ts.map +0 -1
- package/dist/repository/repository.base.js.map +0 -1
- package/dist/seed/index.d.ts.map +0 -1
- package/dist/seed/index.js.map +0 -1
- package/dist/seed/seed.runner.d.ts.map +0 -1
- package/dist/seed/seed.runner.js.map +0 -1
- package/dist/transaction/index.d.ts.map +0 -1
- package/dist/transaction/index.js.map +0 -1
- package/dist/transaction/transaction.core.d.ts.map +0 -1
- package/dist/transaction/transaction.core.js.map +0 -1
- package/dist/unitOfWork/index.d.ts.map +0 -1
- package/dist/unitOfWork/index.js.map +0 -1
- package/dist/unitOfWork/unitOfWork.core.d.ts.map +0 -1
- package/dist/unitOfWork/unitOfWork.core.js.map +0 -1
|
@@ -2,56 +2,113 @@
|
|
|
2
2
|
* @zudojs/database — Database Client Core
|
|
3
3
|
*
|
|
4
4
|
* Prisma-backed database client implementation.
|
|
5
|
+
*
|
|
6
|
+
* The client is a thin lifecycle wrapper over a Prisma client. It never
|
|
7
|
+
* reads runtime values from the generated `Prisma` namespace, so it works
|
|
8
|
+
* whether the consumer's generated client lives in `@prisma/client` or in a
|
|
9
|
+
* custom output directory (the Prisma 7 default).
|
|
10
|
+
*
|
|
11
|
+
* Only PostgreSQL is exercised by the runners, locks and health helpers in
|
|
12
|
+
* this package.
|
|
5
13
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import { DatabaseError } from "@zudojs/errors";
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import { DatabaseError, DatabaseOperation } from "@zudojs/errors";
|
|
8
16
|
import { createDefaultLogger } from "./databaseClient.logger.js";
|
|
9
|
-
|
|
17
|
+
import { normalizeDatabaseError } from "./databaseClient.errors.js";
|
|
18
|
+
/**
|
|
19
|
+
* Isolation levels accepted by Prisma's interactive transactions. The
|
|
20
|
+
* values are identical to the string names, so no namespace lookup is
|
|
21
|
+
* required.
|
|
22
|
+
*/
|
|
23
|
+
export const SUPPORTED_ISOLATION_LEVELS = Object.freeze([
|
|
24
|
+
"ReadUncommitted",
|
|
25
|
+
"ReadCommitted",
|
|
26
|
+
"RepeatableRead",
|
|
27
|
+
"Serializable",
|
|
28
|
+
]);
|
|
29
|
+
/**
|
|
30
|
+
* Prisma-backed database client.
|
|
31
|
+
*
|
|
32
|
+
* `connect()` de-duplicates concurrent calls through a shared in-flight
|
|
33
|
+
* promise, and `disconnect()` waits for an in-flight connect before
|
|
34
|
+
* tearing the client down.
|
|
35
|
+
*/
|
|
10
36
|
export class DatabaseClient {
|
|
11
37
|
prisma;
|
|
12
38
|
logger;
|
|
13
39
|
options;
|
|
14
40
|
status = "disconnected";
|
|
15
41
|
connectedAt;
|
|
42
|
+
connectPromise;
|
|
43
|
+
disconnectPromise;
|
|
16
44
|
constructor(options = {}) {
|
|
17
45
|
this.options = options;
|
|
18
46
|
this.logger = options.logger ?? createDefaultLogger();
|
|
19
|
-
this.prisma =
|
|
20
|
-
options.prisma ??
|
|
21
|
-
new PrismaClient({
|
|
22
|
-
log: options.logging
|
|
23
|
-
? [
|
|
24
|
-
{ emit: "event", level: "query" },
|
|
25
|
-
{ emit: "stdout", level: "error" },
|
|
26
|
-
{ emit: "stdout", level: "warn" },
|
|
27
|
-
]
|
|
28
|
-
: [{ emit: "stdout", level: "error" }],
|
|
29
|
-
});
|
|
47
|
+
this.prisma = options.prisma ?? createPrismaClient(options);
|
|
30
48
|
this.registerQueryLogging();
|
|
31
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns the underlying Prisma client.
|
|
52
|
+
*/
|
|
32
53
|
getPrisma() {
|
|
33
54
|
return this.prisma;
|
|
34
55
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Opens the connection. Concurrent calls share one in-flight attempt.
|
|
58
|
+
*/
|
|
59
|
+
connect() {
|
|
60
|
+
if (this.status === "connected")
|
|
61
|
+
return Promise.resolve();
|
|
62
|
+
if (this.connectPromise)
|
|
63
|
+
return this.connectPromise;
|
|
64
|
+
if (this.disconnectPromise) {
|
|
65
|
+
return this.disconnectPromise.then(() => this.connect());
|
|
66
|
+
}
|
|
38
67
|
this.status = "connecting";
|
|
68
|
+
this.connectPromise = this.performConnect().finally(() => {
|
|
69
|
+
this.connectPromise = undefined;
|
|
70
|
+
});
|
|
71
|
+
return this.connectPromise;
|
|
72
|
+
}
|
|
73
|
+
async performConnect() {
|
|
39
74
|
try {
|
|
40
75
|
await this.withTimeout(this.prisma.$connect(), this.options.connectionTimeoutMs ?? 10_000, "Database connection timed out.");
|
|
76
|
+
if (this.status !== "connecting") {
|
|
77
|
+
// A disconnect raced this connect; leave its final state alone.
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
41
80
|
this.status = "connected";
|
|
42
81
|
this.connectedAt = new Date();
|
|
43
82
|
this.logger.info("Database connected.");
|
|
44
83
|
}
|
|
45
84
|
catch (error) {
|
|
46
85
|
this.status = "error";
|
|
47
|
-
const normalized =
|
|
86
|
+
const normalized = normalizeDatabaseError(error, {
|
|
87
|
+
operation: DatabaseOperation.CONNECT,
|
|
88
|
+
fallbackMessage: "Database connection failed.",
|
|
89
|
+
});
|
|
48
90
|
this.logger.error(normalized.message, normalized);
|
|
49
91
|
throw normalized;
|
|
50
92
|
}
|
|
51
93
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Closes the connection. Waits for an in-flight connect first.
|
|
96
|
+
*/
|
|
97
|
+
disconnect() {
|
|
98
|
+
if (this.disconnectPromise)
|
|
99
|
+
return this.disconnectPromise;
|
|
100
|
+
if (this.status === "disconnected" && !this.connectPromise) {
|
|
101
|
+
return Promise.resolve();
|
|
102
|
+
}
|
|
103
|
+
this.disconnectPromise = this.performDisconnect().finally(() => {
|
|
104
|
+
this.disconnectPromise = undefined;
|
|
105
|
+
});
|
|
106
|
+
return this.disconnectPromise;
|
|
107
|
+
}
|
|
108
|
+
async performDisconnect() {
|
|
109
|
+
if (this.connectPromise) {
|
|
110
|
+
await this.connectPromise.catch(() => undefined);
|
|
111
|
+
}
|
|
55
112
|
this.status = "disconnecting";
|
|
56
113
|
try {
|
|
57
114
|
await this.prisma.$disconnect();
|
|
@@ -61,18 +118,27 @@ export class DatabaseClient {
|
|
|
61
118
|
}
|
|
62
119
|
catch (error) {
|
|
63
120
|
this.status = "error";
|
|
64
|
-
const normalized =
|
|
121
|
+
const normalized = normalizeDatabaseError(error, {
|
|
122
|
+
operation: DatabaseOperation.DISCONNECT,
|
|
123
|
+
fallbackMessage: "Database disconnection failed.",
|
|
124
|
+
});
|
|
65
125
|
this.logger.error(normalized.message, normalized);
|
|
66
126
|
throw normalized;
|
|
67
127
|
}
|
|
68
128
|
}
|
|
69
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Executes a lightweight `SELECT 1`.
|
|
131
|
+
*/
|
|
132
|
+
async ping(options = {}) {
|
|
70
133
|
try {
|
|
71
|
-
await this.prisma.$
|
|
134
|
+
await this.runRaw(() => this.prisma.$queryRawUnsafe("SELECT 1"), options, "Database ping timed out.");
|
|
72
135
|
}
|
|
73
136
|
catch (error) {
|
|
74
137
|
this.status = "error";
|
|
75
|
-
throw
|
|
138
|
+
throw normalizeDatabaseError(error, {
|
|
139
|
+
operation: DatabaseOperation.QUERY,
|
|
140
|
+
fallbackMessage: "Database ping failed.",
|
|
141
|
+
});
|
|
76
142
|
}
|
|
77
143
|
}
|
|
78
144
|
getStatus() {
|
|
@@ -81,18 +147,23 @@ export class DatabaseClient {
|
|
|
81
147
|
getConnectedAt() {
|
|
82
148
|
return this.connectedAt;
|
|
83
149
|
}
|
|
84
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Pings the database and reports the real lifecycle status.
|
|
152
|
+
*/
|
|
153
|
+
async healthCheck(options = {}) {
|
|
85
154
|
const startedAt = Date.now();
|
|
86
155
|
try {
|
|
87
|
-
await this.ping();
|
|
156
|
+
await this.ping(options);
|
|
88
157
|
return {
|
|
89
|
-
status:
|
|
158
|
+
status: this.status,
|
|
90
159
|
latencyMs: Date.now() - startedAt,
|
|
91
160
|
checkedAt: new Date(),
|
|
92
161
|
};
|
|
93
162
|
}
|
|
94
163
|
catch (error) {
|
|
95
|
-
const normalized =
|
|
164
|
+
const normalized = normalizeDatabaseError(error, {
|
|
165
|
+
fallbackMessage: "Database health check failed.",
|
|
166
|
+
});
|
|
96
167
|
return {
|
|
97
168
|
status: "error",
|
|
98
169
|
latencyMs: Date.now() - startedAt,
|
|
@@ -101,36 +172,108 @@ export class DatabaseClient {
|
|
|
101
172
|
};
|
|
102
173
|
}
|
|
103
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Runs a callback inside a Prisma interactive transaction.
|
|
177
|
+
*
|
|
178
|
+
* When `options.signal` aborts, the abort is raised *inside* the
|
|
179
|
+
* transaction callback so Prisma rolls the transaction back; the caller
|
|
180
|
+
* is released at the same moment. Racing only the outer promise let the
|
|
181
|
+
* callback finish and the transaction commit after the caller had
|
|
182
|
+
* already been told it was aborted.
|
|
183
|
+
*/
|
|
104
184
|
async transaction(callback, options = {}) {
|
|
105
|
-
if (typeof callback !== "function")
|
|
185
|
+
if (typeof callback !== "function") {
|
|
106
186
|
throw new TypeError("A transaction callback is required.");
|
|
187
|
+
}
|
|
188
|
+
throwIfAborted(options.signal);
|
|
107
189
|
await this.ensureConnected();
|
|
108
|
-
const transactionOptions =
|
|
190
|
+
const transactionOptions = buildPrismaTransactionOptions(options);
|
|
109
191
|
try {
|
|
110
|
-
return await this.prisma.$transaction(async (transaction) => callback(transaction), transactionOptions);
|
|
192
|
+
return await raceAbort(this.prisma.$transaction(async (transaction) => raceAbort(callback(transaction), options.signal), transactionOptions), options.signal);
|
|
111
193
|
}
|
|
112
194
|
catch (error) {
|
|
113
|
-
const normalized =
|
|
195
|
+
const normalized = normalizeDatabaseError(error, {
|
|
196
|
+
operation: DatabaseOperation.TRANSACTION,
|
|
197
|
+
fallbackMessage: "Database transaction failed.",
|
|
198
|
+
metadata: options.metadata,
|
|
199
|
+
});
|
|
114
200
|
this.logger.error(normalized.message, normalized);
|
|
115
201
|
throw normalized;
|
|
116
202
|
}
|
|
117
203
|
}
|
|
118
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Executes a raw statement with positional parameters and returns the
|
|
206
|
+
* affected row count.
|
|
207
|
+
*/
|
|
208
|
+
async executeRawUnsafe(sql, values = [], options = {}) {
|
|
209
|
+
validateSql(sql);
|
|
119
210
|
await this.ensureConnected();
|
|
120
211
|
try {
|
|
121
|
-
return await this.prisma.$
|
|
212
|
+
return await this.runRaw(() => this.prisma.$executeRawUnsafe(sql, ...values), options, "Raw database execution timed out.");
|
|
122
213
|
}
|
|
123
214
|
catch (error) {
|
|
124
|
-
throw
|
|
215
|
+
throw normalizeDatabaseError(error, {
|
|
216
|
+
operation: DatabaseOperation.QUERY,
|
|
217
|
+
fallbackMessage: "Raw database execution failed.",
|
|
218
|
+
metadata: options.metadata,
|
|
219
|
+
});
|
|
125
220
|
}
|
|
126
221
|
}
|
|
127
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Executes a raw query with positional parameters.
|
|
224
|
+
*/
|
|
225
|
+
async queryRawUnsafe(sql, values = [], options = {}) {
|
|
226
|
+
validateSql(sql);
|
|
128
227
|
await this.ensureConnected();
|
|
129
228
|
try {
|
|
130
|
-
return
|
|
229
|
+
return await this.runRaw(() => this.prisma.$queryRawUnsafe(sql, ...values), options, "Raw database query timed out.");
|
|
131
230
|
}
|
|
132
231
|
catch (error) {
|
|
133
|
-
throw
|
|
232
|
+
throw normalizeDatabaseError(error, {
|
|
233
|
+
operation: DatabaseOperation.QUERY,
|
|
234
|
+
fallbackMessage: "Raw database query failed.",
|
|
235
|
+
metadata: options.metadata,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Executes a `Prisma.sql` tagged statement.
|
|
241
|
+
*
|
|
242
|
+
* Only available when the underlying client supports `$executeRaw`.
|
|
243
|
+
*/
|
|
244
|
+
async executeRaw(query, options = {}) {
|
|
245
|
+
const prisma = this.prisma;
|
|
246
|
+
if (typeof prisma.$executeRaw !== "function") {
|
|
247
|
+
throw new TypeError("The Prisma client does not support $executeRaw.");
|
|
248
|
+
}
|
|
249
|
+
await this.ensureConnected();
|
|
250
|
+
try {
|
|
251
|
+
return await this.runRaw(() => prisma.$executeRaw(query), options, "Raw database execution timed out.");
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
throw normalizeDatabaseError(error, {
|
|
255
|
+
operation: DatabaseOperation.QUERY,
|
|
256
|
+
fallbackMessage: "Raw database execution failed.",
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Executes a `Prisma.sql` tagged query.
|
|
262
|
+
*/
|
|
263
|
+
async queryRaw(query, options = {}) {
|
|
264
|
+
const prisma = this.prisma;
|
|
265
|
+
if (typeof prisma.$queryRaw !== "function") {
|
|
266
|
+
throw new TypeError("The Prisma client does not support $queryRaw.");
|
|
267
|
+
}
|
|
268
|
+
await this.ensureConnected();
|
|
269
|
+
try {
|
|
270
|
+
return await this.runRaw(() => prisma.$queryRaw(query), options, "Raw database query timed out.");
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
throw normalizeDatabaseError(error, {
|
|
274
|
+
operation: DatabaseOperation.QUERY,
|
|
275
|
+
fallbackMessage: "Raw database query failed.",
|
|
276
|
+
});
|
|
134
277
|
}
|
|
135
278
|
}
|
|
136
279
|
async ensureConnected() {
|
|
@@ -143,58 +286,195 @@ export class DatabaseClient {
|
|
|
143
286
|
registerQueryLogging() {
|
|
144
287
|
if (!this.options.logging)
|
|
145
288
|
return;
|
|
146
|
-
|
|
147
|
-
prisma.$on?.("query", (event) => {
|
|
289
|
+
this.prisma.$on?.("query", (event) => {
|
|
148
290
|
this.logger.debug("Database query executed.", {
|
|
149
291
|
durationMs: event.duration,
|
|
150
292
|
target: event.target,
|
|
151
293
|
});
|
|
152
294
|
});
|
|
153
295
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
case "ReadUncommitted":
|
|
166
|
-
return Prisma.TransactionIsolationLevel.ReadUncommitted;
|
|
167
|
-
case "ReadCommitted":
|
|
168
|
-
return Prisma.TransactionIsolationLevel.ReadCommitted;
|
|
169
|
-
case "RepeatableRead":
|
|
170
|
-
return Prisma.TransactionIsolationLevel.RepeatableRead;
|
|
171
|
-
case "Serializable":
|
|
172
|
-
return Prisma.TransactionIsolationLevel.Serializable;
|
|
173
|
-
default:
|
|
174
|
-
throw new TypeError(`Unsupported transaction isolation level: ${String(level)}`);
|
|
175
|
-
}
|
|
296
|
+
/**
|
|
297
|
+
* Applies `signal` and `timeoutMs` to a raw operation.
|
|
298
|
+
*
|
|
299
|
+
* Timeouts are client-side only: the caller stops waiting, but the
|
|
300
|
+
* statement keeps running on the server until it completes. Use
|
|
301
|
+
* `statement_timeout` for server-side cancellation.
|
|
302
|
+
*/
|
|
303
|
+
runRaw(operation, options, timeoutMessage) {
|
|
304
|
+
throwIfAborted(options.signal);
|
|
305
|
+
const promise = raceAbort(operation(), options.signal);
|
|
306
|
+
return this.withTimeout(promise, options.timeoutMs ?? 0, timeoutMessage);
|
|
176
307
|
}
|
|
177
308
|
async withTimeout(promise, timeoutMs, message) {
|
|
178
|
-
if (timeoutMs <= 0)
|
|
309
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
179
310
|
return promise;
|
|
180
311
|
let timer;
|
|
181
312
|
const timeout = new Promise((_, reject) => {
|
|
182
313
|
timer = setTimeout(() => {
|
|
183
|
-
reject(new DatabaseError(message
|
|
314
|
+
reject(new DatabaseError(message, {
|
|
315
|
+
code: "ERR_DATABASE_TIMEOUT",
|
|
316
|
+
statusCode: 503,
|
|
317
|
+
metadata: { timeoutMs },
|
|
318
|
+
}));
|
|
184
319
|
}, timeoutMs);
|
|
185
320
|
});
|
|
186
321
|
try {
|
|
187
322
|
return await Promise.race([promise, timeout]);
|
|
188
323
|
}
|
|
189
324
|
finally {
|
|
190
|
-
|
|
325
|
+
if (timer)
|
|
326
|
+
clearTimeout(timer);
|
|
191
327
|
}
|
|
192
328
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
329
|
+
}
|
|
330
|
+
const requirePeer = createRequire(import.meta.url);
|
|
331
|
+
let cachedPrismaClientConstructor;
|
|
332
|
+
/**
|
|
333
|
+
* Resolves the generated `PrismaClient` constructor on first use.
|
|
334
|
+
*
|
|
335
|
+
* The lookup is deliberately lazy. `@prisma/client` is a peer dependency, and
|
|
336
|
+
* until the consumer runs `prisma generate` the installed package is a stub
|
|
337
|
+
* that exports no `PrismaClient`. A static value import therefore made merely
|
|
338
|
+
* importing `@zudojs/database` fail with a bare `SyntaxError` — including for
|
|
339
|
+
* consumers who pass their own client through `options.prisma`, or whose
|
|
340
|
+
* generated client lives in a custom output directory (the Prisma 7 default)
|
|
341
|
+
* and so never needs this constructor at all.
|
|
342
|
+
*
|
|
343
|
+
* @throws {DatabaseError} when no generated client can be resolved.
|
|
344
|
+
*/
|
|
345
|
+
function resolvePrismaClientConstructor() {
|
|
346
|
+
if (cachedPrismaClientConstructor)
|
|
347
|
+
return cachedPrismaClientConstructor;
|
|
348
|
+
const guidance = "Install it and run `prisma generate`, or pass an already-constructed client as `prisma` in the DatabaseClient options.";
|
|
349
|
+
let module;
|
|
350
|
+
try {
|
|
351
|
+
module = requirePeer("@prisma/client");
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
throw new DatabaseError(`DatabaseClient could not load the "@prisma/client" peer dependency. ${guidance}`, {
|
|
355
|
+
code: "ERR_DATABASE_CONNECTION",
|
|
356
|
+
operation: DatabaseOperation.CONNECT,
|
|
357
|
+
isOperational: false,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (typeof module.PrismaClient !== "function") {
|
|
361
|
+
throw new DatabaseError(`"@prisma/client" is installed but exports no PrismaClient, which means the client has not been generated yet. ${guidance}`, {
|
|
362
|
+
code: "ERR_DATABASE_CONNECTION",
|
|
363
|
+
operation: DatabaseOperation.CONNECT,
|
|
364
|
+
isOperational: false,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
cachedPrismaClientConstructor = module.PrismaClient;
|
|
368
|
+
return cachedPrismaClientConstructor;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Builds a Prisma client from the supplied options.
|
|
372
|
+
*
|
|
373
|
+
* @throws {DatabaseError} when neither `prisma` nor `adapter` is provided.
|
|
374
|
+
*/
|
|
375
|
+
function createPrismaClient(options) {
|
|
376
|
+
if (!options.adapter) {
|
|
377
|
+
throw new DatabaseError("DatabaseClient requires either a pre-built `prisma` client or a Prisma driver `adapter` (for example @prisma/adapter-pg).", {
|
|
378
|
+
code: "ERR_DATABASE_CONNECTION",
|
|
379
|
+
operation: DatabaseOperation.CONNECT,
|
|
380
|
+
isOperational: false,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
const log = options.logging
|
|
384
|
+
? [
|
|
385
|
+
{ emit: "event", level: "query" },
|
|
386
|
+
{ emit: "stdout", level: "error" },
|
|
387
|
+
{ emit: "stdout", level: "warn" },
|
|
388
|
+
]
|
|
389
|
+
: [{ emit: "stdout", level: "error" }];
|
|
390
|
+
try {
|
|
391
|
+
const Constructor = resolvePrismaClientConstructor();
|
|
392
|
+
return new Constructor({ adapter: options.adapter, log });
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
throw normalizeDatabaseError(error, {
|
|
396
|
+
operation: DatabaseOperation.CONNECT,
|
|
397
|
+
fallbackMessage: "Failed to construct the Prisma client.",
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Maps package transaction options onto Prisma's interactive transaction
|
|
403
|
+
* options. Isolation levels are validated against the supported list and
|
|
404
|
+
* passed through as strings.
|
|
405
|
+
*/
|
|
406
|
+
export function buildPrismaTransactionOptions(options = {}) {
|
|
407
|
+
const result = {};
|
|
408
|
+
if (options.maxWaitMs !== undefined) {
|
|
409
|
+
validatePositive(options.maxWaitMs, "maxWaitMs");
|
|
410
|
+
result.maxWait = Math.floor(options.maxWaitMs);
|
|
411
|
+
}
|
|
412
|
+
if (options.timeoutMs !== undefined) {
|
|
413
|
+
validatePositive(options.timeoutMs, "timeoutMs");
|
|
414
|
+
result.timeout = Math.floor(options.timeoutMs);
|
|
197
415
|
}
|
|
416
|
+
if (options.isolationLevel !== undefined) {
|
|
417
|
+
if (!SUPPORTED_ISOLATION_LEVELS.includes(options.isolationLevel)) {
|
|
418
|
+
throw new TypeError(`Unsupported transaction isolation level: ${String(options.isolationLevel)}`);
|
|
419
|
+
}
|
|
420
|
+
result.isolationLevel = options.isolationLevel;
|
|
421
|
+
}
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
function validatePositive(value, name) {
|
|
425
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
426
|
+
throw new TypeError(`Transaction ${name} must be a positive finite number.`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function validateSql(sql) {
|
|
430
|
+
if (typeof sql !== "string" || sql.trim().length === 0) {
|
|
431
|
+
throw new TypeError("A non-empty SQL string is required.");
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Throws an abort-typed `DatabaseError` when the signal is already aborted.
|
|
436
|
+
*/
|
|
437
|
+
export function throwIfAborted(signal) {
|
|
438
|
+
if (signal?.aborted)
|
|
439
|
+
throw createAbortError(signal);
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Error raised when an operation is cancelled through an `AbortSignal`.
|
|
443
|
+
*/
|
|
444
|
+
export class DatabaseAbortError extends DatabaseError {
|
|
445
|
+
constructor(signal) {
|
|
446
|
+
super("Database operation aborted.", {
|
|
447
|
+
code: "ERR_ABORTED",
|
|
448
|
+
statusCode: 499,
|
|
449
|
+
metadata: { aborted: true },
|
|
450
|
+
cause: signal?.reason,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Creates the error used when an operation is aborted through a signal.
|
|
456
|
+
*/
|
|
457
|
+
export function createAbortError(signal) {
|
|
458
|
+
return new DatabaseAbortError(signal);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Rejects as soon as the signal aborts, even if the operation is still
|
|
462
|
+
* running. The abort listener is removed once the operation settles.
|
|
463
|
+
*/
|
|
464
|
+
export function raceAbort(promise, signal) {
|
|
465
|
+
if (!signal)
|
|
466
|
+
return promise;
|
|
467
|
+
if (signal.aborted) {
|
|
468
|
+
promise.catch(() => undefined);
|
|
469
|
+
return Promise.reject(createAbortError(signal));
|
|
470
|
+
}
|
|
471
|
+
return new Promise((resolve, reject) => {
|
|
472
|
+
const onAbort = () => reject(createAbortError(signal));
|
|
473
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
474
|
+
promise.then(resolve, reject).finally(() => {
|
|
475
|
+
signal.removeEventListener("abort", onAbort);
|
|
476
|
+
});
|
|
477
|
+
});
|
|
198
478
|
}
|
|
199
479
|
/** Creates a database client. */
|
|
200
480
|
export function createDatabaseClient(options = {}) {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/database — Error normalisation
|
|
3
|
+
*
|
|
4
|
+
* Converts Prisma (and arbitrary) failures into `DatabaseError` instances
|
|
5
|
+
* that carry the Prisma error code as `databaseCode`, the operation that
|
|
6
|
+
* failed, and an HTTP-friendly status/code so callers can distinguish a
|
|
7
|
+
* unique violation from a missing row from an outage without reaching into
|
|
8
|
+
* `cause`.
|
|
9
|
+
*
|
|
10
|
+
* Prisma errors are detected structurally (a `code` such as `P2002` plus a
|
|
11
|
+
* `clientVersion`), so the mapping works regardless of which copy of
|
|
12
|
+
* `@prisma/client` produced the error.
|
|
13
|
+
*/
|
|
14
|
+
import { DatabaseError, DatabaseOperation, type DatabaseErrorOptions } from "@zudojs/errors";
|
|
15
|
+
import type { DatabaseErrorInfo } from "../databaseType/databaseType.type.js";
|
|
16
|
+
/**
|
|
17
|
+
* Structural `DatabaseError` guard.
|
|
18
|
+
*
|
|
19
|
+
* `@zudojs/errors` resolves from the npm registry (exact `0.1.0` pins are a
|
|
20
|
+
* repo-wide convention), so a consumer may hold a second copy of the
|
|
21
|
+
* package. `instanceof` would then fail and errors would be double-wrapped;
|
|
22
|
+
* this guard accepts any `Error` that carries the `DatabaseError` shape
|
|
23
|
+
* (`category: "database"` plus a string `code`).
|
|
24
|
+
*/
|
|
25
|
+
export declare function isDatabaseErrorLike(value: unknown): value is DatabaseError;
|
|
26
|
+
/**
|
|
27
|
+
* Semantic outcome of a database failure.
|
|
28
|
+
*/
|
|
29
|
+
export type DatabaseErrorKind = "conflict" | "not-found" | "constraint" | "serialization" | "timeout" | "connection" | "validation" | "unknown";
|
|
30
|
+
/**
|
|
31
|
+
* Options for {@link normalizeDatabaseError}.
|
|
32
|
+
*/
|
|
33
|
+
export interface NormalizeDatabaseErrorOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Operation that failed. Used when the error does not already carry one.
|
|
36
|
+
*/
|
|
37
|
+
readonly operation?: DatabaseOperation;
|
|
38
|
+
/**
|
|
39
|
+
* Message used when the underlying error has none, or when the underlying
|
|
40
|
+
* message must not be exposed (connection failures).
|
|
41
|
+
*/
|
|
42
|
+
readonly fallbackMessage?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Extra metadata merged into the normalised error.
|
|
45
|
+
*/
|
|
46
|
+
readonly metadata?: DatabaseErrorOptions["metadata"];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Minimal structural view of a Prisma known-request / initialization error.
|
|
50
|
+
*/
|
|
51
|
+
export interface PrismaErrorLike {
|
|
52
|
+
readonly code?: string;
|
|
53
|
+
readonly errorCode?: string;
|
|
54
|
+
readonly meta?: Readonly<Record<string, unknown>>;
|
|
55
|
+
readonly clientVersion?: string;
|
|
56
|
+
readonly message?: string;
|
|
57
|
+
readonly retryable?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Codes that indicate a transient transaction failure worth retrying.
|
|
61
|
+
*/
|
|
62
|
+
export declare const RETRYABLE_DATABASE_CODES: ReadonlySet<string>;
|
|
63
|
+
/**
|
|
64
|
+
* Detects a Prisma error structurally.
|
|
65
|
+
*/
|
|
66
|
+
export declare function isPrismaError(value: unknown): value is PrismaErrorLike;
|
|
67
|
+
/**
|
|
68
|
+
* Returns the Prisma / driver error code carried by an error, if any.
|
|
69
|
+
*/
|
|
70
|
+
export declare function getDatabaseErrorCode(error: unknown): string | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Classifies a database error.
|
|
73
|
+
*/
|
|
74
|
+
export declare function getDatabaseErrorKind(error: unknown): DatabaseErrorKind;
|
|
75
|
+
/**
|
|
76
|
+
* Determines whether a transaction failure is safe to retry.
|
|
77
|
+
*
|
|
78
|
+
* Retrying re-runs the whole callback, so callbacks passed to
|
|
79
|
+
* `withTransactionRetry` must be idempotent outside the transaction.
|
|
80
|
+
*/
|
|
81
|
+
export declare function isRetryableTransactionError(error: unknown): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Determines whether an error represents a unique-constraint conflict.
|
|
84
|
+
*/
|
|
85
|
+
export declare function isConflictError(error: unknown): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Determines whether an error represents a missing record.
|
|
88
|
+
*/
|
|
89
|
+
export declare function isNotFoundError(error: unknown): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Returns a copy of a `DatabaseError` with extra metadata, preserving
|
|
92
|
+
* `operation`, `driver` and `databaseCode` (which `BaseError.withMetadata`
|
|
93
|
+
* drops).
|
|
94
|
+
*/
|
|
95
|
+
export declare function withDatabaseErrorMetadata(error: DatabaseError, metadata: NonNullable<DatabaseErrorOptions["metadata"]>): DatabaseError;
|
|
96
|
+
/**
|
|
97
|
+
* Converts any thrown value into a `DatabaseError`.
|
|
98
|
+
*
|
|
99
|
+
* Existing `DatabaseError`s are returned as-is unless `operation` or
|
|
100
|
+
* `metadata` need to be attached, in which case a copy carrying the extra
|
|
101
|
+
* information (and the original as `cause`) is returned. Prisma errors are
|
|
102
|
+
* mapped by code; connection failures get a fixed message so that host
|
|
103
|
+
* names from Prisma's messages are never exposed.
|
|
104
|
+
*/
|
|
105
|
+
export declare function normalizeDatabaseError(error: unknown, options?: NormalizeDatabaseErrorOptions): DatabaseError;
|
|
106
|
+
/**
|
|
107
|
+
* Converts any failure into a plain, serialisable {@link DatabaseErrorInfo}
|
|
108
|
+
* (the shape exported for logging and API responses). The error is
|
|
109
|
+
* normalised first, so Prisma codes appear as `code`.
|
|
110
|
+
*/
|
|
111
|
+
export declare function toDatabaseErrorInfo(error: unknown, options?: NormalizeDatabaseErrorOptions): DatabaseErrorInfo;
|
|
112
|
+
//# sourceMappingURL=databaseClient.errors.d.ts.map
|