turbine-orm 0.4.0 → 0.7.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 (57) hide show
  1. package/README.md +243 -26
  2. package/dist/cjs/cli/config.js +151 -0
  3. package/dist/cjs/cli/index.js +1176 -0
  4. package/dist/cjs/cli/migrate.js +446 -0
  5. package/dist/cjs/cli/ui.js +233 -0
  6. package/dist/cjs/client.js +512 -0
  7. package/dist/cjs/errors.js +293 -0
  8. package/dist/cjs/generate.js +321 -0
  9. package/dist/cjs/index.js +94 -0
  10. package/dist/cjs/introspect.js +287 -0
  11. package/dist/cjs/package.json +1 -0
  12. package/dist/cjs/pipeline.js +78 -0
  13. package/dist/cjs/query.js +1891 -0
  14. package/dist/cjs/schema-builder.js +238 -0
  15. package/dist/cjs/schema-sql.js +509 -0
  16. package/dist/cjs/schema.js +140 -0
  17. package/dist/cjs/serverless.js +110 -0
  18. package/dist/cli/config.js +6 -16
  19. package/dist/cli/index.js +256 -49
  20. package/dist/cli/migrate.d.ts +35 -6
  21. package/dist/cli/migrate.js +124 -76
  22. package/dist/cli/ui.js +5 -9
  23. package/dist/client.d.ts +87 -3
  24. package/dist/client.js +122 -46
  25. package/dist/errors.d.ts +138 -0
  26. package/dist/errors.js +278 -0
  27. package/dist/generate.js +37 -11
  28. package/dist/index.d.ts +10 -8
  29. package/dist/index.js +15 -11
  30. package/dist/introspect.js +3 -5
  31. package/dist/pipeline.js +8 -1
  32. package/dist/query.d.ts +310 -45
  33. package/dist/query.js +565 -237
  34. package/dist/schema-builder.js +91 -23
  35. package/dist/schema-sql.d.ts +6 -2
  36. package/dist/schema-sql.js +180 -26
  37. package/dist/schema.js +4 -1
  38. package/dist/serverless.d.ts +91 -139
  39. package/dist/serverless.js +86 -173
  40. package/package.json +44 -21
  41. package/dist/cli/config.d.ts.map +0 -1
  42. package/dist/cli/index.d.ts.map +0 -1
  43. package/dist/cli/migrate.d.ts.map +0 -1
  44. package/dist/cli/ui.d.ts.map +0 -1
  45. package/dist/client.d.ts.map +0 -1
  46. package/dist/generate.d.ts.map +0 -1
  47. package/dist/index.d.ts.map +0 -1
  48. package/dist/introspect.d.ts.map +0 -1
  49. package/dist/pipeline.d.ts.map +0 -1
  50. package/dist/query.d.ts.map +0 -1
  51. package/dist/schema-builder.d.ts.map +0 -1
  52. package/dist/schema-sql.d.ts.map +0 -1
  53. package/dist/schema.d.ts.map +0 -1
  54. package/dist/serverless.d.ts.map +0 -1
  55. package/dist/types.d.ts +0 -93
  56. package/dist/types.d.ts.map +0 -1
  57. package/dist/types.js +0 -126
package/dist/client.js CHANGED
@@ -22,21 +22,9 @@
22
22
  * ```
23
23
  */
24
24
  import pg from 'pg';
25
- import { QueryInterface } from './query.js';
25
+ import { TimeoutError, wrapPgError } from './errors.js';
26
26
  import { executePipeline } from './pipeline.js';
27
- // Parse int8 (bigint, OID 20) as JavaScript number instead of string.
28
- // Safe for values up to Number.MAX_SAFE_INTEGER (9,007,199,254,740,991).
29
- pg.types.setTypeParser(20, (val) => {
30
- const n = Number(val);
31
- return Number.isSafeInteger(n) ? n : val; // fall back to string for huge values
32
- });
33
- // Parse numeric (OID 1700) as number when safe, string otherwise
34
- pg.types.setTypeParser(1700, (val) => {
35
- const n = Number(val);
36
- if (val.includes('.') && Number.isFinite(n))
37
- return n;
38
- return Number.isSafeInteger(n) ? n : val;
39
- });
27
+ import { QueryInterface } from './query.js';
40
28
  /** Maps isolation level names to SQL */
41
29
  const ISOLATION_LEVELS = {
42
30
  ReadUncommitted: 'READ UNCOMMITTED',
@@ -56,12 +44,14 @@ export class TransactionClient {
56
44
  client;
57
45
  schema;
58
46
  middlewares;
47
+ queryOptions;
59
48
  tableCache = new Map();
60
49
  savepointCounter = 0;
61
- constructor(client, schema, middlewares) {
50
+ constructor(client, schema, middlewares, queryOptions) {
62
51
  this.client = client;
63
52
  this.schema = schema;
64
53
  this.middlewares = middlewares;
54
+ this.queryOptions = queryOptions;
65
55
  // Auto-create typed table accessors for all tables in the schema
66
56
  for (const tableName of Object.keys(schema.tables)) {
67
57
  const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
@@ -83,7 +73,7 @@ export class TransactionClient {
83
73
  // Create a QueryInterface that uses the transaction client as its "pool"
84
74
  // We use a proxy pool that routes queries through the transaction client
85
75
  const txPool = this.createTxPool();
86
- qi = new QueryInterface(txPool, name, this.schema, this.middlewares);
76
+ qi = new QueryInterface(txPool, name, this.schema, this.middlewares, this.queryOptions);
87
77
  this.tableCache.set(name, qi);
88
78
  }
89
79
  return qi;
@@ -116,20 +106,36 @@ export class TransactionClient {
116
106
  sql += `$${i + 1}`;
117
107
  }
118
108
  });
119
- const result = await this.client.query(sql, values);
120
- return result.rows;
109
+ try {
110
+ const result = await this.client.query(sql, values);
111
+ return result.rows;
112
+ }
113
+ catch (err) {
114
+ throw wrapPgError(err);
115
+ }
121
116
  }
122
117
  /**
123
118
  * Create a pool-like wrapper around the transaction client.
124
119
  * This allows QueryInterface to work with the transaction connection
125
120
  * without knowing it's in a transaction.
121
+ *
122
+ * pg driver errors thrown by queries are translated into typed Turbine
123
+ * errors via wrapPgError so transaction-scoped queries surface the same
124
+ * typed errors as pool-scoped queries.
126
125
  */
127
126
  createTxPool() {
128
127
  const client = this.client;
129
128
  // Return a minimal pool-compatible object that routes queries
130
129
  // through the transaction client
131
130
  return {
132
- query: (text, values) => client.query(text, values),
131
+ query: async (text, values) => {
132
+ try {
133
+ return await client.query(text, values);
134
+ }
135
+ catch (err) {
136
+ throw wrapPgError(err);
137
+ }
138
+ },
133
139
  connect: () => Promise.resolve(client),
134
140
  };
135
141
  }
@@ -142,33 +148,81 @@ export class TurbineClient {
142
148
  pool;
143
149
  /** The schema metadata this client was built from */
144
150
  schema;
151
+ static int8ParserRegistered = false;
145
152
  logging;
146
153
  tableCache = new Map();
147
154
  middlewares = [];
155
+ queryOptions;
156
+ /** True when Turbine created the pool and is responsible for tearing it down */
157
+ ownsPool = true;
148
158
  constructor(config = {}, schema) {
159
+ /**
160
+ * Parse int8 (bigint, OID 20) as JavaScript number instead of string.
161
+ * Safe for values up to Number.MAX_SAFE_INTEGER (9,007,199,254,740,991).
162
+ *
163
+ * NOTE: For values exceeding Number.MAX_SAFE_INTEGER, the parser falls back
164
+ * to returning the raw string to avoid precision loss. The generated TypeScript
165
+ * type maps int8/bigint to `number`, which is correct for the vast majority of
166
+ * use cases (IDs, counts, timestamps). If you store values > 2^53 - 1 in a
167
+ * bigint column, the runtime return type will be `string` for those rows.
168
+ *
169
+ * NOTE: We intentionally do NOT register a parser for numeric (OID 1700).
170
+ * Postgres numeric is arbitrary-precision, so the default pg driver behavior
171
+ * of returning a string is correct and matches the generated TypeScript type
172
+ * (numeric → string). Users who want number can cast explicitly in SQL.
173
+ */
174
+ // Only register the int8 parser when we own the pg driver. External
175
+ // pools (Neon HTTP, Vercel Postgres) may ship their own pg-types fork
176
+ // and rely on their own parser configuration — don't mutate global state
177
+ // we don't own.
178
+ if (!config.pool && !TurbineClient.int8ParserRegistered) {
179
+ pg.types.setTypeParser(20, (val) => {
180
+ const n = Number(val);
181
+ return Number.isSafeInteger(n) ? n : val;
182
+ });
183
+ TurbineClient.int8ParserRegistered = true;
184
+ }
149
185
  this.logging = config.logging ?? false;
150
186
  this.schema = schema;
151
- const poolConfig = {
152
- max: config.poolSize ?? 10,
153
- idleTimeoutMillis: config.idleTimeoutMs ?? 30_000,
154
- connectionTimeoutMillis: config.connectionTimeoutMs ?? 5_000,
187
+ this.queryOptions = {
188
+ defaultLimit: config.defaultLimit,
189
+ warnOnUnlimited: config.warnOnUnlimited,
155
190
  };
156
- if (config.connectionString) {
157
- poolConfig.connectionString = config.connectionString;
191
+ if (config.pool) {
192
+ // External pool — use directly. Turbine doesn't manage its lifecycle.
193
+ this.pool = config.pool;
194
+ this.ownsPool = false;
195
+ if (this.logging) {
196
+ console.log(`[turbine] Using external pool — ${Object.keys(schema.tables).length} tables`);
197
+ }
158
198
  }
159
199
  else {
160
- poolConfig.host = config.host ?? 'localhost';
161
- poolConfig.port = config.port ?? 5432;
162
- poolConfig.database = config.database ?? 'postgres';
163
- poolConfig.user = config.user ?? 'postgres';
164
- poolConfig.password = config.password;
165
- }
166
- this.pool = new pg.Pool(poolConfig);
167
- this.pool.on('error', (err) => {
168
- console.error('[turbine] Unexpected pool error:', err.message);
169
- });
170
- if (this.logging) {
171
- console.log(`[turbine] Pool created — max ${poolConfig.max} connections, ${Object.keys(schema.tables).length} tables`);
200
+ const poolConfig = {
201
+ max: config.poolSize ?? 10,
202
+ idleTimeoutMillis: config.idleTimeoutMs ?? 30_000,
203
+ connectionTimeoutMillis: config.connectionTimeoutMs ?? 5_000,
204
+ };
205
+ if (config.connectionString) {
206
+ poolConfig.connectionString = config.connectionString;
207
+ }
208
+ else {
209
+ poolConfig.host = config.host ?? 'localhost';
210
+ poolConfig.port = config.port ?? 5432;
211
+ poolConfig.database = config.database ?? 'postgres';
212
+ poolConfig.user = config.user ?? 'postgres';
213
+ poolConfig.password = config.password;
214
+ }
215
+ if (config.ssl !== undefined) {
216
+ poolConfig.ssl = config.ssl;
217
+ }
218
+ this.pool = new pg.Pool(poolConfig);
219
+ this.ownsPool = true;
220
+ this.pool.on('error', (err) => {
221
+ console.error('[turbine] Unexpected pool error:', err.message);
222
+ });
223
+ if (this.logging) {
224
+ console.log(`[turbine] Pool created — max ${poolConfig.max} connections, ${Object.keys(schema.tables).length} tables`);
225
+ }
172
226
  }
173
227
  // Auto-create typed table accessors for all tables in the schema
174
228
  for (const tableName of Object.keys(schema.tables)) {
@@ -187,6 +241,11 @@ export class TurbineClient {
187
241
  /**
188
242
  * Register a middleware function that runs before/after every query.
189
243
  *
244
+ * Middleware can inspect and log query parameters, modify results after execution,
245
+ * and measure timing. Note: query SQL is generated before middleware runs, so
246
+ * modifying params.args in middleware will NOT affect the executed SQL.
247
+ * To intercept queries before SQL generation, use the raw() method instead.
248
+ *
190
249
  * @example
191
250
  * ```ts
192
251
  * // Query timing middleware
@@ -225,7 +284,7 @@ export class TurbineClient {
225
284
  table(name) {
226
285
  let qi = this.tableCache.get(name);
227
286
  if (!qi) {
228
- qi = new QueryInterface(this.pool, name, this.schema, this.middlewares);
287
+ qi = new QueryInterface(this.pool, name, this.schema, this.middlewares, this.queryOptions);
229
288
  this.tableCache.set(name, qi);
230
289
  }
231
290
  return qi;
@@ -270,8 +329,13 @@ export class TurbineClient {
270
329
  if (this.logging) {
271
330
  console.log(`[turbine] Raw SQL: ${sql.trim().substring(0, 120)}...`);
272
331
  }
273
- const result = await this.pool.query(sql, values);
274
- return result.rows;
332
+ try {
333
+ const result = await this.pool.query(sql, values);
334
+ return result.rows;
335
+ }
336
+ catch (err) {
337
+ throw wrapPgError(err);
338
+ }
275
339
  }
276
340
  // -------------------------------------------------------------------------
277
341
  // Transaction support (raw — legacy)
@@ -338,7 +402,7 @@ export class TurbineClient {
338
402
  }
339
403
  await client.query(beginSQL);
340
404
  // Create the transaction client with typed table accessors
341
- const tx = new TransactionClient(client, this.schema, this.middlewares);
405
+ const tx = new TransactionClient(client, this.schema, this.middlewares, this.queryOptions);
342
406
  // Dynamically attach table accessors to tx
343
407
  for (const tableName of Object.keys(this.schema.tables)) {
344
408
  const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
@@ -355,7 +419,7 @@ export class TurbineClient {
355
419
  let timer;
356
420
  const timeoutPromise = new Promise((_, reject) => {
357
421
  timer = setTimeout(() => {
358
- reject(new Error(`[turbine] Transaction timed out after ${timeout}ms`));
422
+ reject(new TimeoutError(timeout, 'Transaction'));
359
423
  }, timeout);
360
424
  });
361
425
  try {
@@ -406,8 +470,17 @@ export class TurbineClient {
406
470
  }
407
471
  /**
408
472
  * Gracefully shut down the connection pool.
473
+ *
474
+ * If Turbine was given an external pool via `TurbineConfig.pool`, this
475
+ * method is a no-op — the caller is responsible for the pool's lifecycle.
409
476
  */
410
477
  async disconnect() {
478
+ if (!this.ownsPool) {
479
+ if (this.logging) {
480
+ console.log('[turbine] disconnect() skipped — external pool is not owned by Turbine');
481
+ }
482
+ return;
483
+ }
411
484
  await this.pool.end();
412
485
  if (this.logging) {
413
486
  console.log('[turbine] Pool disconnected');
@@ -417,12 +490,15 @@ export class TurbineClient {
417
490
  async end() {
418
491
  return this.disconnect();
419
492
  }
420
- /** Pool statistics for monitoring. */
493
+ /**
494
+ * Pool statistics for monitoring. Returns zeros for pools that don't
495
+ * expose connection counts (e.g., stateless HTTP drivers like Neon).
496
+ */
421
497
  get stats() {
422
498
  return {
423
- totalCount: this.pool.totalCount,
424
- idleCount: this.pool.idleCount,
425
- waitingCount: this.pool.waitingCount,
499
+ totalCount: this.pool.totalCount ?? 0,
500
+ idleCount: this.pool.idleCount ?? 0,
501
+ waitingCount: this.pool.waitingCount ?? 0,
426
502
  };
427
503
  }
428
504
  }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * turbine-orm — Error types
3
+ *
4
+ * Typed errors with error codes for programmatic handling.
5
+ * All Turbine errors extend TurbineError which includes a `code` property.
6
+ */
7
+ /** Error codes for all Turbine errors */
8
+ export declare const TurbineErrorCode: {
9
+ readonly NOT_FOUND: "TURBINE_E001";
10
+ readonly TIMEOUT: "TURBINE_E002";
11
+ readonly VALIDATION: "TURBINE_E003";
12
+ readonly CONNECTION: "TURBINE_E004";
13
+ readonly RELATION: "TURBINE_E005";
14
+ readonly MIGRATION: "TURBINE_E006";
15
+ readonly CIRCULAR_RELATION: "TURBINE_E007";
16
+ readonly UNIQUE_VIOLATION: "TURBINE_E008";
17
+ readonly FOREIGN_KEY_VIOLATION: "TURBINE_E009";
18
+ readonly NOT_NULL_VIOLATION: "TURBINE_E010";
19
+ readonly CHECK_VIOLATION: "TURBINE_E011";
20
+ };
21
+ export type TurbineErrorCode = (typeof TurbineErrorCode)[keyof typeof TurbineErrorCode];
22
+ /** Base error class for all Turbine errors */
23
+ export declare class TurbineError extends Error {
24
+ readonly code: TurbineErrorCode;
25
+ constructor(code: TurbineErrorCode, message: string, options?: {
26
+ cause?: unknown;
27
+ });
28
+ }
29
+ /**
30
+ * Thrown when a record is not found (findUniqueOrThrow, findFirstOrThrow,
31
+ * update/delete against a non-matching row, etc.)
32
+ *
33
+ * Supports two call styles for back-compat:
34
+ * - `new NotFoundError()` / `new NotFoundError('custom message')`
35
+ * - `new NotFoundError({ table, where, operation, cause, message })`
36
+ *
37
+ * When called with an options object and no explicit `message`, a Prisma-style
38
+ * message is built automatically, e.g.:
39
+ * `[turbine] findUniqueOrThrow on "users" found no record matching where: {"id":1}`
40
+ */
41
+ export declare class NotFoundError extends TurbineError {
42
+ readonly table?: string;
43
+ readonly where?: unknown;
44
+ readonly operation?: string;
45
+ constructor(input?: string | {
46
+ table?: string;
47
+ where?: unknown;
48
+ operation?: string;
49
+ cause?: unknown;
50
+ message?: string;
51
+ });
52
+ }
53
+ /** Thrown when a query or transaction exceeds the configured timeout */
54
+ export declare class TimeoutError extends TurbineError {
55
+ readonly timeoutMs: number;
56
+ constructor(timeoutMs: number, context?: string);
57
+ }
58
+ /** Thrown when query arguments fail validation (unknown column, invalid operator, etc.) */
59
+ export declare class ValidationError extends TurbineError {
60
+ constructor(message: string);
61
+ }
62
+ /** Thrown when a database connection fails */
63
+ export declare class ConnectionError extends TurbineError {
64
+ constructor(message: string);
65
+ }
66
+ /** Thrown when a relation reference is invalid */
67
+ export declare class RelationError extends TurbineError {
68
+ constructor(message: string);
69
+ }
70
+ /** Thrown when a migration operation fails */
71
+ export declare class MigrationError extends TurbineError {
72
+ constructor(message: string);
73
+ }
74
+ /** Thrown when circular relation nesting is detected */
75
+ export declare class CircularRelationError extends TurbineError {
76
+ readonly path: string[];
77
+ constructor(path: string[]);
78
+ }
79
+ /** Thrown when a UNIQUE constraint is violated (pg code 23505) */
80
+ export declare class UniqueConstraintError extends TurbineError {
81
+ readonly constraint?: string;
82
+ readonly columns?: string[];
83
+ readonly table?: string;
84
+ constructor(opts?: {
85
+ constraint?: string;
86
+ columns?: string[];
87
+ table?: string;
88
+ message?: string;
89
+ cause?: unknown;
90
+ });
91
+ }
92
+ /** Thrown when a FOREIGN KEY constraint is violated (pg code 23503) */
93
+ export declare class ForeignKeyError extends TurbineError {
94
+ readonly constraint?: string;
95
+ readonly table?: string;
96
+ constructor(opts?: {
97
+ constraint?: string;
98
+ table?: string;
99
+ message?: string;
100
+ cause?: unknown;
101
+ });
102
+ }
103
+ /** Thrown when a NOT NULL constraint is violated (pg code 23502) */
104
+ export declare class NotNullViolationError extends TurbineError {
105
+ readonly column?: string;
106
+ readonly table?: string;
107
+ constructor(opts?: {
108
+ column?: string;
109
+ table?: string;
110
+ message?: string;
111
+ cause?: unknown;
112
+ });
113
+ }
114
+ /** Thrown when a CHECK constraint is violated (pg code 23514) */
115
+ export declare class CheckConstraintError extends TurbineError {
116
+ readonly constraint?: string;
117
+ readonly table?: string;
118
+ constructor(opts?: {
119
+ constraint?: string;
120
+ table?: string;
121
+ message?: string;
122
+ cause?: unknown;
123
+ });
124
+ }
125
+ /**
126
+ * Translate a pg driver error into a typed Turbine error.
127
+ * If the error doesn't match a known constraint code, returns it unchanged.
128
+ *
129
+ * Maps:
130
+ * 23505 (unique_violation) -> UniqueConstraintError
131
+ * 23503 (foreign_key_violation) -> ForeignKeyError
132
+ * 23502 (not_null_violation) -> NotNullViolationError
133
+ * 23514 (check_violation) -> CheckConstraintError
134
+ *
135
+ * The original pg error is preserved as `.cause` on the wrapped error.
136
+ */
137
+ export declare function wrapPgError(err: unknown): unknown;
138
+ //# sourceMappingURL=errors.d.ts.map
package/dist/errors.js ADDED
@@ -0,0 +1,278 @@
1
+ /**
2
+ * turbine-orm — Error types
3
+ *
4
+ * Typed errors with error codes for programmatic handling.
5
+ * All Turbine errors extend TurbineError which includes a `code` property.
6
+ */
7
+ /** Error codes for all Turbine errors */
8
+ export const TurbineErrorCode = {
9
+ NOT_FOUND: 'TURBINE_E001',
10
+ TIMEOUT: 'TURBINE_E002',
11
+ VALIDATION: 'TURBINE_E003',
12
+ CONNECTION: 'TURBINE_E004',
13
+ RELATION: 'TURBINE_E005',
14
+ MIGRATION: 'TURBINE_E006',
15
+ CIRCULAR_RELATION: 'TURBINE_E007',
16
+ UNIQUE_VIOLATION: 'TURBINE_E008',
17
+ FOREIGN_KEY_VIOLATION: 'TURBINE_E009',
18
+ NOT_NULL_VIOLATION: 'TURBINE_E010',
19
+ CHECK_VIOLATION: 'TURBINE_E011',
20
+ };
21
+ /** Base error class for all Turbine errors */
22
+ export class TurbineError extends Error {
23
+ code;
24
+ constructor(code, message, options) {
25
+ super(message, options);
26
+ this.name = 'TurbineError';
27
+ this.code = code;
28
+ }
29
+ }
30
+ /**
31
+ * Thrown when a record is not found (findUniqueOrThrow, findFirstOrThrow,
32
+ * update/delete against a non-matching row, etc.)
33
+ *
34
+ * Supports two call styles for back-compat:
35
+ * - `new NotFoundError()` / `new NotFoundError('custom message')`
36
+ * - `new NotFoundError({ table, where, operation, cause, message })`
37
+ *
38
+ * When called with an options object and no explicit `message`, a Prisma-style
39
+ * message is built automatically, e.g.:
40
+ * `[turbine] findUniqueOrThrow on "users" found no record matching where: {"id":1}`
41
+ */
42
+ export class NotFoundError extends TurbineError {
43
+ table;
44
+ where;
45
+ operation;
46
+ constructor(input) {
47
+ // Back-compat: string argument (or undefined) — replicate legacy behavior.
48
+ if (typeof input === 'string' || input === undefined) {
49
+ super(TurbineErrorCode.NOT_FOUND, input ?? 'Record not found');
50
+ this.name = 'NotFoundError';
51
+ return;
52
+ }
53
+ const { table, where, operation, cause } = input;
54
+ let message = input.message;
55
+ if (!message) {
56
+ if (operation && table) {
57
+ const wherePart = where !== undefined ? ` matching where: ${JSON.stringify(where)}` : '';
58
+ message = `[turbine] ${operation} on "${table}" found no record${wherePart}`;
59
+ }
60
+ else if (table) {
61
+ const wherePart = where !== undefined ? ` matching where ${JSON.stringify(where)}` : '';
62
+ message = `[turbine] No record found in "${table}"${wherePart}`;
63
+ }
64
+ else {
65
+ message = '[turbine] Record not found';
66
+ }
67
+ }
68
+ super(TurbineErrorCode.NOT_FOUND, message, { cause });
69
+ this.name = 'NotFoundError';
70
+ this.table = table;
71
+ this.where = where;
72
+ this.operation = operation;
73
+ }
74
+ }
75
+ /** Thrown when a query or transaction exceeds the configured timeout */
76
+ export class TimeoutError extends TurbineError {
77
+ timeoutMs;
78
+ constructor(timeoutMs, context = 'Query') {
79
+ super(TurbineErrorCode.TIMEOUT, `[turbine] ${context} timed out after ${timeoutMs}ms`);
80
+ this.name = 'TimeoutError';
81
+ this.timeoutMs = timeoutMs;
82
+ }
83
+ }
84
+ /** Thrown when query arguments fail validation (unknown column, invalid operator, etc.) */
85
+ export class ValidationError extends TurbineError {
86
+ constructor(message) {
87
+ super(TurbineErrorCode.VALIDATION, message);
88
+ this.name = 'ValidationError';
89
+ }
90
+ }
91
+ /** Thrown when a database connection fails */
92
+ export class ConnectionError extends TurbineError {
93
+ constructor(message) {
94
+ super(TurbineErrorCode.CONNECTION, message);
95
+ this.name = 'ConnectionError';
96
+ }
97
+ }
98
+ /** Thrown when a relation reference is invalid */
99
+ export class RelationError extends TurbineError {
100
+ constructor(message) {
101
+ super(TurbineErrorCode.RELATION, message);
102
+ this.name = 'RelationError';
103
+ }
104
+ }
105
+ /** Thrown when a migration operation fails */
106
+ export class MigrationError extends TurbineError {
107
+ constructor(message) {
108
+ super(TurbineErrorCode.MIGRATION, message);
109
+ this.name = 'MigrationError';
110
+ }
111
+ }
112
+ /** Thrown when circular relation nesting is detected */
113
+ export class CircularRelationError extends TurbineError {
114
+ path;
115
+ constructor(path) {
116
+ super(TurbineErrorCode.CIRCULAR_RELATION, `[turbine] Circular or too-deep relation nesting detected: ${path.join(' → ')}. Maximum nesting depth is 10.`);
117
+ this.name = 'CircularRelationError';
118
+ this.path = path;
119
+ }
120
+ }
121
+ // ---------------------------------------------------------------------------
122
+ // Database constraint violation errors
123
+ // ---------------------------------------------------------------------------
124
+ /**
125
+ * Extract the `detail` string from a pg-style error stored as `cause`.
126
+ * Returns undefined if the cause is not an object or has no detail.
127
+ */
128
+ function detailFromCause(cause) {
129
+ if (!cause || typeof cause !== 'object')
130
+ return undefined;
131
+ const d = cause.detail;
132
+ return typeof d === 'string' && d.length > 0 ? d : undefined;
133
+ }
134
+ /** Thrown when a UNIQUE constraint is violated (pg code 23505) */
135
+ export class UniqueConstraintError extends TurbineError {
136
+ constraint;
137
+ columns;
138
+ table;
139
+ constructor(opts = {}) {
140
+ const { constraint, columns, table, cause } = opts;
141
+ let message = opts.message;
142
+ if (!message) {
143
+ const constraintPart = constraint ? ` on ${constraint}` : '';
144
+ const columnsPart = columns && columns.length > 0 ? ` (${columns.join(', ')})` : '';
145
+ message = `[turbine] Unique constraint violation${constraintPart}${columnsPart}`;
146
+ const detail = detailFromCause(cause);
147
+ if (detail)
148
+ message += `: ${detail}`;
149
+ }
150
+ super(TurbineErrorCode.UNIQUE_VIOLATION, message, { cause });
151
+ this.name = 'UniqueConstraintError';
152
+ this.constraint = constraint;
153
+ this.columns = columns;
154
+ this.table = table;
155
+ }
156
+ }
157
+ /** Thrown when a FOREIGN KEY constraint is violated (pg code 23503) */
158
+ export class ForeignKeyError extends TurbineError {
159
+ constraint;
160
+ table;
161
+ constructor(opts = {}) {
162
+ const { constraint, table, cause } = opts;
163
+ let message = opts.message;
164
+ if (!message) {
165
+ const constraintPart = constraint ? ` on ${constraint}` : '';
166
+ message = `[turbine] Foreign key constraint violation${constraintPart}`;
167
+ const detail = detailFromCause(cause);
168
+ if (detail)
169
+ message += `: ${detail}`;
170
+ }
171
+ super(TurbineErrorCode.FOREIGN_KEY_VIOLATION, message, { cause });
172
+ this.name = 'ForeignKeyError';
173
+ this.constraint = constraint;
174
+ this.table = table;
175
+ }
176
+ }
177
+ /** Thrown when a NOT NULL constraint is violated (pg code 23502) */
178
+ export class NotNullViolationError extends TurbineError {
179
+ column;
180
+ table;
181
+ constructor(opts = {}) {
182
+ const { column, table, cause } = opts;
183
+ let message = opts.message;
184
+ if (!message) {
185
+ const columnPart = column ? ` on column "${column}"` : '';
186
+ message = `[turbine] NOT NULL constraint violation${columnPart}`;
187
+ const detail = detailFromCause(cause);
188
+ if (detail)
189
+ message += `: ${detail}`;
190
+ }
191
+ super(TurbineErrorCode.NOT_NULL_VIOLATION, message, { cause });
192
+ this.name = 'NotNullViolationError';
193
+ this.column = column;
194
+ this.table = table;
195
+ }
196
+ }
197
+ /** Thrown when a CHECK constraint is violated (pg code 23514) */
198
+ export class CheckConstraintError extends TurbineError {
199
+ constraint;
200
+ table;
201
+ constructor(opts = {}) {
202
+ const { constraint, table, cause } = opts;
203
+ let message = opts.message;
204
+ if (!message) {
205
+ const constraintPart = constraint ? ` on ${constraint}` : '';
206
+ message = `[turbine] Check constraint violation${constraintPart}`;
207
+ const detail = detailFromCause(cause);
208
+ if (detail)
209
+ message += `: ${detail}`;
210
+ }
211
+ super(TurbineErrorCode.CHECK_VIOLATION, message, { cause });
212
+ this.name = 'CheckConstraintError';
213
+ this.constraint = constraint;
214
+ this.table = table;
215
+ }
216
+ }
217
+ /**
218
+ * Parse column names out of a pg `detail` string like:
219
+ * "Key (email)=(foo@bar) already exists."
220
+ * "Key (col1, col2)=(v1, v2) already exists."
221
+ */
222
+ function parseColumnsFromDetail(detail) {
223
+ const m = detail.match(/^Key \(([^)]+)\)/);
224
+ if (!m)
225
+ return undefined;
226
+ return m[1].split(',').map((s) => s.trim());
227
+ }
228
+ /**
229
+ * Translate a pg driver error into a typed Turbine error.
230
+ * If the error doesn't match a known constraint code, returns it unchanged.
231
+ *
232
+ * Maps:
233
+ * 23505 (unique_violation) -> UniqueConstraintError
234
+ * 23503 (foreign_key_violation) -> ForeignKeyError
235
+ * 23502 (not_null_violation) -> NotNullViolationError
236
+ * 23514 (check_violation) -> CheckConstraintError
237
+ *
238
+ * The original pg error is preserved as `.cause` on the wrapped error.
239
+ */
240
+ export function wrapPgError(err) {
241
+ if (!err || typeof err !== 'object')
242
+ return err;
243
+ const e = err;
244
+ if (!e.code)
245
+ return err;
246
+ switch (e.code) {
247
+ case '23505': {
248
+ const cols = e.detail ? parseColumnsFromDetail(e.detail) : undefined;
249
+ return new UniqueConstraintError({
250
+ constraint: e.constraint,
251
+ columns: cols,
252
+ table: e.table,
253
+ cause: err,
254
+ });
255
+ }
256
+ case '23503':
257
+ return new ForeignKeyError({
258
+ constraint: e.constraint,
259
+ table: e.table,
260
+ cause: err,
261
+ });
262
+ case '23502':
263
+ return new NotNullViolationError({
264
+ column: e.column,
265
+ table: e.table,
266
+ cause: err,
267
+ });
268
+ case '23514':
269
+ return new CheckConstraintError({
270
+ constraint: e.constraint,
271
+ table: e.table,
272
+ cause: err,
273
+ });
274
+ default:
275
+ return err;
276
+ }
277
+ }
278
+ //# sourceMappingURL=errors.js.map