turbine-orm 0.7.0 → 0.8.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 +134 -40
- package/dist/cjs/cli/index.js +72 -3
- package/dist/cjs/cli/loader.js +129 -0
- package/dist/cjs/cli/migrate.js +33 -9
- package/dist/cjs/client.js +92 -8
- package/dist/cjs/errors.js +177 -4
- package/dist/cjs/generate.js +120 -9
- package/dist/cjs/index.js +7 -1
- package/dist/cjs/pipeline-submittable.js +403 -0
- package/dist/cjs/pipeline.js +90 -37
- package/dist/cjs/query.js +943 -137
- package/dist/cjs/schema-builder.js +57 -6
- package/dist/cjs/schema-sql.js +85 -19
- package/dist/cjs/serverless.js +8 -7
- package/dist/cli/index.js +72 -3
- package/dist/cli/loader.d.ts +45 -0
- package/dist/cli/loader.js +91 -0
- package/dist/cli/migrate.d.ts +7 -1
- package/dist/cli/migrate.js +33 -9
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +47 -3
- package/dist/client.js +94 -10
- package/dist/errors.d.ts +132 -1
- package/dist/errors.js +171 -3
- package/dist/generate.d.ts +6 -0
- package/dist/generate.js +120 -10
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/pipeline-submittable.d.ts +94 -0
- package/dist/pipeline-submittable.js +397 -0
- package/dist/pipeline.d.ts +37 -9
- package/dist/pipeline.js +89 -37
- package/dist/query.d.ts +268 -17
- package/dist/query.js +941 -137
- package/dist/schema-builder.d.ts +36 -3
- package/dist/schema-builder.js +57 -6
- package/dist/schema-sql.js +85 -19
- package/dist/serverless.d.ts +8 -7
- package/dist/serverless.js +8 -7
- package/package.json +3 -3
package/dist/errors.js
CHANGED
|
@@ -17,6 +17,9 @@ export const TurbineErrorCode = {
|
|
|
17
17
|
FOREIGN_KEY_VIOLATION: 'TURBINE_E009',
|
|
18
18
|
NOT_NULL_VIOLATION: 'TURBINE_E010',
|
|
19
19
|
CHECK_VIOLATION: 'TURBINE_E011',
|
|
20
|
+
DEADLOCK_DETECTED: 'TURBINE_E012',
|
|
21
|
+
SERIALIZATION_FAILURE: 'TURBINE_E013',
|
|
22
|
+
PIPELINE: 'TURBINE_E014',
|
|
20
23
|
};
|
|
21
24
|
/** Base error class for all Turbine errors */
|
|
22
25
|
export class TurbineError extends Error {
|
|
@@ -27,6 +30,47 @@ export class TurbineError extends Error {
|
|
|
27
30
|
this.code = code;
|
|
28
31
|
}
|
|
29
32
|
}
|
|
33
|
+
let errorMessageMode = 'safe';
|
|
34
|
+
/**
|
|
35
|
+
* Set the global NotFoundError message mode. Called from the TurbineClient
|
|
36
|
+
* constructor when `TurbineConfig.errorMessages` is provided.
|
|
37
|
+
*
|
|
38
|
+
* - `'safe'` (default): the message includes only the keys of the where
|
|
39
|
+
* clause (e.g. `where: { id, email }`). Values are redacted.
|
|
40
|
+
* - `'verbose'`: the message includes the full JSON-serialized where
|
|
41
|
+
* clause (e.g. `where: {"id":1,"email":"alice@x.com"}`).
|
|
42
|
+
*/
|
|
43
|
+
export function setErrorMessageMode(mode) {
|
|
44
|
+
errorMessageMode = mode;
|
|
45
|
+
}
|
|
46
|
+
/** Returns the current NotFoundError message mode. Exported for tests. */
|
|
47
|
+
export function getErrorMessageMode() {
|
|
48
|
+
return errorMessageMode;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Render a `where` clause for error messages. In 'safe' mode (the default),
|
|
52
|
+
* only the keys are shown; values are stripped to avoid leaking PII into logs.
|
|
53
|
+
* Nested AND/OR/NOT combinators are recursively rendered.
|
|
54
|
+
*/
|
|
55
|
+
function renderWhereForMessage(where, mode) {
|
|
56
|
+
if (mode === 'verbose') {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.stringify(where);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return '[unserializable]';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// safe mode: keys only
|
|
65
|
+
if (where === null || where === undefined)
|
|
66
|
+
return '';
|
|
67
|
+
if (typeof where !== 'object')
|
|
68
|
+
return '';
|
|
69
|
+
const keys = Object.keys(where);
|
|
70
|
+
if (keys.length === 0)
|
|
71
|
+
return '{}';
|
|
72
|
+
return `{ ${keys.join(', ')} }`;
|
|
73
|
+
}
|
|
30
74
|
/**
|
|
31
75
|
* Thrown when a record is not found (findUniqueOrThrow, findFirstOrThrow,
|
|
32
76
|
* update/delete against a non-matching row, etc.)
|
|
@@ -36,8 +80,16 @@ export class TurbineError extends Error {
|
|
|
36
80
|
* - `new NotFoundError({ table, where, operation, cause, message })`
|
|
37
81
|
*
|
|
38
82
|
* When called with an options object and no explicit `message`, a Prisma-style
|
|
39
|
-
* message is built automatically,
|
|
83
|
+
* message is built automatically. By default, only the where-clause keys are
|
|
84
|
+
* shown to avoid leaking PII into logs:
|
|
85
|
+
* `[turbine] findUniqueOrThrow on "users" found no record matching where: { id }`
|
|
86
|
+
*
|
|
87
|
+
* Set `setErrorMessageMode('verbose')` (or pass `errorMessages: 'verbose'` to
|
|
88
|
+
* the TurbineClient constructor) to include the full where values:
|
|
40
89
|
* `[turbine] findUniqueOrThrow on "users" found no record matching where: {"id":1}`
|
|
90
|
+
*
|
|
91
|
+
* The full `where` object, `table`, and `operation` are always available as
|
|
92
|
+
* structured properties on the error instance regardless of mode.
|
|
41
93
|
*/
|
|
42
94
|
export class NotFoundError extends TurbineError {
|
|
43
95
|
table;
|
|
@@ -54,11 +106,11 @@ export class NotFoundError extends TurbineError {
|
|
|
54
106
|
let message = input.message;
|
|
55
107
|
if (!message) {
|
|
56
108
|
if (operation && table) {
|
|
57
|
-
const wherePart = where !== undefined ? ` matching where: ${
|
|
109
|
+
const wherePart = where !== undefined ? ` matching where: ${renderWhereForMessage(where, errorMessageMode)}` : '';
|
|
58
110
|
message = `[turbine] ${operation} on "${table}" found no record${wherePart}`;
|
|
59
111
|
}
|
|
60
112
|
else if (table) {
|
|
61
|
-
const wherePart = where !== undefined ? ` matching where ${
|
|
113
|
+
const wherePart = where !== undefined ? ` matching where ${renderWhereForMessage(where, errorMessageMode)}` : '';
|
|
62
114
|
message = `[turbine] No record found in "${table}"${wherePart}`;
|
|
63
115
|
}
|
|
64
116
|
else {
|
|
@@ -194,6 +246,71 @@ export class NotNullViolationError extends TurbineError {
|
|
|
194
246
|
this.table = table;
|
|
195
247
|
}
|
|
196
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Thrown when Postgres detects a deadlock (pg code 40P01).
|
|
251
|
+
*
|
|
252
|
+
* This error is **retryable** — when caught, callers can safely retry the
|
|
253
|
+
* transaction (typically with backoff). Catch it explicitly:
|
|
254
|
+
*
|
|
255
|
+
* ```ts
|
|
256
|
+
* try {
|
|
257
|
+
* await db.$transaction(async (tx) => { ... });
|
|
258
|
+
* } catch (err) {
|
|
259
|
+
* if (err instanceof DeadlockError) {
|
|
260
|
+
* // safe to retry
|
|
261
|
+
* }
|
|
262
|
+
* }
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
export class DeadlockError extends TurbineError {
|
|
266
|
+
/** Marks this error as safe to retry */
|
|
267
|
+
isRetryable = true;
|
|
268
|
+
constraint;
|
|
269
|
+
constructor(opts = {}) {
|
|
270
|
+
const { constraint, cause } = opts;
|
|
271
|
+
let message = opts.message;
|
|
272
|
+
if (!message) {
|
|
273
|
+
const pgMessage = cause?.message;
|
|
274
|
+
message = pgMessage ? `[turbine] Deadlock detected: ${pgMessage}` : '[turbine] Deadlock detected';
|
|
275
|
+
}
|
|
276
|
+
super(TurbineErrorCode.DEADLOCK_DETECTED, message, { cause });
|
|
277
|
+
this.name = 'DeadlockError';
|
|
278
|
+
this.constraint = constraint;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Thrown when a Serializable transaction fails due to a serialization
|
|
283
|
+
* conflict (pg code 40001 — `could not serialize access due to ...`).
|
|
284
|
+
*
|
|
285
|
+
* This error is **retryable** — by Postgres documentation, the recommended
|
|
286
|
+
* response is to re-run the entire transaction. Catch it explicitly:
|
|
287
|
+
*
|
|
288
|
+
* ```ts
|
|
289
|
+
* try {
|
|
290
|
+
* await db.$transaction(async (tx) => { ... }, { isolationLevel: 'Serializable' });
|
|
291
|
+
* } catch (err) {
|
|
292
|
+
* if (err instanceof SerializationFailureError) {
|
|
293
|
+
* // safe to retry the whole transaction
|
|
294
|
+
* }
|
|
295
|
+
* }
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
export class SerializationFailureError extends TurbineError {
|
|
299
|
+
/** Marks this error as safe to retry */
|
|
300
|
+
isRetryable = true;
|
|
301
|
+
constructor(opts = {}) {
|
|
302
|
+
const { cause } = opts;
|
|
303
|
+
let message = opts.message;
|
|
304
|
+
if (!message) {
|
|
305
|
+
const pgMessage = cause?.message;
|
|
306
|
+
message = pgMessage
|
|
307
|
+
? `[turbine] Serializable transaction conflict: ${pgMessage}`
|
|
308
|
+
: '[turbine] Serializable transaction conflict';
|
|
309
|
+
}
|
|
310
|
+
super(TurbineErrorCode.SERIALIZATION_FAILURE, message, { cause });
|
|
311
|
+
this.name = 'SerializationFailureError';
|
|
312
|
+
}
|
|
313
|
+
}
|
|
197
314
|
/** Thrown when a CHECK constraint is violated (pg code 23514) */
|
|
198
315
|
export class CheckConstraintError extends TurbineError {
|
|
199
316
|
constraint;
|
|
@@ -214,6 +331,46 @@ export class CheckConstraintError extends TurbineError {
|
|
|
214
331
|
this.table = table;
|
|
215
332
|
}
|
|
216
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Thrown when a non-transactional pipeline has partial failures.
|
|
336
|
+
*
|
|
337
|
+
* In non-transactional mode (`{ transactional: false }`), each query executes
|
|
338
|
+
* independently. If one or more queries fail, the pipeline rejects with a
|
|
339
|
+
* `PipelineError` that carries per-query results so callers can inspect which
|
|
340
|
+
* succeeded and which failed.
|
|
341
|
+
*
|
|
342
|
+
* ```ts
|
|
343
|
+
* try {
|
|
344
|
+
* await db.pipeline([q1, q2, q3], { transactional: false });
|
|
345
|
+
* } catch (err) {
|
|
346
|
+
* if (err instanceof PipelineError) {
|
|
347
|
+
* for (const slot of err.results) {
|
|
348
|
+
* if (slot.status === 'error') console.error(slot.error);
|
|
349
|
+
* }
|
|
350
|
+
* }
|
|
351
|
+
* }
|
|
352
|
+
* ```
|
|
353
|
+
*/
|
|
354
|
+
export class PipelineError extends TurbineError {
|
|
355
|
+
/** Per-query results: each slot is either `{status:'ok', value}` or `{status:'error', error}` */
|
|
356
|
+
results;
|
|
357
|
+
/** Zero-based index of the first query that failed */
|
|
358
|
+
failedIndex;
|
|
359
|
+
/** Tag of the first query that failed (from DeferredQuery.tag) */
|
|
360
|
+
failedTag;
|
|
361
|
+
constructor(opts) {
|
|
362
|
+
const { results, failedIndex, failedTag, cause } = opts;
|
|
363
|
+
const failedCount = results.filter((r) => r.status === 'error').length;
|
|
364
|
+
const message = opts.message ??
|
|
365
|
+
`[turbine] Pipeline completed with ${failedCount} error(s) out of ${results.length} queries` +
|
|
366
|
+
(failedTag ? ` (first failure: ${failedTag} at index ${failedIndex})` : '');
|
|
367
|
+
super(TurbineErrorCode.PIPELINE, message, { cause });
|
|
368
|
+
this.name = 'PipelineError';
|
|
369
|
+
this.results = results;
|
|
370
|
+
this.failedIndex = failedIndex;
|
|
371
|
+
this.failedTag = failedTag;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
217
374
|
/**
|
|
218
375
|
* Parse column names out of a pg `detail` string like:
|
|
219
376
|
* "Key (email)=(foo@bar) already exists."
|
|
@@ -234,6 +391,8 @@ function parseColumnsFromDetail(detail) {
|
|
|
234
391
|
* 23503 (foreign_key_violation) -> ForeignKeyError
|
|
235
392
|
* 23502 (not_null_violation) -> NotNullViolationError
|
|
236
393
|
* 23514 (check_violation) -> CheckConstraintError
|
|
394
|
+
* 40P01 (deadlock_detected) -> DeadlockError (retryable)
|
|
395
|
+
* 40001 (serialization_failure) -> SerializationFailureError (retryable)
|
|
237
396
|
*
|
|
238
397
|
* The original pg error is preserved as `.cause` on the wrapped error.
|
|
239
398
|
*/
|
|
@@ -271,6 +430,15 @@ export function wrapPgError(err) {
|
|
|
271
430
|
table: e.table,
|
|
272
431
|
cause: err,
|
|
273
432
|
});
|
|
433
|
+
case '40P01':
|
|
434
|
+
return new DeadlockError({
|
|
435
|
+
constraint: e.constraint,
|
|
436
|
+
cause: err,
|
|
437
|
+
});
|
|
438
|
+
case '40001':
|
|
439
|
+
return new SerializationFailureError({
|
|
440
|
+
cause: err,
|
|
441
|
+
});
|
|
274
442
|
default:
|
|
275
443
|
return err;
|
|
276
444
|
}
|
package/dist/generate.d.ts
CHANGED
|
@@ -21,4 +21,10 @@ export declare function generate(options: GenerateOptions): {
|
|
|
21
21
|
outDir: string;
|
|
22
22
|
files: string[];
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Generate the contents of `types.ts` (entity interfaces, *Create / *Update,
|
|
26
|
+
* and *Relations brand-field interfaces). Exported so tests can pin the
|
|
27
|
+
* generator output without writing files to disk.
|
|
28
|
+
*/
|
|
29
|
+
export declare function generateTypes(schema: SchemaMetadata): string;
|
|
24
30
|
//# sourceMappingURL=generate.d.ts.map
|
package/dist/generate.js
CHANGED
|
@@ -60,8 +60,33 @@ function generatedFileHeader() {
|
|
|
60
60
|
'',
|
|
61
61
|
];
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Generate the contents of `types.ts` (entity interfaces, *Create / *Update,
|
|
65
|
+
* and *Relations brand-field interfaces). Exported so tests can pin the
|
|
66
|
+
* generator output without writing files to disk.
|
|
67
|
+
*/
|
|
68
|
+
export function generateTypes(schema) {
|
|
64
69
|
const lines = [...generatedFileHeader()];
|
|
70
|
+
// We import UpdateOperatorInput so generated *Update types can express
|
|
71
|
+
// atomic increment / decrement / multiply / divide / set operators on
|
|
72
|
+
// numeric columns (TASK-3.4).
|
|
73
|
+
//
|
|
74
|
+
// RelationDescriptor is the brand-field interface that lets `WithResult`
|
|
75
|
+
// recurse through nested `with` clauses at any depth. The generator emits
|
|
76
|
+
// each `*Relations` member as a `RelationDescriptor<Target, Cardinality,
|
|
77
|
+
// TargetRelations>` so users get full deep `with`-clause type inference
|
|
78
|
+
// out of the box (TASK-2.1).
|
|
79
|
+
lines.push("import type { RelationDescriptor, UpdateOperatorInput } from 'turbine-orm';");
|
|
80
|
+
lines.push('');
|
|
81
|
+
// Pre-compute which tables have relations so we know whether to thread
|
|
82
|
+
// `${TargetType}Relations` (for deep inference) or `{}` (the no-relations
|
|
83
|
+
// default) into each `RelationDescriptor`. Built once up-front because
|
|
84
|
+
// relations can point at tables we haven't iterated to yet.
|
|
85
|
+
const tablesWithRelations = new Set();
|
|
86
|
+
for (const t of Object.values(schema.tables)) {
|
|
87
|
+
if (Object.keys(t.relations).length > 0)
|
|
88
|
+
tablesWithRelations.add(t.name);
|
|
89
|
+
}
|
|
65
90
|
// Generate enum types
|
|
66
91
|
for (const [enumName, labels] of Object.entries(schema.enums)) {
|
|
67
92
|
const typeName = snakeToPascal(enumName);
|
|
@@ -103,27 +128,33 @@ function generateTypes(schema) {
|
|
|
103
128
|
lines.push('};');
|
|
104
129
|
lines.push('');
|
|
105
130
|
// --- Update input type (all fields optional except PK) ---
|
|
131
|
+
// Numeric columns additionally accept `UpdateOperatorInput<number>` so
|
|
132
|
+
// users can write `{ viewCount: { increment: 1 } }` without an `as any`.
|
|
106
133
|
const nonPkCols = table.columns.filter((c) => !table.primaryKey.includes(c.name));
|
|
107
134
|
lines.push(`/** Input type for updating a row in \`${table.name}\` */`);
|
|
108
135
|
lines.push(`export type ${typeName}Update = {`);
|
|
109
136
|
for (const col of nonPkCols) {
|
|
110
|
-
lines.push(` ${col.field}?: ${col.tsType};`);
|
|
137
|
+
lines.push(` ${col.field}?: ${updateFieldType(col.tsType)};`);
|
|
111
138
|
}
|
|
112
139
|
lines.push('};');
|
|
113
140
|
lines.push('');
|
|
114
141
|
// --- Relations map (for type-safe `with` clauses) ---
|
|
142
|
+
//
|
|
143
|
+
// Each relation is emitted as a `RelationDescriptor<Target, Cardinality,
|
|
144
|
+
// TargetRelations>` brand-field interface. This is what enables the
|
|
145
|
+
// recursive `WithResult` type to walk through nested `with` clauses at
|
|
146
|
+
// any depth — `RelationRelations<R[K]>` reads the third type parameter
|
|
147
|
+
// and threads it into the next recursion step. If the target table has
|
|
148
|
+
// no relations of its own, the descriptor uses `{}` (the default).
|
|
115
149
|
const hasRelations = Object.keys(table.relations).length > 0;
|
|
116
150
|
if (hasRelations) {
|
|
117
151
|
lines.push(`/** Available relations for the \`${table.name}\` table */`);
|
|
118
152
|
lines.push(`export interface ${typeName}Relations {`);
|
|
119
153
|
for (const [relName, rel] of Object.entries(table.relations)) {
|
|
120
154
|
const targetType = entityName(rel.to);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
lines.push(` ${relName}: ${targetType} | null;`);
|
|
126
|
-
}
|
|
155
|
+
const cardinality = rel.type === 'hasMany' ? "'many'" : "'one'";
|
|
156
|
+
const targetRelations = tablesWithRelations.has(rel.to) ? `${targetType}Relations` : '{}';
|
|
157
|
+
lines.push(` ${relName}: RelationDescriptor<${targetType}, ${cardinality}, ${targetRelations}>;`);
|
|
127
158
|
}
|
|
128
159
|
lines.push('}');
|
|
129
160
|
lines.push('');
|
|
@@ -227,8 +258,8 @@ function generateIndex(schema) {
|
|
|
227
258
|
const tableEntries = Object.values(schema.tables);
|
|
228
259
|
const lines = [
|
|
229
260
|
...generatedFileHeader(),
|
|
230
|
-
"import { TurbineClient as BaseTurbineClient, QueryInterface } from 'turbine-orm';",
|
|
231
|
-
"import type { TurbineConfig } from 'turbine-orm';",
|
|
261
|
+
"import { TurbineClient as BaseTurbineClient, TransactionClient as BaseTransactionClient, QueryInterface } from 'turbine-orm';",
|
|
262
|
+
"import type { TurbineConfig, TransactionOptions } from 'turbine-orm';",
|
|
232
263
|
"import { SCHEMA } from './metadata.js';",
|
|
233
264
|
];
|
|
234
265
|
// Import all entity types and relations maps
|
|
@@ -241,6 +272,41 @@ function generateIndex(schema) {
|
|
|
241
272
|
}
|
|
242
273
|
lines.push(`import type { ${typeImports.join(', ')} } from './types.js';`);
|
|
243
274
|
lines.push('');
|
|
275
|
+
// -------------------------------------------------------------------------
|
|
276
|
+
// TypedTransactionClient — same typed table accessors as TurbineClient,
|
|
277
|
+
// but scoped to a single transaction connection. The runtime instance is
|
|
278
|
+
// an ordinary `TransactionClient` from turbine-orm; this declaration just
|
|
279
|
+
// teaches TypeScript about the auto-attached accessors so users get
|
|
280
|
+
// autocomplete inside `db.$transaction(async (tx) => tx.users.create(...))`.
|
|
281
|
+
// -------------------------------------------------------------------------
|
|
282
|
+
lines.push('/**');
|
|
283
|
+
lines.push(' * Transaction-scoped client with the same typed table accessors as TurbineClient.');
|
|
284
|
+
lines.push(' * Created automatically by `db.$transaction(async (tx) => ...)` — never instantiate');
|
|
285
|
+
lines.push(' * directly. All queries run on a dedicated connection within a BEGIN/COMMIT block.');
|
|
286
|
+
lines.push(' */');
|
|
287
|
+
lines.push('export class TypedTransactionClient extends BaseTransactionClient {');
|
|
288
|
+
for (const table of tableEntries) {
|
|
289
|
+
const typeName = entityName(table.name);
|
|
290
|
+
const accessor = snakeToCamelStr(table.name);
|
|
291
|
+
const hasRelations = Object.keys(table.relations).length > 0;
|
|
292
|
+
const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
|
|
293
|
+
lines.push(` /** Query interface for the \`${table.name}\` table (transaction-scoped) */`);
|
|
294
|
+
lines.push(` declare readonly ${accessor}: QueryInterface<${genericArgs}>;`);
|
|
295
|
+
}
|
|
296
|
+
lines.push('}');
|
|
297
|
+
lines.push('');
|
|
298
|
+
// Augment the class with a typed `$transaction` overload via interface
|
|
299
|
+
// merging. This adds an additional callable signature whose callback
|
|
300
|
+
// parameter is narrowed to `TypedTransactionClient`, while the base
|
|
301
|
+
// signature (callback parameter `BaseTransactionClient`) remains valid.
|
|
302
|
+
lines.push('export interface TypedTransactionClient {');
|
|
303
|
+
lines.push(' /**');
|
|
304
|
+
lines.push(' * Nested transaction via SAVEPOINT. The callback receives a typed');
|
|
305
|
+
lines.push(' * `TypedTransactionClient` so all table accessors auto-complete.');
|
|
306
|
+
lines.push(' */');
|
|
307
|
+
lines.push(' $transaction<R>(fn: (tx: TypedTransactionClient) => Promise<R>): Promise<R>;');
|
|
308
|
+
lines.push('}');
|
|
309
|
+
lines.push('');
|
|
244
310
|
// Generate the client class with JSDoc
|
|
245
311
|
lines.push('/**');
|
|
246
312
|
lines.push(' * Generated Turbine client with typed table accessors.');
|
|
@@ -275,6 +341,22 @@ function generateIndex(schema) {
|
|
|
275
341
|
lines.push(' }');
|
|
276
342
|
lines.push('}');
|
|
277
343
|
lines.push('');
|
|
344
|
+
// Augment TurbineClient via interface merging with a typed $transaction
|
|
345
|
+
// overload. The callback parameter is narrowed to `TypedTransactionClient`
|
|
346
|
+
// so users get autocomplete on `tx.users`, `tx.posts`, etc. The base
|
|
347
|
+
// signature (callback parameter `BaseTransactionClient`) remains valid as
|
|
348
|
+
// an overload, so prior usage continues to typecheck.
|
|
349
|
+
lines.push('export interface TurbineClient {');
|
|
350
|
+
lines.push(' /**');
|
|
351
|
+
lines.push(' * Run a callback inside a transaction. The callback receives a typed');
|
|
352
|
+
lines.push(' * `TypedTransactionClient` with autocompletion for every table accessor.');
|
|
353
|
+
lines.push(' */');
|
|
354
|
+
lines.push(' $transaction<R>(');
|
|
355
|
+
lines.push(' fn: (tx: TypedTransactionClient) => Promise<R>,');
|
|
356
|
+
lines.push(' options?: TransactionOptions,');
|
|
357
|
+
lines.push(' ): Promise<R>;');
|
|
358
|
+
lines.push('}');
|
|
359
|
+
lines.push('');
|
|
278
360
|
// Factory function with JSDoc
|
|
279
361
|
lines.push('/**');
|
|
280
362
|
lines.push(' * Create a new Turbine client instance.');
|
|
@@ -316,4 +398,32 @@ function quoteIfNeeded(s) {
|
|
|
316
398
|
function snakeToCamelStr(s) {
|
|
317
399
|
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
318
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Build the update-input field type for a column. Numeric columns become
|
|
403
|
+
* `T | UpdateOperatorInput<number> | null?` so users can write atomic
|
|
404
|
+
* operators (`{ increment: 1 }`, `{ multiply: 2 }`, etc.) without casts.
|
|
405
|
+
*
|
|
406
|
+
* The check is purely structural — if the column's TS type contains
|
|
407
|
+
* `'number'` (e.g. `number`, `number | null`), it's eligible. Other
|
|
408
|
+
* scalar types (`string`, `boolean`, `Date`, `unknown`, `Buffer`,
|
|
409
|
+
* `Date | null`, etc.) pass through unchanged.
|
|
410
|
+
*/
|
|
411
|
+
function updateFieldType(tsType) {
|
|
412
|
+
// Strip parens for the regex check; preserve the original string in the output.
|
|
413
|
+
if (containsNumberType(tsType)) {
|
|
414
|
+
return `${tsType} | UpdateOperatorInput<number>`;
|
|
415
|
+
}
|
|
416
|
+
return tsType;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Detect whether a TypeScript type expression contains the `number` primitive
|
|
420
|
+
* as a top-level union member. Conservative on purpose — only matches
|
|
421
|
+
* `number`, `number | null`, `null | number`, etc., not `number[]` or
|
|
422
|
+
* `Record<string, number>`.
|
|
423
|
+
*/
|
|
424
|
+
function containsNumberType(tsType) {
|
|
425
|
+
// Tokenize on `|` and check each member.
|
|
426
|
+
const parts = tsType.split('|').map((p) => p.trim());
|
|
427
|
+
return parts.some((p) => p === 'number');
|
|
428
|
+
}
|
|
319
429
|
//# sourceMappingURL=generate.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
* ```
|
|
34
34
|
*/
|
|
35
35
|
export { type Middleware, type MiddlewareNext, type MiddlewareParams, type PgCompatPool, type PgCompatPoolClient, type PgCompatQueryResult, TransactionClient, type TransactionOptions, TurbineClient, type TurbineConfig, } from './client.js';
|
|
36
|
-
export { CheckConstraintError, CircularRelationError, ConnectionError, ForeignKeyError, MigrationError, NotFoundError, NotNullViolationError, RelationError, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, ValidationError, wrapPgError, } from './errors.js';
|
|
36
|
+
export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, type ErrorMessageMode, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, PipelineError, type PipelineResultSlot, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, ValidationError, wrapPgError, } from './errors.js';
|
|
37
37
|
export { type GenerateOptions, generate } from './generate.js';
|
|
38
38
|
export { type IntrospectOptions, introspect } from './introspect.js';
|
|
39
|
-
export { executePipeline, type PipelineResults } from './pipeline.js';
|
|
40
|
-
export { type AggregateArgs, type AggregateResult, type ArrayFilter, type CountArgs, type CreateArgs, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GroupByArgs, type JsonFilter, type OrderDirection, QueryInterface, type RelationFilter, type TypedWithClause, type UpdateArgs, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type WithClause, type WithOptions, type WithResult, } from './query.js';
|
|
39
|
+
export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
|
|
40
|
+
export { type AggregateArgs, type AggregateResult, type ArrayFilter, type CountArgs, type CreateArgs, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GroupByArgs, type JsonFilter, type OrderDirection, QueryInterface, type RelationDescriptor, type RelationFilter, type TypedWithClause, type UpdateArgs, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type WithClause, type WithOptions, type WithResult, } from './query.js';
|
|
41
41
|
export type { ColumnMetadata, IndexMetadata, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
42
42
|
export { camelToSnake, isDateType, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
43
43
|
export { ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnType, type ColumnTypeName, column, defineSchema, type SchemaDef, type TableDef, table, } from './schema-builder.js';
|
package/dist/index.js
CHANGED
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
// Client
|
|
36
36
|
export { TransactionClient, TurbineClient, } from './client.js';
|
|
37
37
|
// Error types
|
|
38
|
-
export { CheckConstraintError, CircularRelationError, ConnectionError, ForeignKeyError, MigrationError, NotFoundError, NotNullViolationError, RelationError, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, ValidationError, wrapPgError, } from './errors.js';
|
|
38
|
+
export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, PipelineError, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, ValidationError, wrapPgError, } from './errors.js';
|
|
39
39
|
// Code generation
|
|
40
40
|
export { generate } from './generate.js';
|
|
41
41
|
// Introspection
|
|
42
42
|
export { introspect } from './introspect.js';
|
|
43
43
|
// Pipeline
|
|
44
|
-
export { executePipeline } from './pipeline.js';
|
|
44
|
+
export { executePipeline, pipelineSupported } from './pipeline.js';
|
|
45
45
|
// Query builder
|
|
46
46
|
export { QueryInterface, } from './query.js';
|
|
47
47
|
// Schema utilities
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm — Real Postgres pipeline protocol implementation
|
|
3
|
+
*
|
|
4
|
+
* Uses the pg extended-query protocol wire methods (parse/bind/describe/execute/sync)
|
|
5
|
+
* exposed on pg.Client's Connection object to send multiple queries in a single
|
|
6
|
+
* TCP flush. This achieves true 1-RTT pipeline execution instead of the sequential
|
|
7
|
+
* await-per-query approach.
|
|
8
|
+
*
|
|
9
|
+
* The approach (listener-swap):
|
|
10
|
+
* 1. Detach the pg.Client's event listeners from the Connection
|
|
11
|
+
* 2. Attach our own state-machine listeners
|
|
12
|
+
* 3. Cork the TCP stream, push all protocol messages, uncork (one TCP write)
|
|
13
|
+
* 4. Drive a state machine over backend response events
|
|
14
|
+
* 5. Restore original listeners and release the client
|
|
15
|
+
*
|
|
16
|
+
* This is the same pattern used by pg-cursor and pg-query-stream, but extended
|
|
17
|
+
* to handle N queries in a single pipeline.
|
|
18
|
+
*/
|
|
19
|
+
import type { EventEmitter } from 'node:events';
|
|
20
|
+
import type { DeferredQuery } from './query.js';
|
|
21
|
+
/** The pg Connection object — an EventEmitter with wire-protocol methods */
|
|
22
|
+
export interface PgConnection extends EventEmitter {
|
|
23
|
+
stream: {
|
|
24
|
+
cork?: () => void;
|
|
25
|
+
uncork?: () => void;
|
|
26
|
+
writable?: boolean;
|
|
27
|
+
destroy?: (err?: Error) => void;
|
|
28
|
+
write?: (...args: unknown[]) => boolean;
|
|
29
|
+
};
|
|
30
|
+
parse(query: {
|
|
31
|
+
text: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
types?: number[];
|
|
34
|
+
}): void;
|
|
35
|
+
bind(config: {
|
|
36
|
+
portal?: string;
|
|
37
|
+
statement?: string;
|
|
38
|
+
values?: unknown[];
|
|
39
|
+
binary?: boolean;
|
|
40
|
+
valueMapper?: (val: unknown, index: number) => unknown;
|
|
41
|
+
}): void;
|
|
42
|
+
describe(msg: {
|
|
43
|
+
type: 'S' | 'P';
|
|
44
|
+
name?: string;
|
|
45
|
+
}): void;
|
|
46
|
+
execute(config: {
|
|
47
|
+
portal?: string;
|
|
48
|
+
rows?: number;
|
|
49
|
+
}): void;
|
|
50
|
+
sync(): void;
|
|
51
|
+
}
|
|
52
|
+
/** A pg PoolClient with the internal fields we need */
|
|
53
|
+
export interface PgPoolClient {
|
|
54
|
+
connection: PgConnection;
|
|
55
|
+
/** pg.Client sets this to control query queue draining */
|
|
56
|
+
readyForQuery: boolean;
|
|
57
|
+
/** Type parser overrides (if the client has custom type parsers) */
|
|
58
|
+
_types?: unknown;
|
|
59
|
+
release(err?: Error | boolean): void;
|
|
60
|
+
}
|
|
61
|
+
export interface PipelineRunOptions {
|
|
62
|
+
/**
|
|
63
|
+
* Whether to wrap the pipeline in BEGIN/COMMIT (default: true).
|
|
64
|
+
* When true, all queries execute atomically. On error, ROLLBACK is sent.
|
|
65
|
+
* When false, each query gets its own Sync message for error isolation.
|
|
66
|
+
*/
|
|
67
|
+
transactional?: boolean;
|
|
68
|
+
/** Timeout in milliseconds. If exceeded, the connection is destroyed. */
|
|
69
|
+
timeout?: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Execute multiple queries using the Postgres extended-query pipeline protocol.
|
|
73
|
+
*
|
|
74
|
+
* All protocol messages are buffered into a single TCP write via cork/uncork.
|
|
75
|
+
* The backend processes them in order and sends back results which our state
|
|
76
|
+
* machine collects.
|
|
77
|
+
*
|
|
78
|
+
* @param client - A pg PoolClient with an accessible Connection
|
|
79
|
+
* @param queries - Array of DeferredQuery descriptors
|
|
80
|
+
* @param options - Pipeline options (transactional, timeout)
|
|
81
|
+
* @returns Array of transformed results in the same order as queries
|
|
82
|
+
*/
|
|
83
|
+
export declare function runPipelined<T extends readonly DeferredQuery<unknown>[]>(client: PgPoolClient, queries: T, options?: PipelineRunOptions): Promise<unknown[]>;
|
|
84
|
+
/**
|
|
85
|
+
* Check whether a pool client supports the extended-query pipeline protocol.
|
|
86
|
+
*
|
|
87
|
+
* Returns true if the client has a Connection object with the required wire
|
|
88
|
+
* protocol methods (parse, bind, describe, execute, sync) and is an EventEmitter.
|
|
89
|
+
*
|
|
90
|
+
* Returns false for HTTP-based drivers (Neon HTTP, Vercel Postgres), mock pools,
|
|
91
|
+
* and any pool that doesn't expose pg internals.
|
|
92
|
+
*/
|
|
93
|
+
export declare function supportsExtendedPipeline(poolClient: unknown): poolClient is PgPoolClient;
|
|
94
|
+
//# sourceMappingURL=pipeline-submittable.d.ts.map
|