turbine-orm 0.14.0 → 0.15.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/dist/adapters/cockroachdb.js +1 -1
- package/dist/adapters/index.d.ts +7 -4
- package/dist/adapters/index.js +1 -1
- package/dist/adapters/yugabytedb.js +1 -1
- package/dist/cjs/adapters/cockroachdb.js +1 -1
- package/dist/cjs/adapters/index.js +1 -1
- package/dist/cjs/adapters/yugabytedb.js +1 -1
- package/dist/cjs/cli/studio.js +45 -7
- package/dist/cjs/client.js +48 -1
- package/dist/cjs/errors.js +44 -1
- package/dist/cjs/generate.js +86 -0
- package/dist/cjs/index.js +10 -1
- package/dist/cjs/nested-write.js +467 -0
- package/dist/cjs/query/builder.js +205 -10
- package/dist/cli/studio.d.ts +10 -2
- package/dist/cli/studio.js +45 -7
- package/dist/client.d.ts +23 -0
- package/dist/client.js +47 -1
- package/dist/errors.d.ts +23 -0
- package/dist/errors.js +41 -0
- package/dist/generate.js +86 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -2
- package/dist/nested-write.d.ts +95 -0
- package/dist/nested-write.js +461 -0
- package/dist/query/builder.d.ts +28 -12
- package/dist/query/builder.js +173 -11
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +76 -8
- package/package.json +2 -2
|
@@ -166,7 +166,7 @@ export const cockroachdb = {
|
|
|
166
166
|
},
|
|
167
167
|
statementTimeout(seconds) {
|
|
168
168
|
// CockroachDB v23.1+ supports transaction_timeout
|
|
169
|
-
return `SET transaction_timeout =
|
|
169
|
+
return { sql: `SET transaction_timeout = $1`, params: [`${seconds}s`] };
|
|
170
170
|
},
|
|
171
171
|
};
|
|
172
172
|
//# sourceMappingURL=cockroachdb.js.map
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -55,13 +55,16 @@ export interface DatabaseAdapter {
|
|
|
55
55
|
introspectionOverrides?: Partial<IntrospectionOverrides>;
|
|
56
56
|
/**
|
|
57
57
|
* Generate the SQL to set a statement timeout within a transaction.
|
|
58
|
-
* PostgreSQL uses `SET LOCAL statement_timeout =
|
|
59
|
-
* CockroachDB uses `SET transaction_timeout =
|
|
58
|
+
* PostgreSQL uses `SET LOCAL statement_timeout = $1`.
|
|
59
|
+
* CockroachDB uses `SET transaction_timeout = $1` (v23.1+).
|
|
60
60
|
*
|
|
61
61
|
* @param seconds — timeout in seconds
|
|
62
|
-
* @returns the SQL
|
|
62
|
+
* @returns an object with the parameterized SQL and its bound values
|
|
63
63
|
*/
|
|
64
|
-
statementTimeout?(seconds: number):
|
|
64
|
+
statementTimeout?(seconds: number): {
|
|
65
|
+
sql: string;
|
|
66
|
+
params: unknown[];
|
|
67
|
+
};
|
|
65
68
|
/**
|
|
66
69
|
* SQL to create the lock table used by table-based locking adapters.
|
|
67
70
|
* Called during `ensureTrackingTable` when the adapter uses table locks.
|
package/dist/adapters/index.js
CHANGED
|
@@ -31,7 +31,7 @@ export const postgresql = {
|
|
|
31
31
|
await client.query(`SELECT pg_advisory_unlock($1)`, [lockId]);
|
|
32
32
|
},
|
|
33
33
|
statementTimeout(seconds) {
|
|
34
|
-
return `SET LOCAL statement_timeout =
|
|
34
|
+
return { sql: `SET LOCAL statement_timeout = $1`, params: [`${seconds}s`] };
|
|
35
35
|
},
|
|
36
36
|
};
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
@@ -150,7 +150,7 @@ export const yugabytedb = {
|
|
|
150
150
|
},
|
|
151
151
|
statementTimeout(seconds) {
|
|
152
152
|
// YugabyteDB supports standard PostgreSQL statement_timeout
|
|
153
|
-
return `SET LOCAL statement_timeout =
|
|
153
|
+
return { sql: `SET LOCAL statement_timeout = $1`, params: [`${seconds}s`] };
|
|
154
154
|
},
|
|
155
155
|
};
|
|
156
156
|
//# sourceMappingURL=yugabytedb.js.map
|
|
@@ -169,6 +169,6 @@ exports.cockroachdb = {
|
|
|
169
169
|
},
|
|
170
170
|
statementTimeout(seconds) {
|
|
171
171
|
// CockroachDB v23.1+ supports transaction_timeout
|
|
172
|
-
return `SET transaction_timeout =
|
|
172
|
+
return { sql: `SET transaction_timeout = $1`, params: [`${seconds}s`] };
|
|
173
173
|
},
|
|
174
174
|
};
|
|
@@ -34,7 +34,7 @@ exports.postgresql = {
|
|
|
34
34
|
await client.query(`SELECT pg_advisory_unlock($1)`, [lockId]);
|
|
35
35
|
},
|
|
36
36
|
statementTimeout(seconds) {
|
|
37
|
-
return `SET LOCAL statement_timeout =
|
|
37
|
+
return { sql: `SET LOCAL statement_timeout = $1`, params: [`${seconds}s`] };
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
40
|
// ---------------------------------------------------------------------------
|
|
@@ -153,6 +153,6 @@ exports.yugabytedb = {
|
|
|
153
153
|
},
|
|
154
154
|
statementTimeout(seconds) {
|
|
155
155
|
// YugabyteDB supports standard PostgreSQL statement_timeout
|
|
156
|
-
return `SET LOCAL statement_timeout =
|
|
156
|
+
return { sql: `SET LOCAL statement_timeout = $1`, params: [`${seconds}s`] };
|
|
157
157
|
},
|
|
158
158
|
};
|
package/dist/cjs/cli/studio.js
CHANGED
|
@@ -74,8 +74,12 @@ async function startStudio(options) {
|
|
|
74
74
|
});
|
|
75
75
|
const authToken = (0, node_crypto_1.randomBytes)(24).toString('hex');
|
|
76
76
|
const stateDir = (0, node_path_1.resolve)(options.stateDir ?? '.turbine');
|
|
77
|
-
const
|
|
78
|
-
|
|
77
|
+
const statementTimeout = options.adapter?.statementTimeout?.(30) ?? {
|
|
78
|
+
sql: `SET LOCAL statement_timeout = $1`,
|
|
79
|
+
params: ['30s'],
|
|
80
|
+
};
|
|
81
|
+
const rateLimiter = new Map();
|
|
82
|
+
const ctx = { pool, metadata, options, authToken, stateDir, statementTimeout, rateLimiter };
|
|
79
83
|
const server = (0, node_http_1.createServer)((req, res) => {
|
|
80
84
|
handleRequest(req, res, ctx).catch((err) => {
|
|
81
85
|
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
@@ -152,6 +156,14 @@ async function handleRequest(req, res, ctx) {
|
|
|
152
156
|
sendJson(res, 401, { error: 'unauthorized — use the URL printed in the terminal' });
|
|
153
157
|
return;
|
|
154
158
|
}
|
|
159
|
+
// Rate limiting — 100 requests per 60 seconds per authenticated session.
|
|
160
|
+
const rateLimitResult = checkRateLimit(ctx.rateLimiter, ctx.authToken);
|
|
161
|
+
if (!rateLimitResult.allowed) {
|
|
162
|
+
const retryAfter = Math.ceil((rateLimitResult.resetAt - Date.now()) / 1000);
|
|
163
|
+
res.setHeader('Retry-After', String(retryAfter));
|
|
164
|
+
sendJson(res, 429, { error: 'Rate limit exceeded', retryAfter });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
155
167
|
if (pathname === '/api/schema' && req.method === 'GET') {
|
|
156
168
|
return apiSchema(res, ctx);
|
|
157
169
|
}
|
|
@@ -192,6 +204,26 @@ function isAuthorized(req, expectedToken) {
|
|
|
192
204
|
}
|
|
193
205
|
return false;
|
|
194
206
|
}
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Rate limiting
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
const RATE_LIMIT_WINDOW_MS = 60_000; // 60 seconds
|
|
211
|
+
const RATE_LIMIT_MAX_REQUESTS = 100;
|
|
212
|
+
function checkRateLimit(limiter, token) {
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
const entry = limiter.get(token);
|
|
215
|
+
if (!entry || now >= entry.resetAt) {
|
|
216
|
+
// Start a new window
|
|
217
|
+
const resetAt = now + RATE_LIMIT_WINDOW_MS;
|
|
218
|
+
limiter.set(token, { count: 1, resetAt });
|
|
219
|
+
return { allowed: true, resetAt };
|
|
220
|
+
}
|
|
221
|
+
entry.count++;
|
|
222
|
+
if (entry.count > RATE_LIMIT_MAX_REQUESTS) {
|
|
223
|
+
return { allowed: false, resetAt: entry.resetAt };
|
|
224
|
+
}
|
|
225
|
+
return { allowed: true, resetAt: entry.resetAt };
|
|
226
|
+
}
|
|
195
227
|
function constantTimeEqual(a, b) {
|
|
196
228
|
if (a.length !== b.length)
|
|
197
229
|
return false;
|
|
@@ -277,7 +309,7 @@ async function apiTableRows(res, ctx, rawTableName, params) {
|
|
|
277
309
|
let mainWhere = '';
|
|
278
310
|
if (hasSearch && pattern !== null) {
|
|
279
311
|
mainValues.push(pattern);
|
|
280
|
-
const conds = textColumns.map((c) => `${(0, index_js_1.quoteIdent)(c)} ILIKE $3`);
|
|
312
|
+
const conds = textColumns.map((c) => `${(0, index_js_1.quoteIdent)(c)} ILIKE $3 ESCAPE '\\'`);
|
|
281
313
|
mainWhere = `WHERE (${conds.join(' OR ')})`;
|
|
282
314
|
}
|
|
283
315
|
// Count query: $1 = pattern (if search)
|
|
@@ -285,7 +317,7 @@ async function apiTableRows(res, ctx, rawTableName, params) {
|
|
|
285
317
|
let countWhere = '';
|
|
286
318
|
if (hasSearch && pattern !== null) {
|
|
287
319
|
countValues.push(pattern);
|
|
288
|
-
const conds = textColumns.map((c) => `${(0, index_js_1.quoteIdent)(c)} ILIKE $1`);
|
|
320
|
+
const conds = textColumns.map((c) => `${(0, index_js_1.quoteIdent)(c)} ILIKE $1 ESCAPE '\\'`);
|
|
289
321
|
countWhere = `WHERE (${conds.join(' OR ')})`;
|
|
290
322
|
}
|
|
291
323
|
const qualifiedTable = `${(0, index_js_1.quoteIdent)(ctx.options.schema)}.${(0, index_js_1.quoteIdent)(table.name)}`;
|
|
@@ -294,7 +326,7 @@ async function apiTableRows(res, ctx, rawTableName, params) {
|
|
|
294
326
|
const client = await ctx.pool.connect();
|
|
295
327
|
try {
|
|
296
328
|
await client.query('BEGIN READ ONLY');
|
|
297
|
-
await client.query(ctx.
|
|
329
|
+
await client.query(ctx.statementTimeout.sql, ctx.statementTimeout.params);
|
|
298
330
|
const result = await client.query(sql, mainValues);
|
|
299
331
|
const countResult = await client.query(countSql, countValues);
|
|
300
332
|
await client.query('COMMIT');
|
|
@@ -351,6 +383,10 @@ async function apiQuery(req, res, ctx) {
|
|
|
351
383
|
sendJson(res, 400, { error: 'missing sql' });
|
|
352
384
|
return;
|
|
353
385
|
}
|
|
386
|
+
if (rawSql.length > 10_000) {
|
|
387
|
+
sendJson(res, 400, { error: 'query too long — maximum 10,000 characters allowed' });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
354
390
|
if (!isReadOnlyStatement(rawSql)) {
|
|
355
391
|
sendJson(res, 400, {
|
|
356
392
|
error: 'only SELECT / WITH statements are allowed in Studio — use the CLI for writes',
|
|
@@ -360,7 +396,7 @@ async function apiQuery(req, res, ctx) {
|
|
|
360
396
|
const client = await ctx.pool.connect();
|
|
361
397
|
try {
|
|
362
398
|
await client.query('BEGIN READ ONLY');
|
|
363
|
-
await client.query(ctx.
|
|
399
|
+
await client.query(ctx.statementTimeout.sql, ctx.statementTimeout.params);
|
|
364
400
|
const started = Date.now();
|
|
365
401
|
const result = await client.query(rawSql);
|
|
366
402
|
const elapsedMs = Date.now() - started;
|
|
@@ -412,7 +448,7 @@ async function apiBuilder(req, res, ctx) {
|
|
|
412
448
|
const client = await ctx.pool.connect();
|
|
413
449
|
try {
|
|
414
450
|
await client.query('BEGIN READ ONLY');
|
|
415
|
-
await client.query(ctx.
|
|
451
|
+
await client.query(ctx.statementTimeout.sql, ctx.statementTimeout.params);
|
|
416
452
|
const started = Date.now();
|
|
417
453
|
const result = await client.query(deferred.sql, deferred.params);
|
|
418
454
|
const elapsedMs = Date.now() - started;
|
|
@@ -608,6 +644,7 @@ function sendJson(res, status, body) {
|
|
|
608
644
|
'Cache-Control': 'no-store',
|
|
609
645
|
'X-Content-Type-Options': 'nosniff',
|
|
610
646
|
'Referrer-Policy': 'no-referrer',
|
|
647
|
+
'Content-Security-Policy': "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; frame-ancestors 'none'",
|
|
611
648
|
});
|
|
612
649
|
res.end(payload);
|
|
613
650
|
}
|
|
@@ -626,6 +663,7 @@ function sendHtml(res, status, body) {
|
|
|
626
663
|
'X-Content-Type-Options': 'nosniff',
|
|
627
664
|
'X-Frame-Options': 'DENY',
|
|
628
665
|
'Referrer-Policy': 'no-referrer',
|
|
666
|
+
'Content-Security-Policy': "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; frame-ancestors 'none'",
|
|
629
667
|
});
|
|
630
668
|
res.end(body);
|
|
631
669
|
}
|
package/dist/cjs/client.js
CHANGED
|
@@ -27,10 +27,35 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
27
27
|
};
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
29
|
exports.TurbineClient = exports.TransactionClient = void 0;
|
|
30
|
+
exports.withRetry = withRetry;
|
|
30
31
|
const pg_1 = __importDefault(require("pg"));
|
|
31
32
|
const errors_js_1 = require("./errors.js");
|
|
32
33
|
const pipeline_js_1 = require("./pipeline.js");
|
|
33
34
|
const index_js_1 = require("./query/index.js");
|
|
35
|
+
async function withRetry(fn, options) {
|
|
36
|
+
const maxAttempts = options?.maxAttempts ?? 3;
|
|
37
|
+
const baseDelay = options?.baseDelay ?? 50;
|
|
38
|
+
const maxDelay = options?.maxDelay ?? 5000;
|
|
39
|
+
let lastError;
|
|
40
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
41
|
+
try {
|
|
42
|
+
return await fn();
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
lastError = err;
|
|
46
|
+
const isRetryable = err &&
|
|
47
|
+
typeof err === 'object' &&
|
|
48
|
+
'isRetryable' in err &&
|
|
49
|
+
err.isRetryable === true;
|
|
50
|
+
if (!isRetryable || attempt === maxAttempts - 1)
|
|
51
|
+
throw err;
|
|
52
|
+
options?.onRetry?.(err, attempt + 1);
|
|
53
|
+
const delay = Math.min(baseDelay * 2 ** attempt + Math.random() * baseDelay, maxDelay);
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
throw lastError;
|
|
58
|
+
}
|
|
34
59
|
/** Maps isolation level names to SQL */
|
|
35
60
|
const ISOLATION_LEVELS = {
|
|
36
61
|
ReadUncommitted: 'READ UNCOMMITTED',
|
|
@@ -79,7 +104,8 @@ class TransactionClient {
|
|
|
79
104
|
// Create a QueryInterface that uses the transaction client as its "pool"
|
|
80
105
|
// We use a proxy pool that routes queries through the transaction client
|
|
81
106
|
const txPool = this.createTxPool();
|
|
82
|
-
|
|
107
|
+
const txOpts = { ...this.queryOptions, _txScoped: true };
|
|
108
|
+
qi = new index_js_1.QueryInterface(txPool, name, this.schema, this.middlewares, txOpts);
|
|
83
109
|
this.tableCache.set(name, qi);
|
|
84
110
|
}
|
|
85
111
|
return qi;
|
|
@@ -542,6 +568,27 @@ class TurbineClient {
|
|
|
542
568
|
}
|
|
543
569
|
}
|
|
544
570
|
// -------------------------------------------------------------------------
|
|
571
|
+
// Retry — automatic retry for retryable errors (deadlock, serialization)
|
|
572
|
+
// -------------------------------------------------------------------------
|
|
573
|
+
/**
|
|
574
|
+
* Execute an async function with automatic retry on retryable errors.
|
|
575
|
+
*
|
|
576
|
+
* Only errors with `isRetryable === true` (DeadlockError, SerializationFailureError)
|
|
577
|
+
* are retried. Uses exponential backoff with jitter.
|
|
578
|
+
*
|
|
579
|
+
* @example
|
|
580
|
+
* ```ts
|
|
581
|
+
* const result = await db.$retry(() =>
|
|
582
|
+
* db.$transaction(async (tx) => {
|
|
583
|
+
* // ... serializable transaction logic
|
|
584
|
+
* }, { isolationLevel: 'Serializable' })
|
|
585
|
+
* );
|
|
586
|
+
* ```
|
|
587
|
+
*/
|
|
588
|
+
async $retry(fn, options) {
|
|
589
|
+
return withRetry(fn, options);
|
|
590
|
+
}
|
|
591
|
+
// -------------------------------------------------------------------------
|
|
545
592
|
// Connection lifecycle
|
|
546
593
|
// -------------------------------------------------------------------------
|
|
547
594
|
/**
|
package/dist/cjs/errors.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* All Turbine errors extend TurbineError which includes a `code` property.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.PipelineError = exports.CheckConstraintError = exports.SerializationFailureError = exports.DeadlockError = exports.NotNullViolationError = exports.ForeignKeyError = exports.UniqueConstraintError = exports.CircularRelationError = exports.MigrationError = exports.RelationError = exports.ConnectionError = exports.ValidationError = exports.TimeoutError = exports.NotFoundError = exports.TurbineError = exports.TurbineErrorCode = void 0;
|
|
9
|
+
exports.OptimisticLockError = exports.PipelineError = exports.ExclusionConstraintError = exports.CheckConstraintError = exports.SerializationFailureError = exports.DeadlockError = exports.NotNullViolationError = exports.ForeignKeyError = exports.UniqueConstraintError = exports.CircularRelationError = exports.MigrationError = exports.RelationError = exports.ConnectionError = exports.ValidationError = exports.TimeoutError = exports.NotFoundError = exports.TurbineError = exports.TurbineErrorCode = void 0;
|
|
10
10
|
exports.setErrorMessageMode = setErrorMessageMode;
|
|
11
11
|
exports.getErrorMessageMode = getErrorMessageMode;
|
|
12
12
|
exports.wrapPgError = wrapPgError;
|
|
@@ -26,6 +26,8 @@ exports.TurbineErrorCode = {
|
|
|
26
26
|
DEADLOCK_DETECTED: 'TURBINE_E012',
|
|
27
27
|
SERIALIZATION_FAILURE: 'TURBINE_E013',
|
|
28
28
|
PIPELINE: 'TURBINE_E014',
|
|
29
|
+
OPTIMISTIC_LOCK: 'TURBINE_E015',
|
|
30
|
+
EXCLUSION_VIOLATION: 'TURBINE_E016',
|
|
29
31
|
};
|
|
30
32
|
/** Base error class for all Turbine errors */
|
|
31
33
|
class TurbineError extends Error {
|
|
@@ -351,6 +353,26 @@ class CheckConstraintError extends TurbineError {
|
|
|
351
353
|
}
|
|
352
354
|
}
|
|
353
355
|
exports.CheckConstraintError = CheckConstraintError;
|
|
356
|
+
class ExclusionConstraintError extends TurbineError {
|
|
357
|
+
constraint;
|
|
358
|
+
table;
|
|
359
|
+
constructor(opts = {}) {
|
|
360
|
+
const { constraint, table, cause } = opts;
|
|
361
|
+
let message = opts.message;
|
|
362
|
+
if (!message) {
|
|
363
|
+
const constraintPart = constraint ? ` on ${constraint}` : '';
|
|
364
|
+
message = `[turbine] Exclusion constraint violation${constraintPart}`;
|
|
365
|
+
const detail = detailFromCause(cause);
|
|
366
|
+
if (detail)
|
|
367
|
+
message += `: ${detail}`;
|
|
368
|
+
}
|
|
369
|
+
super(exports.TurbineErrorCode.EXCLUSION_VIOLATION, message, { cause });
|
|
370
|
+
this.name = 'ExclusionConstraintError';
|
|
371
|
+
this.constraint = constraint;
|
|
372
|
+
this.table = table;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
exports.ExclusionConstraintError = ExclusionConstraintError;
|
|
354
376
|
/**
|
|
355
377
|
* Thrown when a non-transactional pipeline has partial failures.
|
|
356
378
|
*
|
|
@@ -392,6 +414,20 @@ class PipelineError extends TurbineError {
|
|
|
392
414
|
}
|
|
393
415
|
}
|
|
394
416
|
exports.PipelineError = PipelineError;
|
|
417
|
+
class OptimisticLockError extends TurbineError {
|
|
418
|
+
table;
|
|
419
|
+
versionField;
|
|
420
|
+
expectedVersion;
|
|
421
|
+
constructor(opts) {
|
|
422
|
+
super(exports.TurbineErrorCode.OPTIMISTIC_LOCK, `[turbine] Optimistic lock failed on "${opts.table}" — ` +
|
|
423
|
+
`expected ${opts.versionField} = ${opts.expectedVersion} but row was modified by another transaction`);
|
|
424
|
+
this.name = 'OptimisticLockError';
|
|
425
|
+
this.table = opts.table;
|
|
426
|
+
this.versionField = opts.versionField;
|
|
427
|
+
this.expectedVersion = opts.expectedVersion;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
exports.OptimisticLockError = OptimisticLockError;
|
|
395
431
|
/**
|
|
396
432
|
* Parse column names out of a pg `detail` string like:
|
|
397
433
|
* "Key (email)=(foo@bar) already exists."
|
|
@@ -412,6 +448,7 @@ function parseColumnsFromDetail(detail) {
|
|
|
412
448
|
* 23503 (foreign_key_violation) -> ForeignKeyError
|
|
413
449
|
* 23502 (not_null_violation) -> NotNullViolationError
|
|
414
450
|
* 23514 (check_violation) -> CheckConstraintError
|
|
451
|
+
* 23P01 (exclusion_violation) -> ExclusionConstraintError
|
|
415
452
|
* 40P01 (deadlock_detected) -> DeadlockError (retryable)
|
|
416
453
|
* 40001 (serialization_failure) -> SerializationFailureError (retryable)
|
|
417
454
|
*
|
|
@@ -451,6 +488,12 @@ function wrapPgError(err) {
|
|
|
451
488
|
table: e.table,
|
|
452
489
|
cause: err,
|
|
453
490
|
});
|
|
491
|
+
case '23P01':
|
|
492
|
+
return new ExclusionConstraintError({
|
|
493
|
+
constraint: e.constraint,
|
|
494
|
+
table: e.table,
|
|
495
|
+
cause: err,
|
|
496
|
+
});
|
|
454
497
|
case '40P01':
|
|
455
498
|
return new DeadlockError({
|
|
456
499
|
constraint: e.constraint,
|
package/dist/cjs/generate.js
CHANGED
|
@@ -181,6 +181,92 @@ function generateTypes(schema) {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Nested write types (WhereUnique, NestedCreateInput, NestedUpdateInput,
|
|
186
|
+
// ConnectOrCreate, CreateInput, UpdateInput)
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
for (const table of Object.values(schema.tables)) {
|
|
189
|
+
const typeName = entityName(table.name);
|
|
190
|
+
const hasRels = Object.keys(table.relations).length > 0;
|
|
191
|
+
// WhereUnique — union of unique constraint shapes, deduplicating PK
|
|
192
|
+
const seen = new Set();
|
|
193
|
+
const uniqueSets = [];
|
|
194
|
+
// Always include the primary key first
|
|
195
|
+
const pkKey = table.primaryKey.join(',');
|
|
196
|
+
seen.add(pkKey);
|
|
197
|
+
uniqueSets.push(table.primaryKey);
|
|
198
|
+
// Add unique indexes that aren't duplicates of the PK
|
|
199
|
+
for (const uc of table.uniqueColumns) {
|
|
200
|
+
const ucKey = uc.join(',');
|
|
201
|
+
if (!seen.has(ucKey)) {
|
|
202
|
+
seen.add(ucKey);
|
|
203
|
+
uniqueSets.push(uc);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (uniqueSets.length > 0) {
|
|
207
|
+
const branches = uniqueSets.map((cols) => {
|
|
208
|
+
const fields = cols.map((colName) => {
|
|
209
|
+
const col = table.columns.find((c) => c.name === colName);
|
|
210
|
+
const field = col?.field ?? colName;
|
|
211
|
+
const tsType = col?.tsType ?? 'unknown';
|
|
212
|
+
return `${field}: ${tsType}`;
|
|
213
|
+
});
|
|
214
|
+
return `{ ${fields.join('; ')} }`;
|
|
215
|
+
});
|
|
216
|
+
lines.push(`export type ${typeName}WhereUnique = ${branches.join(' | ')};`);
|
|
217
|
+
lines.push('');
|
|
218
|
+
}
|
|
219
|
+
// CreateInput / UpdateInput — extends base type with optional relation fields
|
|
220
|
+
if (hasRels) {
|
|
221
|
+
lines.push(`export type ${typeName}CreateInput = ${typeName}Create & {`);
|
|
222
|
+
for (const [relName, rel] of Object.entries(table.relations)) {
|
|
223
|
+
const targetType = entityName(rel.to);
|
|
224
|
+
lines.push(` ${relName}?: ${targetType}NestedCreateInput;`);
|
|
225
|
+
}
|
|
226
|
+
lines.push('};');
|
|
227
|
+
lines.push('');
|
|
228
|
+
lines.push(`export type ${typeName}UpdateInput = ${typeName}Update & {`);
|
|
229
|
+
for (const [relName, rel] of Object.entries(table.relations)) {
|
|
230
|
+
const targetType = entityName(rel.to);
|
|
231
|
+
if (rel.type === 'hasMany') {
|
|
232
|
+
lines.push(` ${relName}?: ${targetType}NestedUpdateInput;`);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
lines.push(` ${relName}?: ${targetType}NestedCreateInput;`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
lines.push('};');
|
|
239
|
+
lines.push('');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Emit NestedCreateInput, NestedUpdateInput, ConnectOrCreate for every table
|
|
243
|
+
for (const table of Object.values(schema.tables)) {
|
|
244
|
+
const typeName = entityName(table.name);
|
|
245
|
+
const hasRels = Object.keys(table.relations).length > 0;
|
|
246
|
+
// NestedCreateInput uses *CreateInput (which includes relation fields) when
|
|
247
|
+
// the table has relations, otherwise falls back to the plain *Create type.
|
|
248
|
+
const createRefType = hasRels ? `${typeName}CreateInput` : `${typeName}Create`;
|
|
249
|
+
lines.push(`export interface ${typeName}NestedCreateInput {`);
|
|
250
|
+
lines.push(` create?: ${createRefType} | ${createRefType}[];`);
|
|
251
|
+
lines.push(` connect?: ${typeName}WhereUnique | ${typeName}WhereUnique[];`);
|
|
252
|
+
lines.push(` connectOrCreate?: ${typeName}ConnectOrCreate | ${typeName}ConnectOrCreate[];`);
|
|
253
|
+
lines.push('}');
|
|
254
|
+
lines.push('');
|
|
255
|
+
lines.push(`export interface ${typeName}NestedUpdateInput {`);
|
|
256
|
+
lines.push(` create?: ${createRefType} | ${createRefType}[];`);
|
|
257
|
+
lines.push(` connect?: ${typeName}WhereUnique | ${typeName}WhereUnique[];`);
|
|
258
|
+
lines.push(` connectOrCreate?: ${typeName}ConnectOrCreate | ${typeName}ConnectOrCreate[];`);
|
|
259
|
+
lines.push(` disconnect?: ${typeName}WhereUnique | ${typeName}WhereUnique[];`);
|
|
260
|
+
lines.push(` set?: ${typeName}WhereUnique[];`);
|
|
261
|
+
lines.push(` delete?: ${typeName}WhereUnique | ${typeName}WhereUnique[];`);
|
|
262
|
+
lines.push('}');
|
|
263
|
+
lines.push('');
|
|
264
|
+
lines.push(`export interface ${typeName}ConnectOrCreate {`);
|
|
265
|
+
lines.push(` where: ${typeName}WhereUnique;`);
|
|
266
|
+
lines.push(` create: ${createRefType};`);
|
|
267
|
+
lines.push('}');
|
|
268
|
+
lines.push('');
|
|
269
|
+
}
|
|
184
270
|
return lines.join('\n');
|
|
185
271
|
}
|
|
186
272
|
// ---------------------------------------------------------------------------
|
package/dist/cjs/index.js
CHANGED
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
-
exports.
|
|
37
|
+
exports.table = exports.defineSchema = exports.column = exports.ColumnBuilder = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = exports.camelToSnake = exports.QueryInterface = exports.pipelineSupported = exports.executePipeline = exports.hasRelationFields = exports.executeNestedUpdate = exports.executeNestedCreate = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.PipelineError = exports.OptimisticLockError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.ExclusionConstraintError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.postgresDialect = exports.withRetry = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
|
|
38
|
+
exports.turbineHttp = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = void 0;
|
|
38
39
|
var index_js_1 = require("./adapters/index.js");
|
|
39
40
|
Object.defineProperty(exports, "alloydb", { enumerable: true, get: function () { return index_js_1.alloydb; } });
|
|
40
41
|
Object.defineProperty(exports, "cockroachdb", { enumerable: true, get: function () { return index_js_1.cockroachdb; } });
|
|
@@ -45,6 +46,7 @@ Object.defineProperty(exports, "yugabytedb", { enumerable: true, get: function (
|
|
|
45
46
|
var client_js_1 = require("./client.js");
|
|
46
47
|
Object.defineProperty(exports, "TransactionClient", { enumerable: true, get: function () { return client_js_1.TransactionClient; } });
|
|
47
48
|
Object.defineProperty(exports, "TurbineClient", { enumerable: true, get: function () { return client_js_1.TurbineClient; } });
|
|
49
|
+
Object.defineProperty(exports, "withRetry", { enumerable: true, get: function () { return client_js_1.withRetry; } });
|
|
48
50
|
var dialect_js_1 = require("./dialect.js");
|
|
49
51
|
Object.defineProperty(exports, "postgresDialect", { enumerable: true, get: function () { return dialect_js_1.postgresDialect; } });
|
|
50
52
|
// Error types
|
|
@@ -53,11 +55,13 @@ Object.defineProperty(exports, "CheckConstraintError", { enumerable: true, get:
|
|
|
53
55
|
Object.defineProperty(exports, "CircularRelationError", { enumerable: true, get: function () { return errors_js_1.CircularRelationError; } });
|
|
54
56
|
Object.defineProperty(exports, "ConnectionError", { enumerable: true, get: function () { return errors_js_1.ConnectionError; } });
|
|
55
57
|
Object.defineProperty(exports, "DeadlockError", { enumerable: true, get: function () { return errors_js_1.DeadlockError; } });
|
|
58
|
+
Object.defineProperty(exports, "ExclusionConstraintError", { enumerable: true, get: function () { return errors_js_1.ExclusionConstraintError; } });
|
|
56
59
|
Object.defineProperty(exports, "ForeignKeyError", { enumerable: true, get: function () { return errors_js_1.ForeignKeyError; } });
|
|
57
60
|
Object.defineProperty(exports, "getErrorMessageMode", { enumerable: true, get: function () { return errors_js_1.getErrorMessageMode; } });
|
|
58
61
|
Object.defineProperty(exports, "MigrationError", { enumerable: true, get: function () { return errors_js_1.MigrationError; } });
|
|
59
62
|
Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return errors_js_1.NotFoundError; } });
|
|
60
63
|
Object.defineProperty(exports, "NotNullViolationError", { enumerable: true, get: function () { return errors_js_1.NotNullViolationError; } });
|
|
64
|
+
Object.defineProperty(exports, "OptimisticLockError", { enumerable: true, get: function () { return errors_js_1.OptimisticLockError; } });
|
|
61
65
|
Object.defineProperty(exports, "PipelineError", { enumerable: true, get: function () { return errors_js_1.PipelineError; } });
|
|
62
66
|
Object.defineProperty(exports, "RelationError", { enumerable: true, get: function () { return errors_js_1.RelationError; } });
|
|
63
67
|
Object.defineProperty(exports, "SerializationFailureError", { enumerable: true, get: function () { return errors_js_1.SerializationFailureError; } });
|
|
@@ -74,6 +78,11 @@ Object.defineProperty(exports, "generate", { enumerable: true, get: function ()
|
|
|
74
78
|
// Introspection
|
|
75
79
|
var introspect_js_1 = require("./introspect.js");
|
|
76
80
|
Object.defineProperty(exports, "introspect", { enumerable: true, get: function () { return introspect_js_1.introspect; } });
|
|
81
|
+
// Nested writes
|
|
82
|
+
var nested_write_js_1 = require("./nested-write.js");
|
|
83
|
+
Object.defineProperty(exports, "executeNestedCreate", { enumerable: true, get: function () { return nested_write_js_1.executeNestedCreate; } });
|
|
84
|
+
Object.defineProperty(exports, "executeNestedUpdate", { enumerable: true, get: function () { return nested_write_js_1.executeNestedUpdate; } });
|
|
85
|
+
Object.defineProperty(exports, "hasRelationFields", { enumerable: true, get: function () { return nested_write_js_1.hasRelationFields; } });
|
|
77
86
|
// Pipeline
|
|
78
87
|
var pipeline_js_1 = require("./pipeline.js");
|
|
79
88
|
Object.defineProperty(exports, "executePipeline", { enumerable: true, get: function () { return pipeline_js_1.executePipeline; } });
|