@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.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -17
  3. package/dist/cache/cache.memory.d.ts +167 -0
  4. package/dist/cache/cache.memory.js +186 -40
  5. package/dist/cache/index.d.ts +2 -2
  6. package/dist/cache/index.js +2 -2
  7. package/dist/database/database.core.d.ts +91 -0
  8. package/dist/database/database.core.js +21 -4
  9. package/dist/databaseClient/databaseClient.core.d.ts +171 -14
  10. package/dist/databaseClient/databaseClient.core.js +350 -70
  11. package/dist/databaseClient/databaseClient.errors.d.ts +112 -0
  12. package/dist/databaseClient/databaseClient.errors.js +425 -0
  13. package/dist/databaseClient/index.d.ts +2 -1
  14. package/dist/databaseClient/index.js +2 -1
  15. package/dist/databaseConnection/databaseConnection.manager.d.ts +163 -0
  16. package/dist/databaseConnection/databaseConnection.manager.js +138 -76
  17. package/dist/databaseConnection/index.d.ts +1 -1
  18. package/dist/databaseType/databaseType.type.d.ts +224 -0
  19. package/dist/databaseType/databaseType.type.js +0 -35
  20. package/dist/health/health.check.d.ts +74 -0
  21. package/dist/health/health.check.js +78 -47
  22. package/dist/health/index.d.ts +1 -1
  23. package/dist/health/index.js +1 -1
  24. package/dist/index.d.ts +14 -14
  25. package/dist/index.js +11 -11
  26. package/dist/locks/index.d.ts +2 -2
  27. package/dist/locks/index.js +2 -2
  28. package/dist/locks/locks.core.d.ts +132 -0
  29. package/dist/locks/locks.core.js +150 -85
  30. package/dist/migration/index.d.ts +3 -1
  31. package/dist/migration/index.js +2 -0
  32. package/dist/migration/migration.dialect.d.ts +65 -0
  33. package/dist/migration/migration.dialect.js +63 -0
  34. package/dist/migration/migration.helpers.d.ts +65 -0
  35. package/dist/migration/migration.helpers.js +60 -18
  36. package/dist/migration/migration.runner.d.ts +75 -0
  37. package/dist/migration/migration.runner.js +195 -107
  38. package/dist/migration/migration.types.d.ts +85 -0
  39. package/dist/pagination/index.d.ts +2 -1
  40. package/dist/pagination/index.js +2 -1
  41. package/dist/pagination/pagination.core.d.ts +163 -0
  42. package/dist/pagination/pagination.core.js +78 -9
  43. package/dist/pagination/pagination.keyset.d.ts +55 -0
  44. package/dist/pagination/pagination.keyset.js +108 -0
  45. package/dist/queryBuilder/index.d.ts +4 -3
  46. package/dist/queryBuilder/index.js +3 -2
  47. package/dist/queryBuilder/queryBuilder.core.d.ts +178 -0
  48. package/dist/queryBuilder/queryBuilder.core.js +92 -14
  49. package/dist/queryBuilder/queryBuilder.factory.d.ts +6 -0
  50. package/dist/queryBuilder/queryBuilder.factory.js +0 -25
  51. package/dist/queryBuilder/queryBuilder.filter.d.ts +170 -0
  52. package/dist/queryBuilder/queryBuilder.filter.js +191 -30
  53. package/dist/queryBuilder/queryBuilder.prisma.d.ts +60 -0
  54. package/dist/queryBuilder/queryBuilder.prisma.js +233 -0
  55. package/dist/queryBuilder/queryBuilder.type.d.ts +48 -0
  56. package/dist/relations/index.d.ts +1 -1
  57. package/dist/relations/index.js +1 -1
  58. package/dist/relations/relations.definition.d.ts +184 -0
  59. package/dist/relations/relations.definition.js +186 -19
  60. package/dist/repository/index.d.ts +2 -1
  61. package/dist/repository/index.js +1 -0
  62. package/dist/repository/repository.base.d.ts +283 -0
  63. package/dist/repository/repository.base.js +370 -89
  64. package/dist/repository/repository.errors.d.ts +60 -0
  65. package/dist/repository/repository.errors.js +235 -0
  66. package/dist/seed/index.d.ts +2 -2
  67. package/dist/seed/index.js +1 -1
  68. package/dist/seed/seed.runner.d.ts +160 -0
  69. package/dist/seed/seed.runner.js +194 -225
  70. package/dist/transaction/index.d.ts +1 -1
  71. package/dist/transaction/index.js +1 -1
  72. package/dist/transaction/transaction.core.d.ts +117 -0
  73. package/dist/transaction/transaction.core.js +83 -56
  74. package/dist/unitOfWork/unitOfWork.core.d.ts +44 -0
  75. package/dist/unitOfWork/unitOfWork.core.js +9 -20
  76. package/package.json +44 -24
  77. package/dist/cache/cache.memory.d.ts.map +0 -1
  78. package/dist/cache/cache.memory.js.map +0 -1
  79. package/dist/cache/index.d.ts.map +0 -1
  80. package/dist/cache/index.js.map +0 -1
  81. package/dist/database/database.core.d.ts.map +0 -1
  82. package/dist/database/database.core.js.map +0 -1
  83. package/dist/database/index.d.ts.map +0 -1
  84. package/dist/database/index.js.map +0 -1
  85. package/dist/databaseClient/databaseClient.core.d.ts.map +0 -1
  86. package/dist/databaseClient/databaseClient.core.js.map +0 -1
  87. package/dist/databaseClient/databaseClient.logger.d.ts.map +0 -1
  88. package/dist/databaseClient/databaseClient.logger.js.map +0 -1
  89. package/dist/databaseClient/index.d.ts.map +0 -1
  90. package/dist/databaseClient/index.js.map +0 -1
  91. package/dist/databaseConnection/databaseConnection.manager.d.ts.map +0 -1
  92. package/dist/databaseConnection/databaseConnection.manager.js.map +0 -1
  93. package/dist/databaseConnection/index.d.ts.map +0 -1
  94. package/dist/databaseConnection/index.js.map +0 -1
  95. package/dist/databaseType/databaseType.type.d.ts.map +0 -1
  96. package/dist/databaseType/databaseType.type.js.map +0 -1
  97. package/dist/databaseType/index.d.ts.map +0 -1
  98. package/dist/databaseType/index.js.map +0 -1
  99. package/dist/health/health.check.d.ts.map +0 -1
  100. package/dist/health/health.check.js.map +0 -1
  101. package/dist/health/index.d.ts.map +0 -1
  102. package/dist/health/index.js.map +0 -1
  103. package/dist/index.d.ts.map +0 -1
  104. package/dist/index.js.map +0 -1
  105. package/dist/locks/index.d.ts.map +0 -1
  106. package/dist/locks/index.js.map +0 -1
  107. package/dist/locks/locks.core.d.ts.map +0 -1
  108. package/dist/locks/locks.core.js.map +0 -1
  109. package/dist/migration/index.d.ts.map +0 -1
  110. package/dist/migration/index.js.map +0 -1
  111. package/dist/migration/migration.helpers.d.ts.map +0 -1
  112. package/dist/migration/migration.helpers.js.map +0 -1
  113. package/dist/migration/migration.runner.d.ts.map +0 -1
  114. package/dist/migration/migration.runner.js.map +0 -1
  115. package/dist/migration/migration.types.d.ts.map +0 -1
  116. package/dist/migration/migration.types.js.map +0 -1
  117. package/dist/pagination/index.d.ts.map +0 -1
  118. package/dist/pagination/index.js.map +0 -1
  119. package/dist/pagination/pagination.core.d.ts.map +0 -1
  120. package/dist/pagination/pagination.core.js.map +0 -1
  121. package/dist/queryBuilder/index.d.ts.map +0 -1
  122. package/dist/queryBuilder/index.js.map +0 -1
  123. package/dist/queryBuilder/queryBuilder.core.d.ts.map +0 -1
  124. package/dist/queryBuilder/queryBuilder.core.js.map +0 -1
  125. package/dist/queryBuilder/queryBuilder.factory.d.ts.map +0 -1
  126. package/dist/queryBuilder/queryBuilder.factory.js.map +0 -1
  127. package/dist/queryBuilder/queryBuilder.filter.d.ts.map +0 -1
  128. package/dist/queryBuilder/queryBuilder.filter.js.map +0 -1
  129. package/dist/queryBuilder/queryBuilder.type.d.ts.map +0 -1
  130. package/dist/queryBuilder/queryBuilder.type.js.map +0 -1
  131. package/dist/relations/index.d.ts.map +0 -1
  132. package/dist/relations/index.js.map +0 -1
  133. package/dist/relations/relations.definition.d.ts.map +0 -1
  134. package/dist/relations/relations.definition.js.map +0 -1
  135. package/dist/repository/index.d.ts.map +0 -1
  136. package/dist/repository/index.js.map +0 -1
  137. package/dist/repository/repository.base.d.ts.map +0 -1
  138. package/dist/repository/repository.base.js.map +0 -1
  139. package/dist/seed/index.d.ts.map +0 -1
  140. package/dist/seed/index.js.map +0 -1
  141. package/dist/seed/seed.runner.d.ts.map +0 -1
  142. package/dist/seed/seed.runner.js.map +0 -1
  143. package/dist/transaction/index.d.ts.map +0 -1
  144. package/dist/transaction/index.js.map +0 -1
  145. package/dist/transaction/transaction.core.d.ts.map +0 -1
  146. package/dist/transaction/transaction.core.js.map +0 -1
  147. package/dist/unitOfWork/index.d.ts.map +0 -1
  148. package/dist/unitOfWork/index.js.map +0 -1
  149. package/dist/unitOfWork/unitOfWork.core.d.ts.map +0 -1
  150. 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, Prisma } from "@prisma/client";
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
- /** Prisma-backed database client. */
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
- async connect() {
36
- if (this.status === "connected" || this.status === "connecting")
37
- return;
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 = this.normalizeError(error, "Database connection failed.");
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
- async disconnect() {
53
- if (this.status === "disconnected")
54
- return;
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 = this.normalizeError(error, "Database disconnection failed.");
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
- async ping() {
129
+ /**
130
+ * Executes a lightweight `SELECT 1`.
131
+ */
132
+ async ping(options = {}) {
70
133
  try {
71
- await this.prisma.$queryRaw `SELECT 1`;
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 this.normalizeError(error, "Database ping failed.");
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
- async healthCheck() {
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: "connected",
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 = this.normalizeError(error, "Database health check failed.");
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 = this.buildTransactionOptions(options);
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 = this.normalizeError(error, "Database transaction failed.");
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
- async executeRaw(query) {
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.$executeRaw(query);
212
+ return await this.runRaw(() => this.prisma.$executeRawUnsafe(sql, ...values), options, "Raw database execution timed out.");
122
213
  }
123
214
  catch (error) {
124
- throw this.normalizeError(error, "Raw database execution failed.");
215
+ throw normalizeDatabaseError(error, {
216
+ operation: DatabaseOperation.QUERY,
217
+ fallbackMessage: "Raw database execution failed.",
218
+ metadata: options.metadata,
219
+ });
125
220
  }
126
221
  }
127
- async queryRaw(query) {
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 (await this.prisma.$queryRaw(query));
229
+ return await this.runRaw(() => this.prisma.$queryRawUnsafe(sql, ...values), options, "Raw database query timed out.");
131
230
  }
132
231
  catch (error) {
133
- throw this.normalizeError(error, "Raw database query failed.");
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
- const prisma = this.prisma;
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
- buildTransactionOptions(options) {
155
- return {
156
- maxWait: options.maxWaitMs,
157
- timeout: options.timeoutMs,
158
- isolationLevel: options.isolationLevel
159
- ? this.mapIsolationLevel(options.isolationLevel)
160
- : undefined,
161
- };
162
- }
163
- mapIsolationLevel(level) {
164
- switch (level) {
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
- clearTimeout(timer);
325
+ if (timer)
326
+ clearTimeout(timer);
191
327
  }
192
328
  }
193
- normalizeError(error, fallbackMessage) {
194
- if (error instanceof DatabaseError)
195
- return error;
196
- return new DatabaseError(error instanceof Error ? error.message : fallbackMessage, { cause: error });
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