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/errors.js CHANGED
@@ -20,6 +20,8 @@ export const TurbineErrorCode = {
20
20
  DEADLOCK_DETECTED: 'TURBINE_E012',
21
21
  SERIALIZATION_FAILURE: 'TURBINE_E013',
22
22
  PIPELINE: 'TURBINE_E014',
23
+ OPTIMISTIC_LOCK: 'TURBINE_E015',
24
+ EXCLUSION_VIOLATION: 'TURBINE_E016',
23
25
  };
24
26
  /** Base error class for all Turbine errors */
25
27
  export class TurbineError extends Error {
@@ -331,6 +333,25 @@ export class CheckConstraintError extends TurbineError {
331
333
  this.table = table;
332
334
  }
333
335
  }
336
+ export class ExclusionConstraintError extends TurbineError {
337
+ constraint;
338
+ table;
339
+ constructor(opts = {}) {
340
+ const { constraint, table, cause } = opts;
341
+ let message = opts.message;
342
+ if (!message) {
343
+ const constraintPart = constraint ? ` on ${constraint}` : '';
344
+ message = `[turbine] Exclusion constraint violation${constraintPart}`;
345
+ const detail = detailFromCause(cause);
346
+ if (detail)
347
+ message += `: ${detail}`;
348
+ }
349
+ super(TurbineErrorCode.EXCLUSION_VIOLATION, message, { cause });
350
+ this.name = 'ExclusionConstraintError';
351
+ this.constraint = constraint;
352
+ this.table = table;
353
+ }
354
+ }
334
355
  /**
335
356
  * Thrown when a non-transactional pipeline has partial failures.
336
357
  *
@@ -371,6 +392,19 @@ export class PipelineError extends TurbineError {
371
392
  this.failedTag = failedTag;
372
393
  }
373
394
  }
395
+ export class OptimisticLockError extends TurbineError {
396
+ table;
397
+ versionField;
398
+ expectedVersion;
399
+ constructor(opts) {
400
+ super(TurbineErrorCode.OPTIMISTIC_LOCK, `[turbine] Optimistic lock failed on "${opts.table}" — ` +
401
+ `expected ${opts.versionField} = ${opts.expectedVersion} but row was modified by another transaction`);
402
+ this.name = 'OptimisticLockError';
403
+ this.table = opts.table;
404
+ this.versionField = opts.versionField;
405
+ this.expectedVersion = opts.expectedVersion;
406
+ }
407
+ }
374
408
  /**
375
409
  * Parse column names out of a pg `detail` string like:
376
410
  * "Key (email)=(foo@bar) already exists."
@@ -391,6 +425,7 @@ function parseColumnsFromDetail(detail) {
391
425
  * 23503 (foreign_key_violation) -> ForeignKeyError
392
426
  * 23502 (not_null_violation) -> NotNullViolationError
393
427
  * 23514 (check_violation) -> CheckConstraintError
428
+ * 23P01 (exclusion_violation) -> ExclusionConstraintError
394
429
  * 40P01 (deadlock_detected) -> DeadlockError (retryable)
395
430
  * 40001 (serialization_failure) -> SerializationFailureError (retryable)
396
431
  *
@@ -430,6 +465,12 @@ export function wrapPgError(err) {
430
465
  table: e.table,
431
466
  cause: err,
432
467
  });
468
+ case '23P01':
469
+ return new ExclusionConstraintError({
470
+ constraint: e.constraint,
471
+ table: e.table,
472
+ cause: err,
473
+ });
433
474
  case '40P01':
434
475
  return new DeadlockError({
435
476
  constraint: e.constraint,
package/dist/generate.js CHANGED
@@ -177,6 +177,92 @@ export function generateTypes(schema) {
177
177
  }
178
178
  }
179
179
  }
180
+ // ---------------------------------------------------------------------------
181
+ // Nested write types (WhereUnique, NestedCreateInput, NestedUpdateInput,
182
+ // ConnectOrCreate, CreateInput, UpdateInput)
183
+ // ---------------------------------------------------------------------------
184
+ for (const table of Object.values(schema.tables)) {
185
+ const typeName = entityName(table.name);
186
+ const hasRels = Object.keys(table.relations).length > 0;
187
+ // WhereUnique — union of unique constraint shapes, deduplicating PK
188
+ const seen = new Set();
189
+ const uniqueSets = [];
190
+ // Always include the primary key first
191
+ const pkKey = table.primaryKey.join(',');
192
+ seen.add(pkKey);
193
+ uniqueSets.push(table.primaryKey);
194
+ // Add unique indexes that aren't duplicates of the PK
195
+ for (const uc of table.uniqueColumns) {
196
+ const ucKey = uc.join(',');
197
+ if (!seen.has(ucKey)) {
198
+ seen.add(ucKey);
199
+ uniqueSets.push(uc);
200
+ }
201
+ }
202
+ if (uniqueSets.length > 0) {
203
+ const branches = uniqueSets.map((cols) => {
204
+ const fields = cols.map((colName) => {
205
+ const col = table.columns.find((c) => c.name === colName);
206
+ const field = col?.field ?? colName;
207
+ const tsType = col?.tsType ?? 'unknown';
208
+ return `${field}: ${tsType}`;
209
+ });
210
+ return `{ ${fields.join('; ')} }`;
211
+ });
212
+ lines.push(`export type ${typeName}WhereUnique = ${branches.join(' | ')};`);
213
+ lines.push('');
214
+ }
215
+ // CreateInput / UpdateInput — extends base type with optional relation fields
216
+ if (hasRels) {
217
+ lines.push(`export type ${typeName}CreateInput = ${typeName}Create & {`);
218
+ for (const [relName, rel] of Object.entries(table.relations)) {
219
+ const targetType = entityName(rel.to);
220
+ lines.push(` ${relName}?: ${targetType}NestedCreateInput;`);
221
+ }
222
+ lines.push('};');
223
+ lines.push('');
224
+ lines.push(`export type ${typeName}UpdateInput = ${typeName}Update & {`);
225
+ for (const [relName, rel] of Object.entries(table.relations)) {
226
+ const targetType = entityName(rel.to);
227
+ if (rel.type === 'hasMany') {
228
+ lines.push(` ${relName}?: ${targetType}NestedUpdateInput;`);
229
+ }
230
+ else {
231
+ lines.push(` ${relName}?: ${targetType}NestedCreateInput;`);
232
+ }
233
+ }
234
+ lines.push('};');
235
+ lines.push('');
236
+ }
237
+ }
238
+ // Emit NestedCreateInput, NestedUpdateInput, ConnectOrCreate for every table
239
+ for (const table of Object.values(schema.tables)) {
240
+ const typeName = entityName(table.name);
241
+ const hasRels = Object.keys(table.relations).length > 0;
242
+ // NestedCreateInput uses *CreateInput (which includes relation fields) when
243
+ // the table has relations, otherwise falls back to the plain *Create type.
244
+ const createRefType = hasRels ? `${typeName}CreateInput` : `${typeName}Create`;
245
+ lines.push(`export interface ${typeName}NestedCreateInput {`);
246
+ lines.push(` create?: ${createRefType} | ${createRefType}[];`);
247
+ lines.push(` connect?: ${typeName}WhereUnique | ${typeName}WhereUnique[];`);
248
+ lines.push(` connectOrCreate?: ${typeName}ConnectOrCreate | ${typeName}ConnectOrCreate[];`);
249
+ lines.push('}');
250
+ lines.push('');
251
+ lines.push(`export interface ${typeName}NestedUpdateInput {`);
252
+ lines.push(` create?: ${createRefType} | ${createRefType}[];`);
253
+ lines.push(` connect?: ${typeName}WhereUnique | ${typeName}WhereUnique[];`);
254
+ lines.push(` connectOrCreate?: ${typeName}ConnectOrCreate | ${typeName}ConnectOrCreate[];`);
255
+ lines.push(` disconnect?: ${typeName}WhereUnique | ${typeName}WhereUnique[];`);
256
+ lines.push(` set?: ${typeName}WhereUnique[];`);
257
+ lines.push(` delete?: ${typeName}WhereUnique | ${typeName}WhereUnique[];`);
258
+ lines.push('}');
259
+ lines.push('');
260
+ lines.push(`export interface ${typeName}ConnectOrCreate {`);
261
+ lines.push(` where: ${typeName}WhereUnique;`);
262
+ lines.push(` create: ${createRefType};`);
263
+ lines.push('}');
264
+ lines.push('');
265
+ }
180
266
  return lines.join('\n');
181
267
  }
182
268
  // ---------------------------------------------------------------------------
package/dist/index.d.ts CHANGED
@@ -34,14 +34,15 @@
34
34
  */
35
35
  export type { DatabaseAdapter, IntrospectionOverrides } from './adapters/index.js';
36
36
  export { alloydb, cockroachdb, postgresql, timescale, yugabytedb } from './adapters/index.js';
37
- export { type Middleware, type MiddlewareNext, type MiddlewareParams, type PgCompatPool, type PgCompatPoolClient, type PgCompatQueryResult, TransactionClient, type TransactionOptions, TurbineClient, type TurbineConfig, } from './client.js';
37
+ export { type Middleware, type MiddlewareNext, type MiddlewareParams, type PgCompatPool, type PgCompatPoolClient, type PgCompatQueryResult, type RetryOptions, TransactionClient, type TransactionOptions, TurbineClient, type TurbineConfig, withRetry, } from './client.js';
38
38
  export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, DialectIntrospector, DialectMigrator, DialectName, InsertStatementInput, IntrospectOptions as DialectIntrospectOptions, UpsertStatementInput, } from './dialect.js';
39
39
  export { postgresDialect } from './dialect.js';
40
- 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';
40
+ export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, type ErrorMessageMode, ExclusionConstraintError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, OptimisticLockError, PipelineError, type PipelineResultSlot, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, ValidationError, wrapPgError, } from './errors.js';
41
41
  export { type GenerateOptions, generate } from './generate.js';
42
42
  export { type IntrospectOptions, introspect } from './introspect.js';
43
+ export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type NestedWriteContext, } from './nested-write.js';
43
44
  export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
44
- 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/index.js';
45
+ export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GroupByArgs, type JsonFilter, type NestedCreateOp, type NestedUpdateOp, type OmitResult, type OrderDirection, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type SelectResult, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
45
46
  export type { ColumnMetadata, IndexMetadata, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
46
47
  export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
47
48
  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
@@ -34,14 +34,16 @@
34
34
  */
35
35
  export { alloydb, cockroachdb, postgresql, timescale, yugabytedb } from './adapters/index.js';
36
36
  // Client
37
- export { TransactionClient, TurbineClient, } from './client.js';
37
+ export { TransactionClient, TurbineClient, withRetry, } from './client.js';
38
38
  export { postgresDialect } from './dialect.js';
39
39
  // Error types
40
- export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, PipelineError, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, ValidationError, wrapPgError, } from './errors.js';
40
+ export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, ExclusionConstraintError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, OptimisticLockError, PipelineError, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, ValidationError, wrapPgError, } from './errors.js';
41
41
  // Code generation
42
42
  export { generate } from './generate.js';
43
43
  // Introspection
44
44
  export { introspect } from './introspect.js';
45
+ // Nested writes
46
+ export { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from './nested-write.js';
45
47
  // Pipeline
46
48
  export { executePipeline, pipelineSupported } from './pipeline.js';
47
49
  // Query builder
@@ -0,0 +1,95 @@
1
+ /**
2
+ * turbine-orm — Nested write engine
3
+ *
4
+ * Tree-walking create/update that resolves relation fields in `data` into
5
+ * batched SQL operations within a transaction. Supports create, connect,
6
+ * connectOrCreate, disconnect, set, and delete on related records at
7
+ * arbitrary depth (capped at 10).
8
+ *
9
+ * This module is imported by `query/builder.ts` when the `data` argument
10
+ * of `create()` or `update()` contains relation fields. It never imports
11
+ * `client.ts` directly — the transaction handle is passed in via
12
+ * `NestedWriteContext`.
13
+ */
14
+ import type { RelationDef, SchemaMetadata, TableMetadata } from './schema.js';
15
+ export interface ExtractedFields {
16
+ scalars: Record<string, unknown>;
17
+ relations: Record<string, Record<string, unknown>>;
18
+ }
19
+ /**
20
+ * Transaction context for nested write operations.
21
+ * Matches the subset of TransactionClient that we actually use.
22
+ */
23
+ export interface NestedWriteContext {
24
+ schema: SchemaMetadata;
25
+ tx: {
26
+ table<T extends object>(name: string): {
27
+ create(args: {
28
+ data: Partial<T>;
29
+ }): Promise<T>;
30
+ createMany(args: {
31
+ data: Partial<T>[];
32
+ }): Promise<T[]>;
33
+ update(args: {
34
+ where: Record<string, unknown>;
35
+ data: Record<string, unknown>;
36
+ }): Promise<T>;
37
+ updateMany(args: {
38
+ where: Record<string, unknown>;
39
+ data: Record<string, unknown>;
40
+ allowFullTableScan?: boolean;
41
+ }): Promise<{
42
+ count: number;
43
+ }>;
44
+ delete(args: {
45
+ where: Record<string, unknown>;
46
+ }): Promise<T>;
47
+ deleteMany(args: {
48
+ where: Record<string, unknown>;
49
+ }): Promise<{
50
+ count: number;
51
+ }>;
52
+ findMany(args: {
53
+ where: Record<string, unknown>;
54
+ }): Promise<T[]>;
55
+ findUnique(args: {
56
+ where: Record<string, unknown>;
57
+ with?: Record<string, unknown>;
58
+ }): Promise<T | null>;
59
+ };
60
+ };
61
+ }
62
+ /**
63
+ * Separates scalar data fields from relation operation fields.
64
+ *
65
+ * A key is treated as a relation field only when:
66
+ * 1. It matches a relation name in `tableMeta.relations`
67
+ * 2. Its value is a non-null, non-array, non-Date plain object
68
+ *
69
+ * Everything else goes into `scalars`.
70
+ */
71
+ export declare function extractRelationFields(data: Record<string, unknown>, tableMeta: TableMetadata): ExtractedFields;
72
+ /**
73
+ * Quick check: does `data` contain any relation fields that would trigger
74
+ * the nested write path? Used by QueryInterface to decide whether to
75
+ * delegate to the nested write engine or take the fast scalar-only path.
76
+ */
77
+ export declare function hasRelationFields(data: Record<string, unknown>, tableMeta: TableMetadata): boolean;
78
+ /**
79
+ * Inject the parent row's PK value(s) as FK field(s) into child data.
80
+ * Handles composite keys. Returns a new object (does not mutate input).
81
+ */
82
+ export declare function injectForeignKey(childData: Record<string, unknown>, relation: RelationDef, parentRow: Record<string, unknown>, schema: SchemaMetadata): Record<string, unknown>;
83
+ /**
84
+ * Tree-walking create: inserts the parent row, then processes each relation
85
+ * operation (create, connect, connectOrCreate), and finally reads back the
86
+ * full tree using `findUnique` with an auto-built `with` clause.
87
+ */
88
+ export declare function executeNestedCreate(ctx: NestedWriteContext, tableName: string, data: Record<string, unknown>, depth?: number, path?: string[]): Promise<Record<string, unknown>>;
89
+ /**
90
+ * Tree-walking update: updates the parent row with scalar data, then
91
+ * processes each relation operation (create, connect, connectOrCreate,
92
+ * disconnect, set, delete), and reads back the full tree.
93
+ */
94
+ export declare function executeNestedUpdate(ctx: NestedWriteContext, tableName: string, where: Record<string, unknown>, data: Record<string, unknown>, depth?: number, path?: string[]): Promise<Record<string, unknown>>;
95
+ //# sourceMappingURL=nested-write.d.ts.map