turbine-orm 0.14.0 → 0.16.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.
Files changed (42) hide show
  1. package/dist/adapters/cockroachdb.js +1 -1
  2. package/dist/adapters/index.d.ts +7 -4
  3. package/dist/adapters/index.js +1 -1
  4. package/dist/adapters/yugabytedb.js +1 -1
  5. package/dist/cjs/adapters/cockroachdb.js +1 -1
  6. package/dist/cjs/adapters/index.js +1 -1
  7. package/dist/cjs/adapters/yugabytedb.js +1 -1
  8. package/dist/cjs/cli/index.js +64 -0
  9. package/dist/cjs/cli/observe-ui.js +182 -0
  10. package/dist/cjs/cli/observe.js +242 -0
  11. package/dist/cjs/cli/studio.js +45 -7
  12. package/dist/cjs/client.js +102 -1
  13. package/dist/cjs/errors.js +44 -1
  14. package/dist/cjs/generate.js +86 -0
  15. package/dist/cjs/index.js +10 -1
  16. package/dist/cjs/nested-write.js +557 -0
  17. package/dist/cjs/observe.js +145 -0
  18. package/dist/cjs/query/builder.js +271 -23
  19. package/dist/cli/index.d.ts +1 -0
  20. package/dist/cli/index.js +64 -0
  21. package/dist/cli/observe-ui.d.ts +2 -0
  22. package/dist/cli/observe-ui.js +180 -0
  23. package/dist/cli/observe.d.ts +20 -0
  24. package/dist/cli/observe.js +237 -0
  25. package/dist/cli/studio.d.ts +10 -2
  26. package/dist/cli/studio.js +45 -7
  27. package/dist/client.d.ts +32 -2
  28. package/dist/client.js +102 -2
  29. package/dist/errors.d.ts +23 -0
  30. package/dist/errors.js +41 -0
  31. package/dist/generate.js +86 -0
  32. package/dist/index.d.ts +5 -3
  33. package/dist/index.js +4 -2
  34. package/dist/nested-write.d.ts +95 -0
  35. package/dist/nested-write.js +551 -0
  36. package/dist/observe.d.ts +36 -0
  37. package/dist/observe.js +141 -0
  38. package/dist/query/builder.d.ts +45 -12
  39. package/dist/query/builder.js +239 -24
  40. package/dist/query/index.d.ts +2 -2
  41. package/dist/query/types.d.ts +76 -8
  42. package/package.json +2 -2
@@ -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,36 @@ 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");
33
+ const observe_js_1 = require("./observe.js");
32
34
  const pipeline_js_1 = require("./pipeline.js");
33
35
  const index_js_1 = require("./query/index.js");
36
+ async function withRetry(fn, options) {
37
+ const maxAttempts = options?.maxAttempts ?? 3;
38
+ const baseDelay = options?.baseDelay ?? 50;
39
+ const maxDelay = options?.maxDelay ?? 5000;
40
+ let lastError;
41
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
42
+ try {
43
+ return await fn();
44
+ }
45
+ catch (err) {
46
+ lastError = err;
47
+ const isRetryable = err &&
48
+ typeof err === 'object' &&
49
+ 'isRetryable' in err &&
50
+ err.isRetryable === true;
51
+ if (!isRetryable || attempt === maxAttempts - 1)
52
+ throw err;
53
+ options?.onRetry?.(err, attempt + 1);
54
+ const delay = Math.min(baseDelay * 2 ** attempt + Math.random() * baseDelay, maxDelay);
55
+ await new Promise((resolve) => setTimeout(resolve, delay));
56
+ }
57
+ }
58
+ throw lastError;
59
+ }
34
60
  /** Maps isolation level names to SQL */
35
61
  const ISOLATION_LEVELS = {
36
62
  ReadUncommitted: 'READ UNCOMMITTED',
@@ -79,7 +105,8 @@ class TransactionClient {
79
105
  // Create a QueryInterface that uses the transaction client as its "pool"
80
106
  // We use a proxy pool that routes queries through the transaction client
81
107
  const txPool = this.createTxPool();
82
- qi = new index_js_1.QueryInterface(txPool, name, this.schema, this.middlewares, this.queryOptions);
108
+ const txOpts = { ...this.queryOptions, _txScoped: true };
109
+ qi = new index_js_1.QueryInterface(txPool, name, this.schema, this.middlewares, txOpts);
83
110
  this.tableCache.set(name, qi);
84
111
  }
85
112
  return qi;
@@ -165,7 +192,9 @@ class TurbineClient {
165
192
  logging;
166
193
  tableCache = new Map();
167
194
  middlewares = [];
195
+ queryListeners = new Set();
168
196
  queryOptions;
197
+ errorMessagesSafe;
169
198
  /** True when Turbine created the pool and is responsible for tearing it down */
170
199
  ownsPool = true;
171
200
  constructor(config = {}, schema) {
@@ -199,12 +228,27 @@ class TurbineClient {
199
228
  this.schema = schema;
200
229
  // Respect env var kill switch
201
230
  const envDisablePrepared = typeof process !== 'undefined' && process.env?.TURBINE_DISABLE_PREPARED === '1';
231
+ this.errorMessagesSafe = (config.errorMessages ?? 'safe') === 'safe';
202
232
  this.queryOptions = {
203
233
  defaultLimit: config.defaultLimit,
204
234
  warnOnUnlimited: config.warnOnUnlimited,
205
235
  preparedStatements: envDisablePrepared ? false : (config.preparedStatements ?? !config.pool),
206
236
  sqlCache: config.sqlCache ?? true,
207
237
  dialect: config.dialect,
238
+ _onQuery: (event) => {
239
+ if (this.queryListeners.size === 0)
240
+ return;
241
+ const emitted = this.errorMessagesSafe ? { ...event, params: event.params.map(() => '[REDACTED]') } : event;
242
+ for (const listener of this.queryListeners) {
243
+ try {
244
+ listener(emitted);
245
+ }
246
+ catch (e) {
247
+ if (this.logging)
248
+ console.error('[turbine] Query listener error:', e);
249
+ }
250
+ }
251
+ },
208
252
  };
209
253
  // Apply NotFoundError message redaction mode (default: safe — values are
210
254
  // stripped from messages to avoid leaking PII into error logs).
@@ -257,6 +301,11 @@ class TurbineClient {
257
301
  });
258
302
  }
259
303
  }
304
+ // Auto-start observability from env var
305
+ const observeUrl = typeof process !== 'undefined' ? process.env?.TURBINE_OBSERVE_URL : undefined;
306
+ if (observeUrl) {
307
+ this.$observe({ connectionString: observeUrl }).catch(() => { });
308
+ }
260
309
  }
261
310
  // -------------------------------------------------------------------------
262
311
  // Middleware — intercept all queries
@@ -298,6 +347,37 @@ class TurbineClient {
298
347
  this.tableCache.clear();
299
348
  }
300
349
  // -------------------------------------------------------------------------
350
+ // Event emitter — subscribe to query lifecycle events
351
+ // -------------------------------------------------------------------------
352
+ $on(_event, listener) {
353
+ this.queryListeners.add(listener);
354
+ }
355
+ $off(_event, listener) {
356
+ this.queryListeners.delete(listener);
357
+ }
358
+ // -------------------------------------------------------------------------
359
+ // Observability — automatic metrics collection
360
+ // -------------------------------------------------------------------------
361
+ observeEngine;
362
+ async $observe(config) {
363
+ if (this.observeEngine) {
364
+ await this.observeEngine.stop();
365
+ this.$off('query', this.observeEngine.getListener());
366
+ }
367
+ const engine = new observe_js_1.ObserveEngine(config);
368
+ this.observeEngine = engine;
369
+ await engine.init();
370
+ this.$on('query', engine.getListener());
371
+ return {
372
+ stop: async () => {
373
+ this.$off('query', engine.getListener());
374
+ await engine.stop();
375
+ if (this.observeEngine === engine)
376
+ this.observeEngine = undefined;
377
+ },
378
+ };
379
+ }
380
+ // -------------------------------------------------------------------------
301
381
  // Table accessor — creates QueryInterface for any table
302
382
  // -------------------------------------------------------------------------
303
383
  /**
@@ -542,6 +622,27 @@ class TurbineClient {
542
622
  }
543
623
  }
544
624
  // -------------------------------------------------------------------------
625
+ // Retry — automatic retry for retryable errors (deadlock, serialization)
626
+ // -------------------------------------------------------------------------
627
+ /**
628
+ * Execute an async function with automatic retry on retryable errors.
629
+ *
630
+ * Only errors with `isRetryable === true` (DeadlockError, SerializationFailureError)
631
+ * are retried. Uses exponential backoff with jitter.
632
+ *
633
+ * @example
634
+ * ```ts
635
+ * const result = await db.$retry(() =>
636
+ * db.$transaction(async (tx) => {
637
+ * // ... serializable transaction logic
638
+ * }, { isolationLevel: 'Serializable' })
639
+ * );
640
+ * ```
641
+ */
642
+ async $retry(fn, options) {
643
+ return withRetry(fn, options);
644
+ }
645
+ // -------------------------------------------------------------------------
545
646
  // Connection lifecycle
546
647
  // -------------------------------------------------------------------------
547
648
  /**
@@ -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
  // ---------------------------------------------------------------------------
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; } });