@zudojs/database 0.1.0 → 1.0.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 +223 -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 +165 -14
- package/dist/databaseClient/databaseClient.core.js +304 -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 +128 -0
- package/dist/locks/locks.core.js +144 -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 +270 -0
- package/dist/repository/repository.base.js +350 -84
- 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 +40 -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 { PrismaClient
|
|
7
|
-
import { DatabaseError } from "@zudojs/errors";
|
|
14
|
+
import { PrismaClient } from "@prisma/client";
|
|
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,102 @@ export class DatabaseClient {
|
|
|
101
172
|
};
|
|
102
173
|
}
|
|
103
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Runs a callback inside a Prisma interactive transaction.
|
|
177
|
+
*/
|
|
104
178
|
async transaction(callback, options = {}) {
|
|
105
|
-
if (typeof callback !== "function")
|
|
179
|
+
if (typeof callback !== "function") {
|
|
106
180
|
throw new TypeError("A transaction callback is required.");
|
|
181
|
+
}
|
|
182
|
+
throwIfAborted(options.signal);
|
|
107
183
|
await this.ensureConnected();
|
|
108
|
-
const transactionOptions =
|
|
184
|
+
const transactionOptions = buildPrismaTransactionOptions(options);
|
|
109
185
|
try {
|
|
110
|
-
return await this.prisma.$transaction(async (transaction) => callback(transaction), transactionOptions);
|
|
186
|
+
return await raceAbort(this.prisma.$transaction(async (transaction) => callback(transaction), transactionOptions), options.signal);
|
|
111
187
|
}
|
|
112
188
|
catch (error) {
|
|
113
|
-
const normalized =
|
|
189
|
+
const normalized = normalizeDatabaseError(error, {
|
|
190
|
+
operation: DatabaseOperation.TRANSACTION,
|
|
191
|
+
fallbackMessage: "Database transaction failed.",
|
|
192
|
+
metadata: options.metadata,
|
|
193
|
+
});
|
|
114
194
|
this.logger.error(normalized.message, normalized);
|
|
115
195
|
throw normalized;
|
|
116
196
|
}
|
|
117
197
|
}
|
|
118
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Executes a raw statement with positional parameters and returns the
|
|
200
|
+
* affected row count.
|
|
201
|
+
*/
|
|
202
|
+
async executeRawUnsafe(sql, values = [], options = {}) {
|
|
203
|
+
validateSql(sql);
|
|
204
|
+
await this.ensureConnected();
|
|
205
|
+
try {
|
|
206
|
+
return await this.runRaw(() => this.prisma.$executeRawUnsafe(sql, ...values), options, "Raw database execution timed out.");
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
throw normalizeDatabaseError(error, {
|
|
210
|
+
operation: DatabaseOperation.QUERY,
|
|
211
|
+
fallbackMessage: "Raw database execution failed.",
|
|
212
|
+
metadata: options.metadata,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Executes a raw query with positional parameters.
|
|
218
|
+
*/
|
|
219
|
+
async queryRawUnsafe(sql, values = [], options = {}) {
|
|
220
|
+
validateSql(sql);
|
|
119
221
|
await this.ensureConnected();
|
|
120
222
|
try {
|
|
121
|
-
return await this.prisma.$
|
|
223
|
+
return await this.runRaw(() => this.prisma.$queryRawUnsafe(sql, ...values), options, "Raw database query timed out.");
|
|
122
224
|
}
|
|
123
225
|
catch (error) {
|
|
124
|
-
throw
|
|
226
|
+
throw normalizeDatabaseError(error, {
|
|
227
|
+
operation: DatabaseOperation.QUERY,
|
|
228
|
+
fallbackMessage: "Raw database query failed.",
|
|
229
|
+
metadata: options.metadata,
|
|
230
|
+
});
|
|
125
231
|
}
|
|
126
232
|
}
|
|
127
|
-
|
|
233
|
+
/**
|
|
234
|
+
* Executes a `Prisma.sql` tagged statement.
|
|
235
|
+
*
|
|
236
|
+
* Only available when the underlying client supports `$executeRaw`.
|
|
237
|
+
*/
|
|
238
|
+
async executeRaw(query, options = {}) {
|
|
239
|
+
const prisma = this.prisma;
|
|
240
|
+
if (typeof prisma.$executeRaw !== "function") {
|
|
241
|
+
throw new TypeError("The Prisma client does not support $executeRaw.");
|
|
242
|
+
}
|
|
128
243
|
await this.ensureConnected();
|
|
129
244
|
try {
|
|
130
|
-
return
|
|
245
|
+
return await this.runRaw(() => prisma.$executeRaw(query), options, "Raw database execution timed out.");
|
|
131
246
|
}
|
|
132
247
|
catch (error) {
|
|
133
|
-
throw
|
|
248
|
+
throw normalizeDatabaseError(error, {
|
|
249
|
+
operation: DatabaseOperation.QUERY,
|
|
250
|
+
fallbackMessage: "Raw database execution failed.",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Executes a `Prisma.sql` tagged query.
|
|
256
|
+
*/
|
|
257
|
+
async queryRaw(query, options = {}) {
|
|
258
|
+
const prisma = this.prisma;
|
|
259
|
+
if (typeof prisma.$queryRaw !== "function") {
|
|
260
|
+
throw new TypeError("The Prisma client does not support $queryRaw.");
|
|
261
|
+
}
|
|
262
|
+
await this.ensureConnected();
|
|
263
|
+
try {
|
|
264
|
+
return await this.runRaw(() => prisma.$queryRaw(query), options, "Raw database query timed out.");
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
throw normalizeDatabaseError(error, {
|
|
268
|
+
operation: DatabaseOperation.QUERY,
|
|
269
|
+
fallbackMessage: "Raw database query failed.",
|
|
270
|
+
});
|
|
134
271
|
}
|
|
135
272
|
}
|
|
136
273
|
async ensureConnected() {
|
|
@@ -143,58 +280,155 @@ export class DatabaseClient {
|
|
|
143
280
|
registerQueryLogging() {
|
|
144
281
|
if (!this.options.logging)
|
|
145
282
|
return;
|
|
146
|
-
|
|
147
|
-
prisma.$on?.("query", (event) => {
|
|
283
|
+
this.prisma.$on?.("query", (event) => {
|
|
148
284
|
this.logger.debug("Database query executed.", {
|
|
149
285
|
durationMs: event.duration,
|
|
150
286
|
target: event.target,
|
|
151
287
|
});
|
|
152
288
|
});
|
|
153
289
|
}
|
|
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
|
-
}
|
|
290
|
+
/**
|
|
291
|
+
* Applies `signal` and `timeoutMs` to a raw operation.
|
|
292
|
+
*
|
|
293
|
+
* Timeouts are client-side only: the caller stops waiting, but the
|
|
294
|
+
* statement keeps running on the server until it completes. Use
|
|
295
|
+
* `statement_timeout` for server-side cancellation.
|
|
296
|
+
*/
|
|
297
|
+
runRaw(operation, options, timeoutMessage) {
|
|
298
|
+
throwIfAborted(options.signal);
|
|
299
|
+
const promise = raceAbort(operation(), options.signal);
|
|
300
|
+
return this.withTimeout(promise, options.timeoutMs ?? 0, timeoutMessage);
|
|
176
301
|
}
|
|
177
302
|
async withTimeout(promise, timeoutMs, message) {
|
|
178
|
-
if (timeoutMs <= 0)
|
|
303
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
179
304
|
return promise;
|
|
180
305
|
let timer;
|
|
181
306
|
const timeout = new Promise((_, reject) => {
|
|
182
307
|
timer = setTimeout(() => {
|
|
183
|
-
reject(new DatabaseError(message
|
|
308
|
+
reject(new DatabaseError(message, {
|
|
309
|
+
code: "ERR_DATABASE_TIMEOUT",
|
|
310
|
+
statusCode: 503,
|
|
311
|
+
metadata: { timeoutMs },
|
|
312
|
+
}));
|
|
184
313
|
}, timeoutMs);
|
|
185
314
|
});
|
|
186
315
|
try {
|
|
187
316
|
return await Promise.race([promise, timeout]);
|
|
188
317
|
}
|
|
189
318
|
finally {
|
|
190
|
-
|
|
319
|
+
if (timer)
|
|
320
|
+
clearTimeout(timer);
|
|
191
321
|
}
|
|
192
322
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Builds a Prisma client from the supplied options.
|
|
326
|
+
*
|
|
327
|
+
* @throws {DatabaseError} when neither `prisma` nor `adapter` is provided.
|
|
328
|
+
*/
|
|
329
|
+
function createPrismaClient(options) {
|
|
330
|
+
if (!options.adapter) {
|
|
331
|
+
throw new DatabaseError("DatabaseClient requires either a pre-built `prisma` client or a Prisma driver `adapter` (for example @prisma/adapter-pg).", {
|
|
332
|
+
code: "ERR_DATABASE_CONNECTION",
|
|
333
|
+
operation: DatabaseOperation.CONNECT,
|
|
334
|
+
isOperational: false,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
const log = options.logging
|
|
338
|
+
? [
|
|
339
|
+
{ emit: "event", level: "query" },
|
|
340
|
+
{ emit: "stdout", level: "error" },
|
|
341
|
+
{ emit: "stdout", level: "warn" },
|
|
342
|
+
]
|
|
343
|
+
: [{ emit: "stdout", level: "error" }];
|
|
344
|
+
try {
|
|
345
|
+
const Constructor = PrismaClient;
|
|
346
|
+
return new Constructor({ adapter: options.adapter, log });
|
|
197
347
|
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
throw normalizeDatabaseError(error, {
|
|
350
|
+
operation: DatabaseOperation.CONNECT,
|
|
351
|
+
fallbackMessage: "Failed to construct the Prisma client.",
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Maps package transaction options onto Prisma's interactive transaction
|
|
357
|
+
* options. Isolation levels are validated against the supported list and
|
|
358
|
+
* passed through as strings.
|
|
359
|
+
*/
|
|
360
|
+
export function buildPrismaTransactionOptions(options = {}) {
|
|
361
|
+
const result = {};
|
|
362
|
+
if (options.maxWaitMs !== undefined) {
|
|
363
|
+
validatePositive(options.maxWaitMs, "maxWaitMs");
|
|
364
|
+
result.maxWait = Math.floor(options.maxWaitMs);
|
|
365
|
+
}
|
|
366
|
+
if (options.timeoutMs !== undefined) {
|
|
367
|
+
validatePositive(options.timeoutMs, "timeoutMs");
|
|
368
|
+
result.timeout = Math.floor(options.timeoutMs);
|
|
369
|
+
}
|
|
370
|
+
if (options.isolationLevel !== undefined) {
|
|
371
|
+
if (!SUPPORTED_ISOLATION_LEVELS.includes(options.isolationLevel)) {
|
|
372
|
+
throw new TypeError(`Unsupported transaction isolation level: ${String(options.isolationLevel)}`);
|
|
373
|
+
}
|
|
374
|
+
result.isolationLevel = options.isolationLevel;
|
|
375
|
+
}
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
function validatePositive(value, name) {
|
|
379
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
380
|
+
throw new TypeError(`Transaction ${name} must be a positive finite number.`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function validateSql(sql) {
|
|
384
|
+
if (typeof sql !== "string" || sql.trim().length === 0) {
|
|
385
|
+
throw new TypeError("A non-empty SQL string is required.");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Throws an abort-typed `DatabaseError` when the signal is already aborted.
|
|
390
|
+
*/
|
|
391
|
+
export function throwIfAborted(signal) {
|
|
392
|
+
if (signal?.aborted)
|
|
393
|
+
throw createAbortError(signal);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Error raised when an operation is cancelled through an `AbortSignal`.
|
|
397
|
+
*/
|
|
398
|
+
export class DatabaseAbortError extends DatabaseError {
|
|
399
|
+
constructor(signal) {
|
|
400
|
+
super("Database operation aborted.", {
|
|
401
|
+
code: "ERR_ABORTED",
|
|
402
|
+
statusCode: 499,
|
|
403
|
+
metadata: { aborted: true },
|
|
404
|
+
cause: signal?.reason,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Creates the error used when an operation is aborted through a signal.
|
|
410
|
+
*/
|
|
411
|
+
export function createAbortError(signal) {
|
|
412
|
+
return new DatabaseAbortError(signal);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Rejects as soon as the signal aborts, even if the operation is still
|
|
416
|
+
* running. The abort listener is removed once the operation settles.
|
|
417
|
+
*/
|
|
418
|
+
export function raceAbort(promise, signal) {
|
|
419
|
+
if (!signal)
|
|
420
|
+
return promise;
|
|
421
|
+
if (signal.aborted) {
|
|
422
|
+
promise.catch(() => undefined);
|
|
423
|
+
return Promise.reject(createAbortError(signal));
|
|
424
|
+
}
|
|
425
|
+
return new Promise((resolve, reject) => {
|
|
426
|
+
const onAbort = () => reject(createAbortError(signal));
|
|
427
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
428
|
+
promise.then(resolve, reject).finally(() => {
|
|
429
|
+
signal.removeEventListener("abort", onAbort);
|
|
430
|
+
});
|
|
431
|
+
});
|
|
198
432
|
}
|
|
199
433
|
/** Creates a database client. */
|
|
200
434
|
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
|