turbine-orm 0.13.3 → 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.
@@ -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 = '${seconds}s'`;
169
+ return { sql: `SET transaction_timeout = $1`, params: [`${seconds}s`] };
170
170
  },
171
171
  };
172
172
  //# sourceMappingURL=cockroachdb.js.map
@@ -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 = '<n>s'`.
59
- * CockroachDB uses `SET transaction_timeout = '<n>s'` (v23.1+).
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 string to execute
62
+ * @returns an object with the parameterized SQL and its bound values
63
63
  */
64
- statementTimeout?(seconds: number): string;
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.
@@ -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 = '${seconds}s'`;
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 = '${seconds}s'`;
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 = '${seconds}s'`;
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 = '${seconds}s'`;
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 = '${seconds}s'`;
156
+ return { sql: `SET LOCAL statement_timeout = $1`, params: [`${seconds}s`] };
157
157
  },
158
158
  };
@@ -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 statementTimeoutSQL = options.adapter?.statementTimeout?.(30) ?? `SET LOCAL statement_timeout = '30s'`;
78
- const ctx = { pool, metadata, options, authToken, stateDir, statementTimeoutSQL };
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.statementTimeoutSQL);
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.statementTimeoutSQL);
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.statementTimeoutSQL);
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
  }
@@ -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
- qi = new index_js_1.QueryInterface(txPool, name, this.schema, this.middlewares, this.queryOptions);
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
  /**
@@ -9,6 +9,7 @@
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.postgresDialect = void 0;
11
11
  const errors_js_1 = require("./errors.js");
12
+ const schema_js_1 = require("./schema.js");
12
13
  /** PostgreSQL implementation of the dialect contract. */
13
14
  exports.postgresDialect = {
14
15
  name: 'postgresql',
@@ -72,10 +73,11 @@ exports.postgresDialect = {
72
73
  .map((col, i) => `${leftRef}.${this.quoteIdentifier(col)} = ${rightRef}.${this.quoteIdentifier(rightCols[i])}`)
73
74
  .join(' AND ');
74
75
  },
75
- typeToTypeScript(_dialectType, _nullable) {
76
- // Existing PostgreSQL type mapping remains in schema.ts/generate.ts for now.
77
- // This hook is the package boundary MySQL/SQLite implementations will fill.
78
- return 'unknown';
76
+ typeToTypeScript(dialectType, nullable) {
77
+ return (0, schema_js_1.pgTypeToTs)(dialectType, nullable);
78
+ },
79
+ arrayType(baseType) {
80
+ return (0, schema_js_1.pgArrayType)(baseType);
79
81
  },
80
82
  buildColumnType(input) {
81
83
  if (input.type === 'VARCHAR' && input.maxLength != null) {
@@ -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,
@@ -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
  // ---------------------------------------------------------------------------
@@ -218,7 +304,13 @@ function generateMetadata(schema) {
218
304
  // dateColumns
219
305
  const dateCols = [...table.dateColumns];
220
306
  lines.push(` dateColumns: new Set([${dateCols.map((c) => `'${escSQ(c)}'`).join(', ')}]),`);
221
- // pgTypes
307
+ // dialectTypes + pgTypes (pgTypes is kept for backwards compatibility)
308
+ const dialectTypes = table.dialectTypes ?? table.pgTypes;
309
+ lines.push(' dialectTypes: {');
310
+ for (const [col, dialectType] of Object.entries(dialectTypes)) {
311
+ lines.push(` ${quoteIfNeeded(col)}: '${escSQ(dialectType)}',`);
312
+ }
313
+ lines.push(' },');
222
314
  lines.push(' pgTypes: {');
223
315
  for (const [col, pgType] of Object.entries(table.pgTypes)) {
224
316
  lines.push(` ${quoteIfNeeded(col)}: '${escSQ(pgType)}',`);
@@ -392,11 +484,13 @@ function serializeColumn(col) {
392
484
  const parts = [
393
485
  `name: '${escSQ(col.name)}'`,
394
486
  `field: '${escSQ(col.field)}'`,
487
+ `dialectType: '${escSQ(col.dialectType ?? col.pgType)}'`,
395
488
  `pgType: '${escSQ(col.pgType)}'`,
396
489
  `tsType: '${escSQ(col.tsType)}'`,
397
490
  `nullable: ${col.nullable}`,
398
491
  `hasDefault: ${col.hasDefault}`,
399
492
  `isArray: ${col.isArray}`,
493
+ `arrayType: '${escSQ(col.arrayType ?? col.pgArrayType)}'`,
400
494
  `pgArrayType: '${escSQ(col.pgArrayType)}'`,
401
495
  ];
402
496
  if (col.maxLength !== undefined)
package/dist/cjs/index.js CHANGED
@@ -34,7 +34,8 @@
34
34
  * ```
35
35
  */
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.turbineHttp = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = 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.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.PipelineError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.postgresDialect = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
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; } });
@@ -14,6 +14,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.introspect = introspect;
16
16
  const pg_1 = __importDefault(require("pg"));
17
+ const dialect_js_1 = require("./dialect.js");
17
18
  const schema_js_1 = require("./schema.js");
18
19
  // ---------------------------------------------------------------------------
19
20
  // SQL queries (all parameterized, no interpolation)
@@ -101,6 +102,7 @@ const SQL_ENUMS = `
101
102
  // ---------------------------------------------------------------------------
102
103
  async function introspect(options) {
103
104
  const schema = options.schema ?? 'public';
105
+ const dialect = dialect_js_1.postgresDialect;
104
106
  const pool = new pg_1.default.Pool({
105
107
  connectionString: options.connectionString,
106
108
  max: 1,
@@ -137,15 +139,20 @@ async function introspect(options) {
137
139
  const isNullable = row.is_nullable === 'YES';
138
140
  const isArray = row.data_type === 'ARRAY';
139
141
  const baseType = isArray ? row.udt_name.slice(1) : row.udt_name;
142
+ const dialectType = row.udt_name;
143
+ const arrayType = dialect.arrayType?.(baseType) ?? 'text[]';
140
144
  const col = {
141
145
  name: row.column_name,
142
146
  field: (0, schema_js_1.snakeToCamel)(row.column_name),
143
- pgType: row.udt_name,
144
- tsType: (0, schema_js_1.pgTypeToTs)(isArray ? row.udt_name : baseType, isNullable),
147
+ dialectType,
148
+ pgType: dialectType,
149
+ tsType: dialect.typeToTypeScript?.(isArray ? dialectType : baseType, isNullable) ??
150
+ (0, schema_js_1.pgTypeToTs)(isArray ? dialectType : baseType, isNullable),
145
151
  nullable: isNullable,
146
152
  hasDefault: row.column_default !== null,
147
153
  isArray,
148
- pgArrayType: (0, schema_js_1.pgArrayType)(baseType),
154
+ arrayType,
155
+ pgArrayType: arrayType,
149
156
  maxLength: row.character_maximum_length ?? undefined,
150
157
  };
151
158
  if (!columnsByTable.has(tableName))
@@ -281,14 +288,16 @@ async function introspect(options) {
281
288
  const columnMap = {};
282
289
  const reverseColumnMap = {};
283
290
  const dateColumns = new Set();
291
+ const dialectTypes = {};
284
292
  const pgTypes = {};
285
293
  const allColumns = [];
286
294
  for (const col of columns) {
287
295
  columnMap[col.field] = col.name;
288
296
  reverseColumnMap[col.name] = col.field;
289
297
  allColumns.push(col.name);
298
+ dialectTypes[col.name] = col.dialectType ?? col.pgType;
290
299
  pgTypes[col.name] = col.pgType;
291
- const baseType = col.isArray ? col.pgType.slice(1) : col.pgType;
300
+ const baseType = col.isArray ? (col.dialectType ?? col.pgType).slice(1) : (col.dialectType ?? col.pgType);
292
301
  if ((0, schema_js_1.isDateType)(baseType)) {
293
302
  dateColumns.add(col.name);
294
303
  }
@@ -299,6 +308,7 @@ async function introspect(options) {
299
308
  columnMap,
300
309
  reverseColumnMap,
301
310
  dateColumns,
311
+ dialectTypes,
302
312
  pgTypes,
303
313
  allColumns,
304
314
  primaryKey: pkByTable.get(tableName) ?? [],