turbine-orm 0.5.0 → 0.7.1

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 (50) hide show
  1. package/README.md +292 -26
  2. package/dist/cjs/cli/config.js +5 -15
  3. package/dist/cjs/cli/index.js +311 -43
  4. package/dist/cjs/cli/loader.js +129 -0
  5. package/dist/cjs/cli/migrate.js +96 -47
  6. package/dist/cjs/cli/ui.js +5 -9
  7. package/dist/cjs/client.js +158 -49
  8. package/dist/cjs/errors.js +424 -0
  9. package/dist/cjs/generate.js +145 -14
  10. package/dist/cjs/index.js +43 -20
  11. package/dist/cjs/introspect.js +3 -5
  12. package/dist/cjs/pipeline.js +9 -2
  13. package/dist/cjs/query.js +544 -115
  14. package/dist/cjs/schema-builder.js +150 -30
  15. package/dist/cjs/schema-sql.js +241 -37
  16. package/dist/cjs/schema.js +5 -2
  17. package/dist/cjs/serverless.js +88 -176
  18. package/dist/cli/config.js +6 -16
  19. package/dist/cli/index.js +316 -48
  20. package/dist/cli/loader.d.ts +45 -0
  21. package/dist/cli/loader.js +91 -0
  22. package/dist/cli/migrate.d.ts +13 -2
  23. package/dist/cli/migrate.js +97 -48
  24. package/dist/cli/ui.d.ts +1 -1
  25. package/dist/cli/ui.js +5 -9
  26. package/dist/client.d.ts +92 -4
  27. package/dist/client.js +158 -49
  28. package/dist/errors.d.ts +225 -0
  29. package/dist/errors.js +405 -0
  30. package/dist/generate.d.ts +7 -1
  31. package/dist/generate.js +148 -18
  32. package/dist/index.d.ts +11 -9
  33. package/dist/index.js +16 -12
  34. package/dist/introspect.d.ts +1 -1
  35. package/dist/introspect.js +4 -6
  36. package/dist/pipeline.d.ts +1 -1
  37. package/dist/pipeline.js +9 -2
  38. package/dist/query.d.ts +374 -38
  39. package/dist/query.js +545 -116
  40. package/dist/schema-builder.d.ts +38 -5
  41. package/dist/schema-builder.js +150 -31
  42. package/dist/schema-sql.d.ts +7 -3
  43. package/dist/schema-sql.js +241 -37
  44. package/dist/schema.d.ts +1 -1
  45. package/dist/schema.js +5 -2
  46. package/dist/serverless.d.ts +92 -139
  47. package/dist/serverless.js +87 -173
  48. package/package.json +33 -16
  49. package/dist/types.d.ts +0 -93
  50. package/dist/types.js +0 -126
package/dist/cli/index.js CHANGED
@@ -19,15 +19,16 @@
19
19
  * npx turbine init --url postgres://...
20
20
  * npx turbine migrate create add_users_table
21
21
  */
22
- import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync } from 'node:fs';
23
- import { resolve, relative } from 'node:path';
24
- import { introspect } from '../introspect.js';
22
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
23
+ import { relative, resolve } from 'node:path';
24
+ import { pathToFileURL } from 'node:url';
25
25
  import { generate } from '../generate.js';
26
+ import { introspect } from '../introspect.js';
26
27
  import { schemaDiff, schemaPush } from '../schema-sql.js';
27
- import { loadConfig, resolveConfig, findConfigFile, configTemplate } from './config.js';
28
- import { createMigration, migrateUp, migrateDown, migrateStatus, listMigrationFiles, } from './migrate.js';
29
- import { bold, dim, red, green, yellow, blue, cyan, gray, magenta, greenBright, cyanBright, yellowBright, symbols, box, table as formatTable, Spinner, header, success, error, warn, info, label, newline, divider, banner, elapsed, redactUrl, } from './ui.js';
30
- import { pathToFileURL } from 'node:url';
28
+ import { configTemplate, findConfigFile, loadConfig, resolveConfig } from './config.js';
29
+ import { needsTsLoader, registerTsLoader } from './loader.js';
30
+ import { createMigration, listMigrationFiles, migrateDown, migrateStatus, migrateUp } from './migrate.js';
31
+ import { banner, blue, bold, box, cyan, dim, divider, elapsed, error, table as formatTable, gray, green, header, info, label, magenta, newline, red, redactUrl, Spinner, success, symbols, warn, yellow, } from './ui.js';
31
32
  function parseArgs() {
32
33
  const args = process.argv.slice(2);
33
34
  const result = {
@@ -75,6 +76,12 @@ function parseArgs() {
75
76
  case '--dry-run':
76
77
  result.dryRun = true;
77
78
  break;
79
+ case '--auto':
80
+ result.auto = true;
81
+ break;
82
+ case '--allow-drift':
83
+ result.allowDrift = true;
84
+ break;
78
85
  case '--force':
79
86
  case '-f':
80
87
  result.force = true;
@@ -83,6 +90,10 @@ function parseArgs() {
83
90
  case '-v':
84
91
  result.verbose = true;
85
92
  break;
93
+ case '--help':
94
+ case '-h':
95
+ result.help = true;
96
+ break;
86
97
  default:
87
98
  if (!arg.startsWith('-')) {
88
99
  result.positional.push(arg);
@@ -93,6 +104,36 @@ function parseArgs() {
93
104
  return result;
94
105
  }
95
106
  // ---------------------------------------------------------------------------
107
+ // TypeScript loader — user-facing error helper
108
+ // ---------------------------------------------------------------------------
109
+ /**
110
+ * Print a friendly error explaining how to install tsx, then exit.
111
+ * Called when we know we need to load a `.ts` file but the loader isn't available.
112
+ */
113
+ function failMissingTsLoader(filePath, reason) {
114
+ newline();
115
+ error(`Cannot load TypeScript file: ${filePath}`);
116
+ newline();
117
+ if (reason === 'unsupported') {
118
+ console.log(` ${dim('Your Node.js version does not support')} ${cyan('module.register()')}.`);
119
+ console.log(` ${dim('Upgrade to Node.js')} ${cyan('20.6+')} ${dim('or use a')} ${cyan('.js')} ${dim('/')} ${cyan('.mjs')} ${dim('config file.')}`);
120
+ }
121
+ else {
122
+ console.log(` ${dim('Loading .ts config / schema files requires')} ${cyan('tsx')} ${dim('to be installed.')}`);
123
+ newline();
124
+ console.log(` ${dim('Install it as a dev dependency:')}`);
125
+ console.log(` ${cyan('npm install --save-dev tsx')}`);
126
+ console.log(` ${dim('or')}`);
127
+ console.log(` ${cyan('pnpm add -D tsx')}`);
128
+ console.log(` ${dim('or')}`);
129
+ console.log(` ${cyan('yarn add -D tsx')}`);
130
+ newline();
131
+ console.log(` ${dim('Alternatively, rename your file to')} ${cyan('.js')} ${dim('or')} ${cyan('.mjs')}.`);
132
+ }
133
+ newline();
134
+ process.exit(1);
135
+ }
136
+ // ---------------------------------------------------------------------------
96
137
  // Helpers
97
138
  // ---------------------------------------------------------------------------
98
139
  function requireUrl(config) {
@@ -115,6 +156,15 @@ async function loadSchemaFile(schemaFile) {
115
156
  console.log(` ${dim('Create one with:')} ${cyan('turbine init')}`);
116
157
  process.exit(1);
117
158
  }
159
+ // If this is a TypeScript file, ensure the tsx ESM loader is registered
160
+ // before we attempt the dynamic import. Without this, Node throws
161
+ // ERR_UNKNOWN_FILE_EXTENSION for `.ts`.
162
+ if (needsTsLoader(absPath)) {
163
+ const status = await registerTsLoader();
164
+ if (status === 'missing' || status === 'unsupported') {
165
+ failMissingTsLoader(schemaFile, status);
166
+ }
167
+ }
118
168
  try {
119
169
  const fileUrl = pathToFileURL(absPath).href;
120
170
  const mod = await import(fileUrl);
@@ -129,6 +179,11 @@ async function loadSchemaFile(schemaFile) {
129
179
  error(`Failed to load schema file: ${schemaFile}`);
130
180
  if (err instanceof Error) {
131
181
  console.log(` ${dim(err.message)}`);
182
+ // If the error is the classic ERR_UNKNOWN_FILE_EXTENSION, give a hint.
183
+ if (err.message.includes('ERR_UNKNOWN_FILE_EXTENSION') || err.message.includes('Unknown file extension')) {
184
+ newline();
185
+ console.log(` ${dim('Hint: install')} ${cyan('tsx')} ${dim('to load .ts files:')} ${cyan('npm install --save-dev tsx')}`);
186
+ }
132
187
  }
133
188
  process.exit(1);
134
189
  }
@@ -140,7 +195,7 @@ async function cmdInit(args, config) {
140
195
  banner();
141
196
  header('Initializing Turbine project');
142
197
  // Detect environment
143
- const envUrl = process.env['DATABASE_URL'];
198
+ const envUrl = process.env.DATABASE_URL;
144
199
  const hasEnvFile = existsSync('.env');
145
200
  const hasEnvLocal = existsSync('.env.local');
146
201
  if (envUrl) {
@@ -179,7 +234,7 @@ async function cmdInit(args, config) {
179
234
  mkdirSync(migrDir, { recursive: true });
180
235
  // Create .gitkeep
181
236
  writeFileSync(`${migrDir}/.gitkeep`, '', 'utf-8');
182
- success(`Created ${cyan(migrDir + '/')}`);
237
+ success(`Created ${cyan(`${migrDir}/`)}`);
183
238
  }
184
239
  else {
185
240
  info(`Migrations dir already exists: ${dim(migrDir)}`);
@@ -187,7 +242,7 @@ async function cmdInit(args, config) {
187
242
  // Create output directory
188
243
  if (!existsSync(config.out)) {
189
244
  mkdirSync(config.out, { recursive: true });
190
- success(`Created ${cyan(config.out + '/')}`);
245
+ success(`Created ${cyan(`${config.out}/`)}`);
191
246
  }
192
247
  // Create seed file template
193
248
  const seedDir = config.seedFile.substring(0, config.seedFile.lastIndexOf('/'));
@@ -231,7 +286,7 @@ async function cmdInit(args, config) {
231
286
  * Define your database schema in TypeScript.
232
287
  * Use \`npx turbine push\` to sync it to your database.
233
288
  *
234
- * @see https://batadata.com/docs/turbine/schema
289
+ * @see https://turbineorm.dev
235
290
  */
236
291
 
237
292
  import { defineSchema } from 'turbine-orm';
@@ -282,15 +337,15 @@ export default defineSchema({
282
337
  spinner.succeed(`Found ${bold(String(tableCount))} tables`);
283
338
  const genSpinner = new Spinner('Generating TypeScript client').start();
284
339
  const result = generate({ schema, outDir: config.out, connectionString: url });
285
- genSpinner.succeed(`Generated ${bold(String(result.files.length))} files to ${cyan(config.out + '/')}`);
340
+ genSpinner.succeed(`Generated ${bold(String(result.files.length))} files to ${cyan(`${config.out}/`)}`);
286
341
  }
287
342
  catch (err) {
288
343
  spinner.fail('Could not connect to database');
289
344
  if (err instanceof Error) {
290
- console.log(` ${dim(err.message)}`);
345
+ console.log(` ${dim(redactUrl(err.message))}`);
291
346
  }
292
347
  newline();
293
- info('You can run generation later with: ' + cyan('npx turbine generate'));
348
+ info(`You can run generation later with: ${cyan('npx turbine generate')}`);
294
349
  }
295
350
  }
296
351
  // Next steps
@@ -366,7 +421,7 @@ async function cmdGenerate(args, config) {
366
421
  genSpinner.succeed(`Generated ${bold(String(result.files.length))} files in ${elapsed(startTime)}`);
367
422
  // List files
368
423
  for (const file of result.files) {
369
- console.log(` ${dim(symbols.teeEnd)} ${cyan(result.outDir + '/' + file)}`);
424
+ console.log(` ${dim(symbols.teeEnd)} ${cyan(`${result.outDir}/${file}`)}`);
370
425
  }
371
426
  // Usage hint
372
427
  newline();
@@ -414,9 +469,11 @@ async function cmdPush(args, config) {
414
469
  for (const a of diff.alter) {
415
470
  console.log(` ${yellow(symbols.arrowRight)} ${a.table}`);
416
471
  for (const col of a.columns) {
417
- const actionLabel = col.action === 'add' ? green('+ add') :
418
- col.action === 'drop' ? red('- drop') :
419
- yellow('~ ' + col.action.replace('_', ' '));
472
+ const actionLabel = col.action === 'add'
473
+ ? green('+ add')
474
+ : col.action === 'drop'
475
+ ? red('- drop')
476
+ : yellow(`~ ${col.action.replace('_', ' ')}`);
420
477
  console.log(` ${actionLabel} ${col.column}`);
421
478
  }
422
479
  }
@@ -470,17 +527,21 @@ async function cmdMigrate(args, config) {
470
527
  console.log(` ${bold('turbine migrate')} ${dim('— SQL-first migration system')}`);
471
528
  newline();
472
529
  console.log(` ${bold('Commands:')}`);
473
- console.log(` ${cyan('create <name>')} Create a new migration file`);
474
- console.log(` ${cyan('up')} Apply pending migrations`);
475
- console.log(` ${cyan('down')} Rollback last migration`);
476
- console.log(` ${cyan('status')} Show migration status`);
530
+ console.log(` ${cyan('create <name>')} Create a new migration file`);
531
+ console.log(` ${cyan('create <name> --auto')} Auto-generate from schema diff`);
532
+ console.log(` ${cyan('up')} Apply pending migrations`);
533
+ console.log(` ${cyan('down')} Rollback last migration`);
534
+ console.log(` ${cyan('status')} Show migration status`);
477
535
  newline();
478
536
  console.log(` ${bold('Options:')}`);
479
- console.log(` ${cyan('--step, -n')} Number of migrations to apply/rollback`);
480
- console.log(` ${cyan('--dry-run')} Show SQL without executing`);
537
+ console.log(` ${cyan('--auto')} Auto-generate UP/DOWN SQL from schema diff`);
538
+ console.log(` ${cyan('--step, -n')} Number of migrations to apply/rollback`);
539
+ console.log(` ${cyan('--dry-run')} Show SQL without executing`);
540
+ console.log(` ${cyan('--allow-drift')} Bypass checksum validation on ${cyan('migrate up')} ${dim('(advanced)')}`);
481
541
  newline();
482
542
  console.log(` ${bold('Examples:')}`);
483
543
  console.log(` ${dim('npx turbine migrate create add_users_table')}`);
544
+ console.log(` ${dim('npx turbine migrate create add_email_index --auto')}`);
484
545
  console.log(` ${dim('npx turbine migrate up')}`);
485
546
  console.log(` ${dim('npx turbine migrate down --step 2')}`);
486
547
  newline();
@@ -514,9 +575,61 @@ async function cmdMigrateCreate(args, config) {
514
575
  newline();
515
576
  console.log(` ${dim('Usage:')} ${cyan('npx turbine migrate create <name>')}`);
516
577
  console.log(` ${dim('Example:')} ${cyan('npx turbine migrate create add_users_table')}`);
578
+ console.log(` ${dim('Auto:')} ${cyan('npx turbine migrate create my_change --auto')}`);
517
579
  newline();
518
580
  process.exit(1);
519
581
  }
582
+ if (args.auto) {
583
+ // Auto-generate migration from schema diff
584
+ const url = requireUrl(config);
585
+ label('Database', redactUrl(url));
586
+ label('Schema file', config.schemaFile);
587
+ newline();
588
+ const schemaDef = await loadSchemaFile(config.schemaFile);
589
+ const diffSpinner = new Spinner('Computing schema diff').start();
590
+ const diff = await schemaDiff(schemaDef, url);
591
+ if (diff.statements.length === 0) {
592
+ diffSpinner.succeed('Database is already in sync — nothing to migrate');
593
+ newline();
594
+ return;
595
+ }
596
+ diffSpinner.succeed(`Found ${bold(String(diff.statements.length))} change(s)`);
597
+ newline();
598
+ const upSQL = diff.statements.join('\n');
599
+ const downSQL = diff.reverseStatements.join('\n');
600
+ const file = createMigration(config.migrationsDir, name, { up: upSQL, down: downSQL });
601
+ const relPath = relative(process.cwd(), file.path);
602
+ success(`Created auto-migration: ${bold(file.filename)}`);
603
+ newline();
604
+ console.log(` ${dim('File:')} ${cyan(relPath)}`);
605
+ newline();
606
+ // Show summary of changes
607
+ if (diff.create.length > 0) {
608
+ console.log(` ${green('+ Create')} ${diff.create.length} table(s): ${diff.create.map((t) => t.name).join(', ')}`);
609
+ }
610
+ if (diff.alter.length > 0) {
611
+ console.log(` ${yellow('~ Alter')} ${diff.alter.length} table(s):`);
612
+ for (const a of diff.alter) {
613
+ for (const col of a.columns) {
614
+ const actionLabel = col.action === 'add'
615
+ ? green('+ add')
616
+ : col.action === 'drop'
617
+ ? red('- drop')
618
+ : col.action === 'add_unique'
619
+ ? green('+ unique')
620
+ : col.action === 'drop_unique'
621
+ ? red('- unique')
622
+ : yellow(`~ ${col.action.replace(/_/g, ' ')}`);
623
+ console.log(` ${actionLabel} ${a.table}.${col.column}`);
624
+ }
625
+ }
626
+ }
627
+ newline();
628
+ console.log(` ${dim('Review the migration, then run:')}`);
629
+ console.log(` ${cyan('npx turbine migrate up')}`);
630
+ newline();
631
+ return;
632
+ }
520
633
  const file = createMigration(config.migrationsDir, name);
521
634
  const relPath = relative(process.cwd(), file.path);
522
635
  success(`Created migration: ${bold(file.filename)}`);
@@ -540,9 +653,18 @@ async function cmdMigrateUp(args, config) {
540
653
  newline();
541
654
  return;
542
655
  }
656
+ // Big, loud warning when bypassing drift detection — this is a deliberately
657
+ // dangerous operation and the user should see it on every invocation.
658
+ if (args.allowDrift) {
659
+ warn('--allow-drift is set — checksum validation is DISABLED for this run.');
660
+ console.log(` ${dim('Applied migrations may have been modified or deleted on disk.')}`);
661
+ console.log(` ${dim('Proceed only if you are intentionally rewriting migration history.')}`);
662
+ newline();
663
+ }
543
664
  const spinner = new Spinner('Applying migrations').start();
544
665
  const result = await migrateUp(url, config.migrationsDir, {
545
666
  step: args.step,
667
+ allowDrift: args.allowDrift,
546
668
  });
547
669
  if (result.applied.length === 0 && result.errors.length === 0) {
548
670
  spinner.succeed('All migrations are up to date');
@@ -598,7 +720,7 @@ async function cmdMigrateDown(args, config) {
598
720
  }
599
721
  newline();
600
722
  }
601
- async function cmdMigrateStatus(args, config) {
723
+ async function cmdMigrateStatus(_args, config) {
602
724
  banner();
603
725
  const url = requireUrl(config);
604
726
  label('Database', redactUrl(url));
@@ -628,19 +750,22 @@ async function cmdMigrateStatus(args, config) {
628
750
  const rows = statuses.map((s) => {
629
751
  let status;
630
752
  if (s.applied && s.checksumValid === false) {
631
- status = red(symbols.warning + ' Drifted');
753
+ status = red(`${symbols.warning} Drifted`);
632
754
  }
633
755
  else if (s.applied) {
634
- status = green(symbols.check + ' Applied');
756
+ status = green(`${symbols.check} Applied`);
635
757
  }
636
758
  else {
637
- status = yellow(symbols.dot + ' Pending');
759
+ status = yellow(`${symbols.dot} Pending`);
638
760
  }
639
761
  return [
640
762
  status,
641
763
  s.file.filename,
642
764
  s.appliedAt
643
- ? dim(s.appliedAt.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'))
765
+ ? dim(s.appliedAt
766
+ .toISOString()
767
+ .replace('T', ' ')
768
+ .replace(/\.\d+Z$/, ' UTC'))
644
769
  : dim('—'),
645
770
  ];
646
771
  });
@@ -654,7 +779,7 @@ async function cmdMigrateStatus(args, config) {
654
779
  // ---------------------------------------------------------------------------
655
780
  // Command: seed
656
781
  // ---------------------------------------------------------------------------
657
- async function cmdSeed(args, config) {
782
+ async function cmdSeed(_args, config) {
658
783
  banner();
659
784
  const seedFile = resolve(config.seedFile);
660
785
  label('Seed file', config.seedFile);
@@ -670,22 +795,20 @@ async function cmdSeed(args, config) {
670
795
  const spinner = new Spinner('Running seed file').start();
671
796
  try {
672
797
  // Use child_process to run the seed file via tsx or node
673
- const { execSync } = await import('node:child_process');
798
+ const { execFileSync } = await import('node:child_process');
674
799
  // Try tsx first (most compatible with .ts files), fall back to node --experimental-strip-types
675
800
  const runners = [
676
- { cmd: 'npx tsx', name: 'tsx' },
677
- { cmd: 'node --experimental-strip-types', name: 'node' },
801
+ { cmd: 'npx', args: ['tsx', seedFile], name: 'tsx' },
802
+ { cmd: 'node', args: ['--experimental-strip-types', seedFile], name: 'node' },
678
803
  ];
679
- // Shell-escape the seed file path to prevent injection
680
- const escapedSeedFile = seedFile.replace(/'/g, "'\\''");
681
804
  let ran = false;
682
805
  for (const runner of runners) {
683
806
  try {
684
- execSync(`${runner.cmd} '${escapedSeedFile}'`, {
807
+ execFileSync(runner.cmd, runner.args, {
685
808
  stdio: 'inherit',
686
809
  env: {
687
810
  ...process.env,
688
- DATABASE_URL: config.url || process.env['DATABASE_URL'],
811
+ DATABASE_URL: config.url || process.env.DATABASE_URL,
689
812
  },
690
813
  });
691
814
  ran = true;
@@ -707,7 +830,7 @@ async function cmdSeed(args, config) {
707
830
  catch (err) {
708
831
  spinner.fail('Seed failed');
709
832
  if (err instanceof Error) {
710
- console.log(` ${dim(err.message)}`);
833
+ console.log(` ${dim(redactUrl(err.message))}`);
711
834
  }
712
835
  newline();
713
836
  process.exit(1);
@@ -717,7 +840,7 @@ async function cmdSeed(args, config) {
717
840
  // ---------------------------------------------------------------------------
718
841
  // Command: status
719
842
  // ---------------------------------------------------------------------------
720
- async function cmdStatus(args, config) {
843
+ async function cmdStatus(_args, config) {
721
844
  banner();
722
845
  const url = requireUrl(config);
723
846
  label('Database', redactUrl(url));
@@ -735,7 +858,7 @@ async function cmdStatus(args, config) {
735
858
  newline();
736
859
  for (const tbl of Object.values(schema.tables)) {
737
860
  const relCount = Object.keys(tbl.relations).length;
738
- const pk = tbl.primaryKey.join(', ') || dim('(none)');
861
+ const _pk = tbl.primaryKey.join(', ') || dim('(none)');
739
862
  console.log(` ${bold(cyan(tbl.name))}`);
740
863
  for (let i = 0; i < tbl.columns.length; i++) {
741
864
  const col = tbl.columns[i];
@@ -744,7 +867,7 @@ async function cmdStatus(args, config) {
744
867
  const nullable = col.nullable ? dim('?') : '';
745
868
  const def = col.hasDefault ? dim(' (default)') : '';
746
869
  const pkLabel = tbl.primaryKey.includes(col.name) ? ` ${magenta('PK')}` : '';
747
- console.log(` ${dim(prefix)} ${col.field}${nullable}: ${green(col.tsType)}${pkLabel}${def} ${gray(symbols.arrow + ' ' + col.pgType)}`);
870
+ console.log(` ${dim(prefix)} ${col.field}${nullable}: ${green(col.tsType)}${pkLabel}${def} ${gray(`${symbols.arrow} ${col.pgType}`)}`);
748
871
  }
749
872
  const rels = Object.entries(tbl.relations);
750
873
  if (rels.length > 0) {
@@ -777,11 +900,139 @@ async function cmdStudio(_args, _config) {
777
900
  'A local web UI for browsing your database,',
778
901
  'exploring relations, and managing data.',
779
902
  '',
780
- `Follow ${cyan('@batadata')} for updates.`,
903
+ `Follow ${cyan('@turbineorm')} for updates.`,
781
904
  ].join('\n'), { title: bold(cyan('Studio')), padding: 2 }));
782
905
  newline();
783
906
  }
784
907
  // ---------------------------------------------------------------------------
908
+ // Subcommand help
909
+ // ---------------------------------------------------------------------------
910
+ function showSubcommandHelp(command) {
911
+ const helpMap = {
912
+ init: showInitHelp,
913
+ generate: showGenerateHelp,
914
+ pull: showGenerateHelp,
915
+ push: showPushHelp,
916
+ migrate: showMigrateHelp,
917
+ migration: showMigrateHelp,
918
+ seed: showSeedHelp,
919
+ status: showStatusHelp,
920
+ };
921
+ const fn = helpMap[command];
922
+ if (fn) {
923
+ fn();
924
+ return true;
925
+ }
926
+ return false;
927
+ }
928
+ function showInitHelp() {
929
+ banner();
930
+ console.log(` ${bold('turbine init')} — Initialize a Turbine project`);
931
+ newline();
932
+ console.log(` ${bold('Usage:')}`);
933
+ console.log(` npx turbine init ${dim('[options]')}`);
934
+ newline();
935
+ console.log(` Creates ${cyan('turbine.config.ts')}, migrations directory, seed file template,`);
936
+ console.log(` and schema file template.`);
937
+ newline();
938
+ console.log(` ${bold('Options:')}`);
939
+ console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string to embed in config`);
940
+ console.log(` ${cyan('--force, -f')} Overwrite existing config file`);
941
+ newline();
942
+ }
943
+ function showGenerateHelp() {
944
+ banner();
945
+ console.log(` ${bold('turbine generate')} — Introspect database and generate TypeScript types`);
946
+ newline();
947
+ console.log(` ${bold('Usage:')}`);
948
+ console.log(` npx turbine generate ${dim('[options]')}`);
949
+ newline();
950
+ console.log(` Connects to your database, reads the schema, and generates:`);
951
+ console.log(` ${dim('•')} ${cyan('types.ts')} — Entity interfaces, Create/Update input types`);
952
+ console.log(` ${dim('•')} ${cyan('metadata.ts')} — Runtime schema metadata`);
953
+ console.log(` ${dim('•')} ${cyan('index.ts')} — Configured client with typed table accessors`);
954
+ newline();
955
+ console.log(` ${bold('Options:')}`);
956
+ console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
957
+ console.log(` ${cyan('--out, -o')} ${dim('<dir>')} Output directory ${dim('(default: ./generated/turbine)')}`);
958
+ console.log(` ${cyan('--schema, -s')} ${dim('<name>')} Postgres schema ${dim('(default: public)')}`);
959
+ console.log(` ${cyan('--include')} ${dim('<tables>')} Comma-separated tables to include`);
960
+ console.log(` ${cyan('--exclude')} ${dim('<tables>')} Comma-separated tables to exclude`);
961
+ newline();
962
+ }
963
+ function showPushHelp() {
964
+ banner();
965
+ console.log(` ${bold('turbine push')} — Apply schema-builder definitions to database`);
966
+ newline();
967
+ console.log(` ${bold('Usage:')}`);
968
+ console.log(` npx turbine push ${dim('[options]')}`);
969
+ newline();
970
+ console.log(` Reads your ${cyan('turbine/schema.ts')} file, diffs against the live database,`);
971
+ console.log(` and applies CREATE/ALTER statements.`);
972
+ newline();
973
+ console.log(` ${bold('Options:')}`);
974
+ console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
975
+ console.log(` ${cyan('--dry-run')} Show SQL without executing`);
976
+ console.log(` ${cyan('--verbose, -v')} Show detailed output`);
977
+ newline();
978
+ }
979
+ function showMigrateHelp() {
980
+ banner();
981
+ console.log(` ${bold('turbine migrate')} — SQL migration management`);
982
+ newline();
983
+ console.log(` ${bold('Usage:')}`);
984
+ console.log(` npx turbine migrate ${cyan('<subcommand>')} ${dim('[options]')}`);
985
+ newline();
986
+ console.log(` ${bold('Subcommands:')}`);
987
+ console.log(` ${cyan('create')} ${dim('<name>')} Create a new migration file`);
988
+ console.log(` ${cyan('up')} Apply pending migrations`);
989
+ console.log(` ${cyan('down')} Rollback last migration`);
990
+ console.log(` ${cyan('status')} Show applied/pending migrations`);
991
+ newline();
992
+ console.log(` ${bold('Options:')}`);
993
+ console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
994
+ console.log(` ${cyan('--step, -n')} ${dim('<N>')} Number of migrations to apply/rollback`);
995
+ console.log(` ${cyan('--dry-run')} Show SQL without executing`);
996
+ console.log(` ${cyan('--allow-drift')} Bypass checksum validation ${dim('(migrate up only — advanced)')}`);
997
+ console.log(` ${cyan('--verbose, -v')} Show detailed output`);
998
+ newline();
999
+ console.log(` ${bold('Examples:')}`);
1000
+ console.log(` ${dim('$')} npx turbine migrate create add_users_table`);
1001
+ console.log(` ${dim('$')} npx turbine migrate up`);
1002
+ console.log(` ${dim('$')} npx turbine migrate down --step 2`);
1003
+ console.log(` ${dim('$')} npx turbine migrate status`);
1004
+ newline();
1005
+ }
1006
+ function showSeedHelp() {
1007
+ banner();
1008
+ console.log(` ${bold('turbine seed')} — Run seed file`);
1009
+ newline();
1010
+ console.log(` ${bold('Usage:')}`);
1011
+ console.log(` npx turbine seed ${dim('[options]')}`);
1012
+ newline();
1013
+ console.log(` Runs the seed file specified in ${cyan('turbine.config.ts')}`);
1014
+ console.log(` ${dim('(default: ./turbine/seed.ts)')}`);
1015
+ newline();
1016
+ console.log(` ${bold('Options:')}`);
1017
+ console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
1018
+ newline();
1019
+ }
1020
+ function showStatusHelp() {
1021
+ banner();
1022
+ console.log(` ${bold('turbine status')} — Show database schema summary`);
1023
+ newline();
1024
+ console.log(` ${bold('Usage:')}`);
1025
+ console.log(` npx turbine status ${dim('[options]')}`);
1026
+ newline();
1027
+ console.log(` Introspects your database and displays tables, columns,`);
1028
+ console.log(` types, relations, and indexes.`);
1029
+ newline();
1030
+ console.log(` ${bold('Options:')}`);
1031
+ console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
1032
+ console.log(` ${cyan('--schema, -s')} ${dim('<name>')} Postgres schema ${dim('(default: public)')}`);
1033
+ newline();
1034
+ }
1035
+ // ---------------------------------------------------------------------------
785
1036
  // Help
786
1037
  // ---------------------------------------------------------------------------
787
1038
  function showHelp() {
@@ -840,10 +1091,27 @@ async function main() {
840
1091
  showHelp();
841
1092
  return;
842
1093
  }
1094
+ // Subcommand help: e.g. `turbine migrate --help`
1095
+ if (args.help) {
1096
+ if (showSubcommandHelp(args.command))
1097
+ return;
1098
+ showHelp();
1099
+ return;
1100
+ }
843
1101
  if (args.command === 'version' || args.command === '--version' || args.command === '-V') {
844
1102
  showVersion();
845
1103
  return;
846
1104
  }
1105
+ // If the user has a TypeScript config file, register the tsx ESM loader
1106
+ // before we attempt to import it. Otherwise Node throws
1107
+ // ERR_UNKNOWN_FILE_EXTENSION for `.ts`.
1108
+ const configPath = findConfigFile();
1109
+ if (needsTsLoader(configPath)) {
1110
+ const status = await registerTsLoader();
1111
+ if (status === 'missing' || status === 'unsupported') {
1112
+ failMissingTsLoader(configPath ?? 'turbine.config.ts', status);
1113
+ }
1114
+ }
847
1115
  // Load config file
848
1116
  let fileConfig = {};
849
1117
  try {
@@ -905,7 +1173,7 @@ async function main() {
905
1173
  if (err.message.includes('ECONNREFUSED') || err.message.includes('connection')) {
906
1174
  newline();
907
1175
  error(`Could not connect to database`);
908
- console.log(` ${dim(err.message)}`);
1176
+ console.log(` ${dim(redactUrl(err.message))}`);
909
1177
  newline();
910
1178
  console.log(` ${dim('Check that:')}`);
911
1179
  console.log(` ${dim('1.')} Your database is running`);
@@ -915,25 +1183,25 @@ async function main() {
915
1183
  else if (err.message.includes('authentication')) {
916
1184
  newline();
917
1185
  error(`Authentication failed`);
918
- console.log(` ${dim(err.message)}`);
1186
+ console.log(` ${dim(redactUrl(err.message))}`);
919
1187
  }
920
1188
  else if (err.message.includes('does not exist')) {
921
1189
  newline();
922
1190
  error(`Database or schema not found`);
923
- console.log(` ${dim(err.message)}`);
1191
+ console.log(` ${dim(redactUrl(err.message))}`);
924
1192
  }
925
1193
  else {
926
1194
  newline();
927
- error(err.message);
1195
+ error(redactUrl(err.message));
928
1196
  if (args.verbose && err.stack) {
929
1197
  newline();
930
- console.log(dim(err.stack));
1198
+ console.log(dim(redactUrl(err.stack)));
931
1199
  }
932
1200
  }
933
1201
  }
934
1202
  else {
935
1203
  newline();
936
- error(`Unexpected error: ${String(err)}`);
1204
+ error(`Unexpected error: ${redactUrl(String(err))}`);
937
1205
  }
938
1206
  newline();
939
1207
  process.exit(1);
@@ -0,0 +1,45 @@
1
+ /**
2
+ * turbine-orm CLI — TypeScript loader registration
3
+ *
4
+ * The CLI loads user-supplied config and schema files via dynamic `import()`.
5
+ * Plain Node has no built-in `.ts` loader, so importing `turbine.config.ts`
6
+ * blows up with `ERR_UNKNOWN_FILE_EXTENSION` unless we register a TypeScript
7
+ * loader first.
8
+ *
9
+ * Strategy:
10
+ * 1. If the file we're about to import ends in `.ts` / `.mts` / `.cts`,
11
+ * probe whether `tsx/esm` is resolvable from the user's CWD.
12
+ * 2. If yes, call `module.register('tsx/esm', ...)` ONCE per process.
13
+ * 3. If no, surface an actionable error telling the user to install `tsx`.
14
+ *
15
+ * `tsx` is intentionally NOT a runtime dependency — many projects already
16
+ * have it, and adding a heavy dev tool to a 1-dependency ORM would be silly.
17
+ */
18
+ /**
19
+ * Detect whether a config / schema file path needs the tsx ESM loader.
20
+ * Returns true for `.ts`, `.mts`, and `.cts` files; false for `.js`, `.mjs`,
21
+ * `.cjs`, `.json`, missing paths, or anything else.
22
+ */
23
+ export declare function needsTsLoader(filePath: string | null | undefined): boolean;
24
+ /**
25
+ * Probe whether `tsx/esm` is resolvable from the user's current working
26
+ * directory. Returns true if `tsx` is installed in the user's project.
27
+ *
28
+ * Accepts an injected `resolver` so unit tests don't need a real filesystem.
29
+ */
30
+ export declare function canResolveTsx(resolver?: (id: string) => string): boolean;
31
+ export type TsLoaderStatus = 'registered' | 'already' | 'unsupported' | 'missing';
32
+ /**
33
+ * Register the tsx ESM loader so subsequent dynamic imports of `.ts` files
34
+ * work. Safe to call multiple times — internal flag prevents double registration.
35
+ *
36
+ * Returns:
37
+ * - 'registered' loader was successfully registered this call
38
+ * - 'already' a loader was previously registered (idempotent)
39
+ * - 'unsupported' Node lacks `module.register()` (Node < 20.6)
40
+ * - 'missing' `tsx` is not installed in the user's project
41
+ */
42
+ export declare function registerTsLoader(): Promise<TsLoaderStatus>;
43
+ /** Reset the loader state — used by unit tests only. */
44
+ export declare function _resetTsLoaderStateForTests(): void;
45
+ //# sourceMappingURL=loader.d.ts.map