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.
- package/README.md +243 -26
- package/dist/cjs/cli/config.js +151 -0
- package/dist/cjs/cli/index.js +1176 -0
- package/dist/cjs/cli/migrate.js +446 -0
- package/dist/cjs/cli/ui.js +233 -0
- package/dist/cjs/client.js +512 -0
- package/dist/cjs/errors.js +293 -0
- package/dist/cjs/generate.js +321 -0
- package/dist/cjs/index.js +94 -0
- package/dist/cjs/introspect.js +287 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pipeline.js +78 -0
- package/dist/cjs/query.js +1891 -0
- package/dist/cjs/schema-builder.js +238 -0
- package/dist/cjs/schema-sql.js +509 -0
- package/dist/cjs/schema.js +140 -0
- package/dist/cjs/serverless.js +110 -0
- package/dist/cli/config.js +6 -16
- package/dist/cli/index.js +256 -49
- package/dist/cli/migrate.d.ts +35 -6
- package/dist/cli/migrate.js +124 -76
- package/dist/cli/ui.js +5 -9
- package/dist/client.d.ts +87 -3
- package/dist/client.js +122 -46
- package/dist/errors.d.ts +138 -0
- package/dist/errors.js +278 -0
- package/dist/generate.js +37 -11
- package/dist/index.d.ts +10 -8
- package/dist/index.js +15 -11
- package/dist/introspect.js +3 -5
- package/dist/pipeline.js +8 -1
- package/dist/query.d.ts +310 -45
- package/dist/query.js +565 -237
- package/dist/schema-builder.js +91 -23
- package/dist/schema-sql.d.ts +6 -2
- package/dist/schema-sql.js +180 -26
- package/dist/schema.js +4 -1
- package/dist/serverless.d.ts +91 -139
- package/dist/serverless.js +86 -173
- package/package.json +44 -21
- package/dist/cli/config.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/migrate.d.ts.map +0 -1
- package/dist/cli/ui.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/generate.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/introspect.d.ts.map +0 -1
- package/dist/pipeline.d.ts.map +0 -1
- package/dist/query.d.ts.map +0 -1
- package/dist/schema-builder.d.ts.map +0 -1
- package/dist/schema-sql.d.ts.map +0 -1
- package/dist/schema.d.ts.map +0 -1
- package/dist/serverless.d.ts.map +0 -1
- package/dist/types.d.ts +0 -93
- package/dist/types.d.ts.map +0 -1
- 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 {
|
|
25
|
+
import { TimeoutError, wrapPgError } from './errors.js';
|
|
26
26
|
import { executePipeline } from './pipeline.js';
|
|
27
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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) =>
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
connectionTimeoutMillis: config.connectionTimeoutMs ?? 5_000,
|
|
187
|
+
this.queryOptions = {
|
|
188
|
+
defaultLimit: config.defaultLimit,
|
|
189
|
+
warnOnUnlimited: config.warnOnUnlimited,
|
|
155
190
|
};
|
|
156
|
-
if (config.
|
|
157
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
}
|
package/dist/errors.d.ts
ADDED
|
@@ -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
|