@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.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -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 +165 -14
  10. package/dist/databaseClient/databaseClient.core.js +304 -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 +128 -0
  29. package/dist/locks/locks.core.js +144 -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 +270 -0
  63. package/dist/repository/repository.base.js +350 -84
  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 +40 -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 { PrismaClient } from "@prisma/client";
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,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 = this.buildTransactionOptions(options);
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 = this.normalizeError(error, "Database transaction failed.");
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
- async executeRaw(query) {
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.$executeRaw(query);
223
+ return await this.runRaw(() => this.prisma.$queryRawUnsafe(sql, ...values), options, "Raw database query timed out.");
122
224
  }
123
225
  catch (error) {
124
- throw this.normalizeError(error, "Raw database execution failed.");
226
+ throw normalizeDatabaseError(error, {
227
+ operation: DatabaseOperation.QUERY,
228
+ fallbackMessage: "Raw database query failed.",
229
+ metadata: options.metadata,
230
+ });
125
231
  }
126
232
  }
127
- async queryRaw(query) {
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 (await this.prisma.$queryRaw(query));
245
+ return await this.runRaw(() => prisma.$executeRaw(query), options, "Raw database execution timed out.");
131
246
  }
132
247
  catch (error) {
133
- throw this.normalizeError(error, "Raw database query failed.");
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
- const prisma = this.prisma;
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
- 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
- }
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
- clearTimeout(timer);
319
+ if (timer)
320
+ clearTimeout(timer);
191
321
  }
192
322
  }
193
- normalizeError(error, fallbackMessage) {
194
- if (error instanceof DatabaseError)
195
- return error;
196
- return new DatabaseError(error instanceof Error ? error.message : fallbackMessage, { cause: error });
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