turbine-orm 0.23.1 → 0.24.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 (80) hide show
  1. package/README.md +4 -1
  2. package/dist/adapters/cockroachdb.d.ts +0 -1
  3. package/dist/adapters/cockroachdb.js +0 -1
  4. package/dist/adapters/index.d.ts +0 -1
  5. package/dist/adapters/index.js +0 -1
  6. package/dist/adapters/yugabytedb.d.ts +0 -1
  7. package/dist/adapters/yugabytedb.js +0 -1
  8. package/dist/cjs/cli/config.js +14 -0
  9. package/dist/cjs/cli/index.js +60 -0
  10. package/dist/cjs/schema-builder.js +29 -1
  11. package/dist/cjs/schema-sql.js +19 -5
  12. package/dist/cli/config.d.ts +14 -1
  13. package/dist/cli/config.js +13 -1
  14. package/dist/cli/index.d.ts +0 -1
  15. package/dist/cli/index.js +61 -2
  16. package/dist/cli/loader.d.ts +0 -1
  17. package/dist/cli/loader.js +0 -1
  18. package/dist/cli/migrate.d.ts +0 -1
  19. package/dist/cli/migrate.js +0 -1
  20. package/dist/cli/observe-ui.d.ts +0 -1
  21. package/dist/cli/observe-ui.js +0 -1
  22. package/dist/cli/observe.d.ts +0 -1
  23. package/dist/cli/observe.js +0 -1
  24. package/dist/cli/studio-ui.generated.d.ts +0 -1
  25. package/dist/cli/studio-ui.generated.js +0 -1
  26. package/dist/cli/studio.d.ts +0 -1
  27. package/dist/cli/studio.js +0 -1
  28. package/dist/cli/ui.d.ts +0 -1
  29. package/dist/cli/ui.js +0 -1
  30. package/dist/client.d.ts +0 -1
  31. package/dist/client.js +0 -1
  32. package/dist/dialect.d.ts +0 -1
  33. package/dist/dialect.js +0 -1
  34. package/dist/errors.d.ts +0 -1
  35. package/dist/errors.js +0 -1
  36. package/dist/generate.d.ts +0 -1
  37. package/dist/generate.js +0 -1
  38. package/dist/index.d.ts +0 -1
  39. package/dist/index.js +0 -1
  40. package/dist/introspect.d.ts +0 -1
  41. package/dist/introspect.js +0 -1
  42. package/dist/mssql.d.ts +0 -1
  43. package/dist/mssql.js +0 -1
  44. package/dist/mysql.d.ts +0 -1
  45. package/dist/mysql.js +0 -1
  46. package/dist/nested-write.d.ts +0 -1
  47. package/dist/nested-write.js +0 -1
  48. package/dist/observe.d.ts +0 -1
  49. package/dist/observe.js +0 -1
  50. package/dist/pipeline-submittable.d.ts +0 -1
  51. package/dist/pipeline-submittable.js +0 -1
  52. package/dist/pipeline.d.ts +0 -1
  53. package/dist/pipeline.js +0 -1
  54. package/dist/powdb.d.ts +0 -1
  55. package/dist/powdb.js +0 -1
  56. package/dist/powql.d.ts +0 -1
  57. package/dist/powql.js +0 -1
  58. package/dist/query/builder.d.ts +0 -1
  59. package/dist/query/builder.js +0 -1
  60. package/dist/query/index.d.ts +0 -1
  61. package/dist/query/index.js +0 -1
  62. package/dist/query/types.d.ts +0 -1
  63. package/dist/query/types.js +0 -1
  64. package/dist/query/utils.d.ts +0 -1
  65. package/dist/query/utils.js +0 -1
  66. package/dist/realtime.d.ts +0 -1
  67. package/dist/realtime.js +0 -1
  68. package/dist/schema-builder.d.ts +6 -4
  69. package/dist/schema-builder.js +29 -2
  70. package/dist/schema-sql.d.ts +0 -1
  71. package/dist/schema-sql.js +19 -6
  72. package/dist/schema.d.ts +0 -1
  73. package/dist/schema.js +0 -1
  74. package/dist/serverless.d.ts +0 -1
  75. package/dist/serverless.js +0 -1
  76. package/dist/sqlite.d.ts +0 -1
  77. package/dist/sqlite.js +0 -1
  78. package/dist/typed-sql.d.ts +0 -1
  79. package/dist/typed-sql.js +0 -1
  80. package/package.json +4 -4
package/README.md CHANGED
@@ -51,8 +51,9 @@ Performance is at parity with Prisma and Drizzle — the real reasons to choose
51
51
  ## Quick Start
52
52
 
53
53
  ```bash
54
- # 1. Install
54
+ # 1. Install (the CLI also needs tsx to load .ts config/schema files)
55
55
  npm install turbine-orm
56
+ npm install --save-dev tsx
56
57
 
57
58
  # 2. Initialize project
58
59
  npx turbine init --url postgres://user:pass@localhost:5432/mydb
@@ -61,6 +62,8 @@ npx turbine init --url postgres://user:pass@localhost:5432/mydb
61
62
  npx turbine generate
62
63
  ```
63
64
 
65
+ > **CLI prerequisites.** The `turbine` CLI loads your `turbine.config.ts` / `turbine/schema.ts` directly, so a fresh project needs (1) `tsx` installed — otherwise `.ts` config loading fails with *"Loading .ts config / schema files requires tsx to be installed"* — and (2) `"type": "module"` in `package.json`, since Turbine is ESM. Without it you'll hit `Error [ERR_REQUIRE_ESM]: Cannot require() ES Module`. `create-next-app` sets neither by default. See [USING-TURBINE-ORM.md §0](docs/USING-TURBINE-ORM.md) for details.
66
+
64
67
  Works with both ESM and CommonJS:
65
68
 
66
69
  ```typescript
@@ -37,4 +37,3 @@
37
37
  */
38
38
  import type { DatabaseAdapter } from './index.js';
39
39
  export declare const cockroachdb: DatabaseAdapter;
40
- //# sourceMappingURL=cockroachdb.d.ts.map
@@ -166,4 +166,3 @@ export const cockroachdb = {
166
166
  return { sql: `SELECT set_config('transaction_timeout', $1, true)`, params: [`${seconds}s`] };
167
167
  },
168
168
  };
169
- //# sourceMappingURL=cockroachdb.js.map
@@ -108,4 +108,3 @@ export declare const alloydb: DatabaseAdapter;
108
108
  export declare const timescale: DatabaseAdapter;
109
109
  export { cockroachdb } from './cockroachdb.js';
110
110
  export { yugabytedb } from './yugabytedb.js';
111
- //# sourceMappingURL=index.d.ts.map
@@ -83,4 +83,3 @@ export const timescale = {
83
83
  // ---------------------------------------------------------------------------
84
84
  export { cockroachdb } from './cockroachdb.js';
85
85
  export { yugabytedb } from './yugabytedb.js';
86
- //# sourceMappingURL=index.js.map
@@ -49,4 +49,3 @@
49
49
  */
50
50
  import type { DatabaseAdapter } from './index.js';
51
51
  export declare const yugabytedb: DatabaseAdapter;
52
- //# sourceMappingURL=yugabytedb.d.ts.map
@@ -155,4 +155,3 @@ export const yugabytedb = {
155
155
  return { sql: `SELECT set_config('statement_timeout', $1, true)`, params: [`${seconds}s`] };
156
156
  },
157
157
  };
158
- //# sourceMappingURL=yugabytedb.js.map
@@ -39,6 +39,7 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  };
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.looksLikeSchemaFilePath = looksLikeSchemaFilePath;
42
43
  exports.loadConfig = loadConfig;
43
44
  exports.findConfigFile = findConfigFile;
44
45
  exports.resolveConfig = resolveConfig;
@@ -46,6 +47,19 @@ exports.configTemplate = configTemplate;
46
47
  const node_fs_1 = require("node:fs");
47
48
  const node_path_1 = require("node:path");
48
49
  const node_url_1 = require("node:url");
50
+ /**
51
+ * Heuristic: does a configured `schema` value actually look like a schema FILE
52
+ * path rather than a Postgres schema name? `schema` is the Postgres namespace
53
+ * to introspect (default `public`); the schema-builder file goes in `schemaFile`.
54
+ * A value containing a path separator or a JS/TS extension is almost certainly a
55
+ * mis-set `schemaFile` — introspecting `WHERE table_schema = './turbine/schema.ts'`
56
+ * silently matches zero tables. Used by `turbine generate` to fail loudly.
57
+ */
58
+ function looksLikeSchemaFilePath(schema) {
59
+ if (!schema)
60
+ return false;
61
+ return schema.includes('/') || schema.includes('\\') || /\.(ts|mts|cts|js|mjs|cjs|json)$/i.test(schema);
62
+ }
49
63
  // ---------------------------------------------------------------------------
50
64
  // Config file names, in priority order
51
65
  // ---------------------------------------------------------------------------
@@ -120,6 +120,9 @@ function parseArgs() {
120
120
  case '--allow-drift':
121
121
  result.allowDrift = true;
122
122
  break;
123
+ case '--allow-empty':
124
+ result.allowEmpty = true;
125
+ break;
123
126
  case '--force':
124
127
  case '-f':
125
128
  result.force = true;
@@ -242,10 +245,28 @@ async function loadSchemaFile(schemaFile) {
242
245
  (0, ui_js_1.newline)();
243
246
  console.log(` ${(0, ui_js_1.dim)('Hint: install')} ${(0, ui_js_1.cyan)('tsx')} ${(0, ui_js_1.dim)('to load .ts files:')} ${(0, ui_js_1.cyan)('npm install --save-dev tsx')}`);
244
247
  }
248
+ printCjsHintIfApplicable(err);
245
249
  }
246
250
  process.exit(1);
247
251
  }
248
252
  }
253
+ /**
254
+ * When a config/schema import blows up with the CommonJS-vs-ESM interop error
255
+ * (`Cannot require() ES Module` / `ERR_REQUIRE_ESM`), the root cause is almost
256
+ * always a project whose `package.json` lacks `"type": "module"`: tsx transpiles
257
+ * the `.ts` file to CJS and then can't `require()` Turbine's ESM build. Point the
258
+ * user at the one-line fix instead of leaving them with a raw Node stack trace.
259
+ */
260
+ function printCjsHintIfApplicable(err) {
261
+ const msg = err.message;
262
+ if (msg.includes('ERR_REQUIRE_ESM') ||
263
+ msg.includes('require() of ES Module') ||
264
+ msg.includes('Cannot require() ES Module')) {
265
+ (0, ui_js_1.newline)();
266
+ console.log(` ${(0, ui_js_1.dim)('Hint: add')} ${(0, ui_js_1.cyan)('"type": "module"')} ${(0, ui_js_1.dim)('to your')} ${(0, ui_js_1.cyan)('package.json')}${(0, ui_js_1.dim)('.')}`);
267
+ console.log(` ${(0, ui_js_1.dim)('Turbine is an ESM package; without it, Node/tsx tries to')} ${(0, ui_js_1.cyan)('require()')} ${(0, ui_js_1.dim)('it and fails.')}`);
268
+ }
269
+ }
249
270
  // ---------------------------------------------------------------------------
250
271
  // Command: init
251
272
  // ---------------------------------------------------------------------------
@@ -443,6 +464,24 @@ async function cmdGenerate(args, config) {
443
464
  (0, ui_js_1.banner)();
444
465
  const url = requireUrl(config);
445
466
  const startTime = performance.now();
467
+ // Guard: `schema` is the Postgres NAMESPACE to introspect (default `public`),
468
+ // NOT the path to your schema-builder file — that goes in `schemaFile`. If the
469
+ // configured `schema` looks like a file path, introspection would silently
470
+ // match zero tables and emit an empty client. Fail loudly instead.
471
+ if (!args.allowEmpty && (0, config_js_1.looksLikeSchemaFilePath)(config.schema)) {
472
+ (0, ui_js_1.error)(`The "schema" option looks like a file path: ${(0, ui_js_1.cyan)(config.schema)}`);
473
+ (0, ui_js_1.newline)();
474
+ console.log(` ${(0, ui_js_1.dim)('"schema" is the Postgres schema NAME to introspect')} ${(0, ui_js_1.dim)('(default:')} ${(0, ui_js_1.cyan)('public')}${(0, ui_js_1.dim)(').')}`);
475
+ console.log(` ${(0, ui_js_1.dim)('The path to your defineSchema() file belongs in')} ${(0, ui_js_1.cyan)('schemaFile')}${(0, ui_js_1.dim)('.')}`);
476
+ (0, ui_js_1.newline)();
477
+ console.log(` ${(0, ui_js_1.dim)('Fix your')} ${(0, ui_js_1.cyan)('turbine.config.ts')}${(0, ui_js_1.dim)(':')}`);
478
+ console.log(` ${(0, ui_js_1.green)('schema:')} ${(0, ui_js_1.cyan)("'public'")}${(0, ui_js_1.dim)(", // or omit — introspects the 'public' schema")}`);
479
+ console.log(` ${(0, ui_js_1.green)('schemaFile:')} ${(0, ui_js_1.cyan)(`'${config.schema}'`)}${(0, ui_js_1.dim)(', // your defineSchema() file (used by `turbine push`)')}`);
480
+ (0, ui_js_1.newline)();
481
+ console.log(` ${(0, ui_js_1.dim)('Re-run with')} ${(0, ui_js_1.cyan)('--allow-empty')} ${(0, ui_js_1.dim)('to introspect this literal schema name anyway.')}`);
482
+ (0, ui_js_1.newline)();
483
+ process.exit(1);
484
+ }
446
485
  (0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
447
486
  (0, ui_js_1.label)('Schema', config.schema);
448
487
  (0, ui_js_1.label)('Output', config.out);
@@ -459,6 +498,24 @@ async function cmdGenerate(args, config) {
459
498
  const totalColumns = Object.values(schema.tables).reduce((sum, t) => sum + t.columns.length, 0);
460
499
  const totalRelations = Object.values(schema.tables).reduce((sum, t) => sum + Object.keys(t.relations).length, 0);
461
500
  spinner.succeed(`Found ${(0, ui_js_1.bold)(String(tableNames.length))} tables, ${(0, ui_js_1.bold)(String(totalColumns))} columns, ${(0, ui_js_1.bold)(String(totalRelations))} relations`);
501
+ // Guard: zero tables means the generated client would be empty. That is almost
502
+ // always a misconfiguration (wrong `schema`, an include/exclude that filtered
503
+ // everything, or a database with no tables yet) rather than intent. Fail loudly
504
+ // instead of silently emitting an empty typed client.
505
+ if (tableNames.length === 0 && !args.allowEmpty) {
506
+ (0, ui_js_1.newline)();
507
+ (0, ui_js_1.error)(`Introspection matched 0 tables in schema ${(0, ui_js_1.cyan)(config.schema)} — refusing to generate an empty client.`);
508
+ (0, ui_js_1.newline)();
509
+ console.log(` ${(0, ui_js_1.dim)('Common causes:')}`);
510
+ console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('schema')} ${(0, ui_js_1.dim)('points at the wrong Postgres namespace')} ${(0, ui_js_1.dim)('(it is the schema NAME, default')} ${(0, ui_js_1.cyan)('public')}${(0, ui_js_1.dim)(').')}`);
511
+ console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.dim)('You meant to set')} ${(0, ui_js_1.cyan)('schemaFile')} ${(0, ui_js_1.dim)('(your defineSchema() file), not')} ${(0, ui_js_1.cyan)('schema')}${(0, ui_js_1.dim)('.')}`);
512
+ console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('include')}/${(0, ui_js_1.cyan)('exclude')} ${(0, ui_js_1.dim)('filtered out every table.')}`);
513
+ console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.dim)('The database has no tables yet — run')} ${(0, ui_js_1.cyan)('turbine push')} ${(0, ui_js_1.dim)('or a migration first.')}`);
514
+ (0, ui_js_1.newline)();
515
+ console.log(` ${(0, ui_js_1.dim)('If an empty client is genuinely what you want, re-run with')} ${(0, ui_js_1.cyan)('--allow-empty')}${(0, ui_js_1.dim)('.')}`);
516
+ (0, ui_js_1.newline)();
517
+ process.exit(1);
518
+ }
462
519
  // Print table summary
463
520
  if (args.verbose) {
464
521
  (0, ui_js_1.newline)();
@@ -1134,6 +1191,7 @@ function showGenerateHelp() {
1134
1191
  console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema ${(0, ui_js_1.dim)('(default: public)')}`);
1135
1192
  console.log(` ${(0, ui_js_1.cyan)('--include')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to include`);
1136
1193
  console.log(` ${(0, ui_js_1.cyan)('--exclude')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to exclude`);
1194
+ console.log(` ${(0, ui_js_1.cyan)('--allow-empty')} Generate even when introspection matches 0 tables`);
1137
1195
  (0, ui_js_1.newline)();
1138
1196
  }
1139
1197
  function showPushHelp() {
@@ -1342,6 +1400,8 @@ async function main() {
1342
1400
  catch (err) {
1343
1401
  if (args.command !== 'init') {
1344
1402
  (0, ui_js_1.warn)(`Could not load config: ${err instanceof Error ? err.message : String(err)}`);
1403
+ if (err instanceof Error)
1404
+ printCjsHintIfApplicable(err);
1345
1405
  }
1346
1406
  }
1347
1407
  const overrides = {
@@ -30,16 +30,28 @@ exports.table = table;
30
30
  exports.applyManyToManyRelations = applyManyToManyRelations;
31
31
  /** Maps shorthand names to actual Postgres type strings */
32
32
  const TYPE_MAP = {
33
- serial: 'BIGSERIAL',
33
+ // `serial` maps to SERIAL (int4). Its values fit in a JS `number` and pg
34
+ // returns them as numbers — so the generated `number` type is accurate.
35
+ // For 64-bit auto-increment keys use `bigserial` (int8), noting that values
36
+ // above Number.MAX_SAFE_INTEGER read back as string. (Changed in 0.24.0;
37
+ // `serial` previously emitted BIGSERIAL.)
38
+ serial: 'SERIAL',
39
+ bigserial: 'BIGSERIAL',
34
40
  bigint: 'BIGINT',
35
41
  integer: 'INTEGER',
36
42
  smallint: 'SMALLINT',
37
43
  text: 'TEXT',
38
44
  varchar: 'VARCHAR',
39
45
  boolean: 'BOOLEAN',
46
+ // `timestamp` is an honest alias for TIMESTAMPTZ (timezone-aware) — Turbine
47
+ // has always emitted TIMESTAMPTZ for it. `timestamptz` is the explicit spelling.
40
48
  timestamp: 'TIMESTAMPTZ',
49
+ timestamptz: 'TIMESTAMPTZ',
41
50
  date: 'DATE',
51
+ // `json` is an alias for JSONB (Turbine has always emitted JSONB). `jsonb`
52
+ // is the explicit spelling.
42
53
  json: 'JSONB',
54
+ jsonb: 'JSONB',
43
55
  uuid: 'UUID',
44
56
  real: 'REAL',
45
57
  double: 'DOUBLE PRECISION',
@@ -184,6 +196,11 @@ class ColumnBuilder {
184
196
  };
185
197
  }
186
198
  serial() {
199
+ // See TYPE_MAP: `serial` is SERIAL (int4) as of 0.24.0. Use bigserial() for 64-bit.
200
+ this._config.type = 'SERIAL';
201
+ return this;
202
+ }
203
+ bigserial() {
187
204
  this._config.type = 'BIGSERIAL';
188
205
  return this;
189
206
  }
@@ -216,6 +233,10 @@ class ColumnBuilder {
216
233
  this._config.type = 'TIMESTAMPTZ';
217
234
  return this;
218
235
  }
236
+ timestamptz() {
237
+ this._config.type = 'TIMESTAMPTZ';
238
+ return this;
239
+ }
219
240
  date() {
220
241
  this._config.type = 'DATE';
221
242
  return this;
@@ -224,6 +245,10 @@ class ColumnBuilder {
224
245
  this._config.type = 'JSONB';
225
246
  return this;
226
247
  }
248
+ jsonb() {
249
+ this._config.type = 'JSONB';
250
+ return this;
251
+ }
227
252
  uuid() {
228
253
  this._config.type = 'UUID';
229
254
  return this;
@@ -276,14 +301,17 @@ exports.ColumnBuilder = ColumnBuilder;
276
301
  /** Type guard: is `prop` a known nullary ColumnBuilder type method? */
277
302
  function isNullaryColumnType(prop) {
278
303
  return (prop === 'serial' ||
304
+ prop === 'bigserial' ||
279
305
  prop === 'bigint' ||
280
306
  prop === 'integer' ||
281
307
  prop === 'smallint' ||
282
308
  prop === 'text' ||
283
309
  prop === 'boolean' ||
284
310
  prop === 'timestamp' ||
311
+ prop === 'timestamptz' ||
285
312
  prop === 'date' ||
286
313
  prop === 'json' ||
314
+ prop === 'jsonb' ||
287
315
  prop === 'uuid' ||
288
316
  prop === 'real' ||
289
317
  prop === 'doublePrecision' ||
@@ -16,6 +16,15 @@ exports.schemaToSQLString = schemaToSQLString;
16
16
  const pg_1 = __importDefault(require("pg"));
17
17
  const dialect_js_1 = require("./dialect.js");
18
18
  const schema_js_1 = require("./schema.js");
19
+ /**
20
+ * Whether a resolved column type is an auto-increment pseudo-type (SERIAL /
21
+ * BIGSERIAL). These carry an implicit sequence default and NOT NULL, and their
22
+ * underlying integer width (int4 vs int8) is never auto-migrated by diff — a
23
+ * width change on a live PK is destructive and must be done by hand.
24
+ */
25
+ function isSerialType(type) {
26
+ return type === 'SERIAL' || type === 'BIGSERIAL';
27
+ }
19
28
  // ---------------------------------------------------------------------------
20
29
  // SQL Generation — SchemaDef → CREATE TABLE statements
21
30
  // ---------------------------------------------------------------------------
@@ -163,7 +172,7 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = dialect_js_1
163
172
  // 2. Is a serial (BIGSERIAL implies NOT NULL), OR
164
173
  // 3. Has a primary key (PKs are NOT NULL)
165
174
  // A column is left nullable if .nullable() was called.
166
- const isSerial = config.type === 'BIGSERIAL';
175
+ const isSerial = isSerialType(config.type);
167
176
  const implicitNotNull = isSerial || config.isPrimaryKey;
168
177
  const notNull = config.isNotNull && !implicitNotNull;
169
178
  // DEFAULT
@@ -352,9 +361,13 @@ async function schemaDiff(schema, connectionString) {
352
361
  result.reverseStatements.unshift(reverseSql);
353
362
  continue;
354
363
  }
355
- // Check type mismatch
364
+ // Check type mismatch. Serial/bigserial columns are exempt: their
365
+ // underlying int4/int8 width must never be auto-altered on a live table
366
+ // (a downcast on a PK loses data / breaks the sequence). This also
367
+ // preserves back-compat for DBs whose `serial` columns were created as
368
+ // BIGSERIAL (int8) before 0.24.0 — `push` won't try to shrink them.
356
369
  const expectedUdt = schemaTypeToUdt(config);
357
- if (expectedUdt && dbCol.udtName !== expectedUdt) {
370
+ if (expectedUdt && !isSerialType(config.type) && dbCol.udtName !== expectedUdt) {
358
371
  const sqlType = config.type === 'VARCHAR' && config.maxLength ? `VARCHAR(${config.maxLength})` : config.type;
359
372
  const oldSqlType = udtToSqlType(dbCol.udtName, dbCol.maxLength);
360
373
  const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${sqlType} USING ${dialect.quoteIdentifier(snakeName)}::${sqlType};`;
@@ -364,7 +377,7 @@ async function schemaDiff(schema, connectionString) {
364
377
  result.reverseStatements.unshift(reverseSql);
365
378
  }
366
379
  // Check NOT NULL mismatch
367
- const shouldBeNotNull = config.isNotNull || config.isPrimaryKey || config.type === 'BIGSERIAL';
380
+ const shouldBeNotNull = config.isNotNull || config.isPrimaryKey || isSerialType(config.type);
368
381
  const isCurrentlyNullable = dbCol.isNullable;
369
382
  if (shouldBeNotNull && isCurrentlyNullable && !config.isNullable) {
370
383
  const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET NOT NULL;`;
@@ -381,7 +394,7 @@ async function schemaDiff(schema, connectionString) {
381
394
  result.reverseStatements.unshift(reverseSql);
382
395
  }
383
396
  // Check DEFAULT value mismatch
384
- const isSerial = config.type === 'BIGSERIAL';
397
+ const isSerial = isSerialType(config.type);
385
398
  if (!isSerial) {
386
399
  const schemaDefault = config.defaultValue ? normalizeDefault(config.defaultValue) : null;
387
400
  const dbDefault = dbCol.columnDefault;
@@ -460,6 +473,7 @@ async function schemaDiff(schema, connectionString) {
460
473
  */
461
474
  function schemaTypeToUdt(config) {
462
475
  const map = {
476
+ SERIAL: 'int4',
463
477
  BIGSERIAL: 'int8',
464
478
  BIGINT: 'int8',
465
479
  INTEGER: 'int4',
@@ -33,6 +33,20 @@ export interface TurbineCliConfig {
33
33
  */
34
34
  adapter?: import('../adapters/index.js').DatabaseAdapter;
35
35
  }
36
+ /**
37
+ * Alias for {@link TurbineCliConfig}. Some docs and examples import the config
38
+ * type as `TurbineConfig`; both names refer to the same shape.
39
+ */
40
+ export type TurbineConfig = TurbineCliConfig;
41
+ /**
42
+ * Heuristic: does a configured `schema` value actually look like a schema FILE
43
+ * path rather than a Postgres schema name? `schema` is the Postgres namespace
44
+ * to introspect (default `public`); the schema-builder file goes in `schemaFile`.
45
+ * A value containing a path separator or a JS/TS extension is almost certainly a
46
+ * mis-set `schemaFile` — introspecting `WHERE table_schema = './turbine/schema.ts'`
47
+ * silently matches zero tables. Used by `turbine generate` to fail loudly.
48
+ */
49
+ export declare function looksLikeSchemaFilePath(schema: string): boolean;
36
50
  /**
37
51
  * Attempt to load a turbine config file from the current directory.
38
52
  * Returns the config if found, or an empty object.
@@ -66,4 +80,3 @@ export interface CliOverrides {
66
80
  */
67
81
  export declare function resolveConfig(fileConfig: TurbineCliConfig, overrides: CliOverrides): ResolvedConfig;
68
82
  export declare function configTemplate(connectionString?: string): string;
69
- //# sourceMappingURL=config.d.ts.map
@@ -7,6 +7,19 @@
7
7
  import { existsSync } from 'node:fs';
8
8
  import { join, resolve } from 'node:path';
9
9
  import { pathToFileURL } from 'node:url';
10
+ /**
11
+ * Heuristic: does a configured `schema` value actually look like a schema FILE
12
+ * path rather than a Postgres schema name? `schema` is the Postgres namespace
13
+ * to introspect (default `public`); the schema-builder file goes in `schemaFile`.
14
+ * A value containing a path separator or a JS/TS extension is almost certainly a
15
+ * mis-set `schemaFile` — introspecting `WHERE table_schema = './turbine/schema.ts'`
16
+ * silently matches zero tables. Used by `turbine generate` to fail loudly.
17
+ */
18
+ export function looksLikeSchemaFilePath(schema) {
19
+ if (!schema)
20
+ return false;
21
+ return schema.includes('/') || schema.includes('\\') || /\.(ts|mts|cts|js|mjs|cjs|json)$/i.test(schema);
22
+ }
10
23
  // ---------------------------------------------------------------------------
11
24
  // Config file names, in priority order
12
25
  // ---------------------------------------------------------------------------
@@ -110,4 +123,3 @@ ${urlLine}
110
123
  export default config;
111
124
  `;
112
125
  }
113
- //# sourceMappingURL=config.js.map
@@ -21,4 +21,3 @@
21
21
  * npx turbine migrate create add_users_table
22
22
  */
23
23
  export {};
24
- //# sourceMappingURL=index.d.ts.map
package/dist/cli/index.js CHANGED
@@ -26,7 +26,7 @@ import { pathToFileURL } from 'node:url';
26
26
  import { generate } from '../generate.js';
27
27
  import { introspect } from '../introspect.js';
28
28
  import { schemaDiff, schemaPush } from '../schema-sql.js';
29
- import { configTemplate, findConfigFile, loadConfig, resolveConfig } from './config.js';
29
+ import { configTemplate, findConfigFile, loadConfig, looksLikeSchemaFilePath, resolveConfig } from './config.js';
30
30
  import { canResolveTsx, getTsLoaderError, needsTsLoader, registerTsLoader } from './loader.js';
31
31
  import { createMigration, listMigrationFiles, migrateDown, migrateStatus, migrateUp } from './migrate.js';
32
32
  import { startObserve } from './observe.js';
@@ -85,6 +85,9 @@ function parseArgs() {
85
85
  case '--allow-drift':
86
86
  result.allowDrift = true;
87
87
  break;
88
+ case '--allow-empty':
89
+ result.allowEmpty = true;
90
+ break;
88
91
  case '--force':
89
92
  case '-f':
90
93
  result.force = true;
@@ -207,10 +210,28 @@ async function loadSchemaFile(schemaFile) {
207
210
  newline();
208
211
  console.log(` ${dim('Hint: install')} ${cyan('tsx')} ${dim('to load .ts files:')} ${cyan('npm install --save-dev tsx')}`);
209
212
  }
213
+ printCjsHintIfApplicable(err);
210
214
  }
211
215
  process.exit(1);
212
216
  }
213
217
  }
218
+ /**
219
+ * When a config/schema import blows up with the CommonJS-vs-ESM interop error
220
+ * (`Cannot require() ES Module` / `ERR_REQUIRE_ESM`), the root cause is almost
221
+ * always a project whose `package.json` lacks `"type": "module"`: tsx transpiles
222
+ * the `.ts` file to CJS and then can't `require()` Turbine's ESM build. Point the
223
+ * user at the one-line fix instead of leaving them with a raw Node stack trace.
224
+ */
225
+ function printCjsHintIfApplicable(err) {
226
+ const msg = err.message;
227
+ if (msg.includes('ERR_REQUIRE_ESM') ||
228
+ msg.includes('require() of ES Module') ||
229
+ msg.includes('Cannot require() ES Module')) {
230
+ newline();
231
+ console.log(` ${dim('Hint: add')} ${cyan('"type": "module"')} ${dim('to your')} ${cyan('package.json')}${dim('.')}`);
232
+ console.log(` ${dim('Turbine is an ESM package; without it, Node/tsx tries to')} ${cyan('require()')} ${dim('it and fails.')}`);
233
+ }
234
+ }
214
235
  // ---------------------------------------------------------------------------
215
236
  // Command: init
216
237
  // ---------------------------------------------------------------------------
@@ -408,6 +429,24 @@ async function cmdGenerate(args, config) {
408
429
  banner();
409
430
  const url = requireUrl(config);
410
431
  const startTime = performance.now();
432
+ // Guard: `schema` is the Postgres NAMESPACE to introspect (default `public`),
433
+ // NOT the path to your schema-builder file — that goes in `schemaFile`. If the
434
+ // configured `schema` looks like a file path, introspection would silently
435
+ // match zero tables and emit an empty client. Fail loudly instead.
436
+ if (!args.allowEmpty && looksLikeSchemaFilePath(config.schema)) {
437
+ error(`The "schema" option looks like a file path: ${cyan(config.schema)}`);
438
+ newline();
439
+ console.log(` ${dim('"schema" is the Postgres schema NAME to introspect')} ${dim('(default:')} ${cyan('public')}${dim(').')}`);
440
+ console.log(` ${dim('The path to your defineSchema() file belongs in')} ${cyan('schemaFile')}${dim('.')}`);
441
+ newline();
442
+ console.log(` ${dim('Fix your')} ${cyan('turbine.config.ts')}${dim(':')}`);
443
+ console.log(` ${green('schema:')} ${cyan("'public'")}${dim(", // or omit — introspects the 'public' schema")}`);
444
+ console.log(` ${green('schemaFile:')} ${cyan(`'${config.schema}'`)}${dim(', // your defineSchema() file (used by `turbine push`)')}`);
445
+ newline();
446
+ console.log(` ${dim('Re-run with')} ${cyan('--allow-empty')} ${dim('to introspect this literal schema name anyway.')}`);
447
+ newline();
448
+ process.exit(1);
449
+ }
411
450
  label('Database', redactUrl(url));
412
451
  label('Schema', config.schema);
413
452
  label('Output', config.out);
@@ -424,6 +463,24 @@ async function cmdGenerate(args, config) {
424
463
  const totalColumns = Object.values(schema.tables).reduce((sum, t) => sum + t.columns.length, 0);
425
464
  const totalRelations = Object.values(schema.tables).reduce((sum, t) => sum + Object.keys(t.relations).length, 0);
426
465
  spinner.succeed(`Found ${bold(String(tableNames.length))} tables, ${bold(String(totalColumns))} columns, ${bold(String(totalRelations))} relations`);
466
+ // Guard: zero tables means the generated client would be empty. That is almost
467
+ // always a misconfiguration (wrong `schema`, an include/exclude that filtered
468
+ // everything, or a database with no tables yet) rather than intent. Fail loudly
469
+ // instead of silently emitting an empty typed client.
470
+ if (tableNames.length === 0 && !args.allowEmpty) {
471
+ newline();
472
+ error(`Introspection matched 0 tables in schema ${cyan(config.schema)} — refusing to generate an empty client.`);
473
+ newline();
474
+ console.log(` ${dim('Common causes:')}`);
475
+ console.log(` ${dim('•')} ${cyan('schema')} ${dim('points at the wrong Postgres namespace')} ${dim('(it is the schema NAME, default')} ${cyan('public')}${dim(').')}`);
476
+ console.log(` ${dim('•')} ${dim('You meant to set')} ${cyan('schemaFile')} ${dim('(your defineSchema() file), not')} ${cyan('schema')}${dim('.')}`);
477
+ console.log(` ${dim('•')} ${cyan('include')}/${cyan('exclude')} ${dim('filtered out every table.')}`);
478
+ console.log(` ${dim('•')} ${dim('The database has no tables yet — run')} ${cyan('turbine push')} ${dim('or a migration first.')}`);
479
+ newline();
480
+ console.log(` ${dim('If an empty client is genuinely what you want, re-run with')} ${cyan('--allow-empty')}${dim('.')}`);
481
+ newline();
482
+ process.exit(1);
483
+ }
427
484
  // Print table summary
428
485
  if (args.verbose) {
429
486
  newline();
@@ -1099,6 +1156,7 @@ function showGenerateHelp() {
1099
1156
  console.log(` ${cyan('--schema, -s')} ${dim('<name>')} Postgres schema ${dim('(default: public)')}`);
1100
1157
  console.log(` ${cyan('--include')} ${dim('<tables>')} Comma-separated tables to include`);
1101
1158
  console.log(` ${cyan('--exclude')} ${dim('<tables>')} Comma-separated tables to exclude`);
1159
+ console.log(` ${cyan('--allow-empty')} Generate even when introspection matches 0 tables`);
1102
1160
  newline();
1103
1161
  }
1104
1162
  function showPushHelp() {
@@ -1307,6 +1365,8 @@ async function main() {
1307
1365
  catch (err) {
1308
1366
  if (args.command !== 'init') {
1309
1367
  warn(`Could not load config: ${err instanceof Error ? err.message : String(err)}`);
1368
+ if (err instanceof Error)
1369
+ printCjsHintIfApplicable(err);
1310
1370
  }
1311
1371
  }
1312
1372
  const overrides = {
@@ -1398,4 +1458,3 @@ async function main() {
1398
1458
  }
1399
1459
  }
1400
1460
  main();
1401
- //# sourceMappingURL=index.js.map
@@ -59,4 +59,3 @@ export declare function getTsLoaderError(): string | null;
59
59
  export declare function registerTsLoader(): Promise<TsLoaderStatus>;
60
60
  /** Reset the loader state — used by unit tests only. */
61
61
  export declare function _resetTsLoaderStateForTests(): void;
62
- //# sourceMappingURL=loader.d.ts.map
@@ -142,4 +142,3 @@ export function _resetTsLoaderStateForTests() {
142
142
  tsLoaderState = null;
143
143
  tsLoaderError = null;
144
144
  }
145
- //# sourceMappingURL=loader.js.map
@@ -150,4 +150,3 @@ export declare function migrateDown(connectionString: string, migrationsDir: str
150
150
  export declare function migrateStatus(connectionString: string, migrationsDir: string, options?: {
151
151
  dialect?: Dialect;
152
152
  }): Promise<MigrationStatus[]>;
153
- //# sourceMappingURL=migrate.d.ts.map
@@ -491,4 +491,3 @@ export async function migrateStatus(connectionString, migrationsDir, options) {
491
491
  await client.end();
492
492
  }
493
493
  }
494
- //# sourceMappingURL=migrate.js.map
@@ -1,2 +1 @@
1
1
  export declare const OBSERVE_HTML = "<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta name=\"color-scheme\" content=\"dark\" />\n <title>Turbine Observe</title>\n <style>\n :root {\n --bg: #0a0a0b;\n --bg-elev: #111113;\n --bg-hover: #1a1a1d;\n --border: #26262b;\n --text: #e6e6ea;\n --text-dim: #8a8a93;\n --accent: #60a5fa;\n --green: #4ade80;\n --red: #f87171;\n --orange: #fb923c;\n --purple: #a78bfa;\n --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n --sans: system-ui, -apple-system, sans-serif;\n --radius: 6px;\n }\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { background: var(--bg); color: var(--text); font-family: var(--sans); font-size: 14px; padding: 24px; }\n h1 { font-size: 20px; margin-bottom: 4px; }\n .subtitle { color: var(--text-dim); margin-bottom: 24px; }\n .controls { display: flex; gap: 8px; margin-bottom: 24px; }\n .controls button {\n background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius);\n color: var(--text); padding: 6px 12px; cursor: pointer; font-size: 13px;\n }\n .controls button.active { border-color: var(--accent); color: var(--accent); }\n .card {\n background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius);\n padding: 16px; margin-bottom: 16px;\n }\n .card h2 { font-size: 14px; color: var(--text-dim); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }\n table { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 12px; }\n th { text-align: left; padding: 6px 8px; color: var(--text-dim); border-bottom: 1px solid var(--border); }\n td { padding: 6px 8px; border-bottom: 1px solid var(--border); }\n .num { text-align: right; }\n .error-rate { color: var(--red); }\n .low-error { color: var(--green); }\n svg { width: 100%; height: 200px; }\n .chart-line { fill: none; stroke-width: 1.5; }\n .line-avg { stroke: var(--accent); }\n .line-p95 { stroke: var(--orange); }\n .line-p99 { stroke: var(--red); }\n .legend { display: flex; gap: 16px; margin-top: 8px; font-size: 12px; color: var(--text-dim); }\n .legend span::before { content: ''; display: inline-block; width: 12px; height: 2px; margin-right: 4px; vertical-align: middle; }\n .legend .l-avg::before { background: var(--accent); }\n .legend .l-p95::before { background: var(--orange); }\n .legend .l-p99::before { background: var(--red); }\n .empty { color: var(--text-dim); text-align: center; padding: 40px; }\n </style>\n</head>\n<body>\n <h1>Turbine Observe</h1>\n <p class=\"subtitle\">Query performance metrics</p>\n <div class=\"controls\">\n <button data-range=\"1h\" class=\"active\">1h</button>\n <button data-range=\"6h\">6h</button>\n <button data-range=\"24h\">24h</button>\n <button data-range=\"7d\">7d</button>\n </div>\n <div class=\"card\" id=\"latency-card\">\n <h2>Latency over time</h2>\n <div id=\"chart\"></div>\n <div class=\"legend\">\n <span class=\"l-avg\">avg</span>\n <span class=\"l-p95\">p95</span>\n <span class=\"l-p99\">p99</span>\n </div>\n </div>\n <div class=\"card\" id=\"models-card\">\n <h2>Top models</h2>\n <div id=\"models-table\"></div>\n </div>\n <div class=\"card\" id=\"errors-card\">\n <h2>Error rates</h2>\n <div id=\"errors-table\"></div>\n </div>\n <script>\n let currentRange = '1h';\n const token = document.cookie.match(/turbine_observe_token=([a-f0-9]+)/)?.[1] || '';\n const headers = { 'x-turbine-token': token };\n\n document.querySelector('.controls').addEventListener('click', e => {\n if (e.target.tagName !== 'BUTTON') return;\n document.querySelectorAll('.controls button').forEach(b => b.classList.remove('active'));\n e.target.classList.add('active');\n currentRange = e.target.dataset.range;\n refresh();\n });\n\n async function fetchJson(path) {\n const res = await fetch(path, { headers });\n if (!res.ok) return null;\n return res.json();\n }\n\n function buildSvgPath(points, width, height, maxY) {\n if (points.length === 0) return '';\n const xStep = width / Math.max(points.length - 1, 1);\n return points.map((y, i) => {\n const px = i * xStep;\n const py = height - (y / maxY) * height;\n return (i === 0 ? 'M' : 'L') + px.toFixed(1) + ',' + py.toFixed(1);\n }).join(' ');\n }\n\n function renderChart(data) {\n const el = document.getElementById('chart');\n if (!data || data.length === 0) { el.innerHTML = '<p class=\"empty\">No data yet</p>'; return; }\n const width = 800; const height = 180;\n const allVals = data.flatMap(d => [d.avg_ms, d.p95_ms, d.p99_ms]);\n const maxY = Math.max(...allVals, 1) * 1.1;\n const avgPath = buildSvgPath(data.map(d => d.avg_ms), width, height, maxY);\n const p95Path = buildSvgPath(data.map(d => d.p95_ms), width, height, maxY);\n const p99Path = buildSvgPath(data.map(d => d.p99_ms), width, height, maxY);\n el.innerHTML = '<svg viewBox=\"0 0 ' + width + ' ' + height + '\" preserveAspectRatio=\"none\">'\n + '<path class=\"chart-line line-avg\" d=\"' + avgPath + '\"/>'\n + '<path class=\"chart-line line-p95\" d=\"' + p95Path + '\"/>'\n + '<path class=\"chart-line line-p99\" d=\"' + p99Path + '\"/>'\n + '</svg>';\n }\n\n function escapeHtml(s) {\n if (s == null) return '';\n return String(s)\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n }\n\n function renderModels(data) {\n const el = document.getElementById('models-table');\n if (!data || data.length === 0) { el.innerHTML = '<p class=\"empty\">No data yet</p>'; return; }\n let html = '<table><thead><tr><th>Model</th><th>Action</th><th class=\"num\">Count</th><th class=\"num\">Avg (ms)</th><th class=\"num\">P95 (ms)</th><th class=\"num\">P99 (ms)</th></tr></thead><tbody>';\n for (const row of data) {\n html += '<tr><td>' + escapeHtml(row.model) + '</td><td>' + escapeHtml(row.action) + '</td>'\n + '<td class=\"num\">' + row.count + '</td>'\n + '<td class=\"num\">' + row.avg_ms.toFixed(1) + '</td>'\n + '<td class=\"num\">' + row.p95_ms.toFixed(1) + '</td>'\n + '<td class=\"num\">' + row.p99_ms.toFixed(1) + '</td></tr>';\n }\n html += '</tbody></table>';\n el.innerHTML = html;\n }\n\n function renderErrors(data) {\n const el = document.getElementById('errors-table');\n if (!data || data.length === 0) { el.innerHTML = '<p class=\"empty\">No errors</p>'; return; }\n let html = '<table><thead><tr><th>Model</th><th>Action</th><th class=\"num\">Total</th><th class=\"num\">Errors</th><th class=\"num\">Rate</th></tr></thead><tbody>';\n for (const row of data) {\n const rate = row.count > 0 ? (row.error_count / row.count * 100).toFixed(1) : '0.0';\n const cls = parseFloat(rate) > 5 ? 'error-rate' : 'low-error';\n html += '<tr><td>' + escapeHtml(row.model) + '</td><td>' + escapeHtml(row.action) + '</td>'\n + '<td class=\"num\">' + row.count + '</td>'\n + '<td class=\"num\">' + row.error_count + '</td>'\n + '<td class=\"num ' + cls + '\">' + rate + '%</td></tr>';\n }\n html += '</tbody></table>';\n el.innerHTML = html;\n }\n\n async function refresh() {\n const [latency, models] = await Promise.all([\n fetchJson('/api/latency?range=' + currentRange),\n fetchJson('/api/models?range=' + currentRange),\n ]);\n renderChart(latency);\n renderModels(models);\n // Derive errors from models data\n const withErrors = (models || []).filter(m => m.error_count > 0);\n renderErrors(withErrors);\n }\n\n refresh();\n setInterval(refresh, 60000);\n </script>\n</body>\n</html>";
2
- //# sourceMappingURL=observe-ui.d.ts.map
@@ -187,4 +187,3 @@ export const OBSERVE_HTML = `<!doctype html>
187
187
  </script>
188
188
  </body>
189
189
  </html>`;
190
- //# sourceMappingURL=observe-ui.js.map
@@ -17,4 +17,3 @@ export interface ObserveServerHandle {
17
17
  url: string;
18
18
  }
19
19
  export declare function startObserve(options: ObserveOptions): Promise<ObserveServerHandle>;
20
- //# sourceMappingURL=observe.d.ts.map
@@ -233,4 +233,3 @@ function openUrl(url) {
233
233
  // Best effort
234
234
  }
235
235
  }
236
- //# sourceMappingURL=observe.js.map
@@ -1,2 +1 @@
1
1
  export declare const STUDIO_HTML: string;
2
- //# sourceMappingURL=studio-ui.generated.d.ts.map