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.
- package/README.md +292 -26
- package/dist/cjs/cli/config.js +5 -15
- package/dist/cjs/cli/index.js +311 -43
- package/dist/cjs/cli/loader.js +129 -0
- package/dist/cjs/cli/migrate.js +96 -47
- package/dist/cjs/cli/ui.js +5 -9
- package/dist/cjs/client.js +158 -49
- package/dist/cjs/errors.js +424 -0
- package/dist/cjs/generate.js +145 -14
- package/dist/cjs/index.js +43 -20
- package/dist/cjs/introspect.js +3 -5
- package/dist/cjs/pipeline.js +9 -2
- package/dist/cjs/query.js +544 -115
- package/dist/cjs/schema-builder.js +150 -30
- package/dist/cjs/schema-sql.js +241 -37
- package/dist/cjs/schema.js +5 -2
- package/dist/cjs/serverless.js +88 -176
- package/dist/cli/config.js +6 -16
- package/dist/cli/index.js +316 -48
- package/dist/cli/loader.d.ts +45 -0
- package/dist/cli/loader.js +91 -0
- package/dist/cli/migrate.d.ts +13 -2
- package/dist/cli/migrate.js +97 -48
- package/dist/cli/ui.d.ts +1 -1
- package/dist/cli/ui.js +5 -9
- package/dist/client.d.ts +92 -4
- package/dist/client.js +158 -49
- package/dist/errors.d.ts +225 -0
- package/dist/errors.js +405 -0
- package/dist/generate.d.ts +7 -1
- package/dist/generate.js +148 -18
- package/dist/index.d.ts +11 -9
- package/dist/index.js +16 -12
- package/dist/introspect.d.ts +1 -1
- package/dist/introspect.js +4 -6
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +9 -2
- package/dist/query.d.ts +374 -38
- package/dist/query.js +545 -116
- package/dist/schema-builder.d.ts +38 -5
- package/dist/schema-builder.js +150 -31
- package/dist/schema-sql.d.ts +7 -3
- package/dist/schema-sql.js +241 -37
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +5 -2
- package/dist/serverless.d.ts +92 -139
- package/dist/serverless.js +87 -173
- package/package.json +33 -16
- package/dist/types.d.ts +0 -93
- 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,
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
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 {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
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
|
|
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://
|
|
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(
|
|
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
|
|
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'
|
|
418
|
-
|
|
419
|
-
|
|
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>')}
|
|
474
|
-
console.log(` ${cyan('
|
|
475
|
-
console.log(` ${cyan('
|
|
476
|
-
console.log(` ${cyan('
|
|
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('--
|
|
480
|
-
console.log(` ${cyan('--
|
|
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(
|
|
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
|
|
753
|
+
status = red(`${symbols.warning} Drifted`);
|
|
632
754
|
}
|
|
633
755
|
else if (s.applied) {
|
|
634
|
-
status = green(symbols.check
|
|
756
|
+
status = green(`${symbols.check} Applied`);
|
|
635
757
|
}
|
|
636
758
|
else {
|
|
637
|
-
status = yellow(symbols.dot
|
|
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
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
807
|
+
execFileSync(runner.cmd, runner.args, {
|
|
685
808
|
stdio: 'inherit',
|
|
686
809
|
env: {
|
|
687
810
|
...process.env,
|
|
688
|
-
DATABASE_URL: config.url || process.env
|
|
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(
|
|
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
|
|
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
|
|
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('@
|
|
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
|