turbine-orm 0.3.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.
- package/LICENSE +21 -0
- package/README.md +295 -0
- package/dist/cli/config.d.ts +58 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +123 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +23 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +935 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/migrate.d.ts +94 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/migrate.js +383 -0
- package/dist/cli/migrate.js.map +1 -0
- package/dist/cli/ui.d.ts +74 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +220 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/client.d.ts +212 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +423 -0
- package/dist/client.js.map +1 -0
- package/dist/generate.d.ts +24 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +289 -0
- package/dist/generate.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/introspect.d.ts +22 -0
- package/dist/introspect.d.ts.map +1 -0
- package/dist/introspect.js +284 -0
- package/dist/introspect.js.map +1 -0
- package/dist/pipeline.d.ts +44 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +69 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/query.d.ts +342 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +1396 -0
- package/dist/query.js.map +1 -0
- package/dist/schema-builder.d.ts +127 -0
- package/dist/schema-builder.d.ts.map +1 -0
- package/dist/schema-builder.js +164 -0
- package/dist/schema-builder.js.map +1 -0
- package/dist/schema-sql.d.ts +71 -0
- package/dist/schema-sql.d.ts.map +1 -0
- package/dist/schema-sql.js +347 -0
- package/dist/schema-sql.js.map +1 -0
- package/dist/schema.d.ts +90 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +129 -0
- package/dist/schema.js.map +1 -0
- package/dist/serverless.d.ts +162 -0
- package/dist/serverless.d.ts.map +1 -0
- package/dist/serverless.js +195 -0
- package/dist/serverless.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +126 -0
- package/dist/types.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @batadata/turbine CLI
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* turbine init — Initialize a Turbine project
|
|
7
|
+
* turbine generate | pull — Introspect database and generate TypeScript types
|
|
8
|
+
* turbine push — Apply schema-builder definitions to database
|
|
9
|
+
* turbine migrate create <name> — Create a new SQL migration file
|
|
10
|
+
* turbine migrate up — Apply pending migrations
|
|
11
|
+
* turbine migrate down — Rollback last migration
|
|
12
|
+
* turbine migrate status — Show migration status
|
|
13
|
+
* turbine seed — Run seed file
|
|
14
|
+
* turbine status — Show schema summary
|
|
15
|
+
* turbine studio — Launch web UI (coming soon)
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* DATABASE_URL=postgres://... npx turbine generate
|
|
19
|
+
* npx turbine init --url postgres://...
|
|
20
|
+
* npx turbine migrate create add_users_table
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync } from 'node:fs';
|
|
23
|
+
import { resolve, relative } from 'node:path';
|
|
24
|
+
import { introspect } from '../introspect.js';
|
|
25
|
+
import { generate } from '../generate.js';
|
|
26
|
+
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';
|
|
31
|
+
function parseArgs() {
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
const result = {
|
|
34
|
+
command: args[0] ?? 'help',
|
|
35
|
+
positional: [],
|
|
36
|
+
};
|
|
37
|
+
let i = 1;
|
|
38
|
+
// Check for subcommand (e.g. "migrate create")
|
|
39
|
+
if (i < args.length && args[i] && !args[i].startsWith('-')) {
|
|
40
|
+
result.subcommand = args[i];
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
for (; i < args.length; i++) {
|
|
44
|
+
const arg = args[i];
|
|
45
|
+
const next = args[i + 1];
|
|
46
|
+
switch (arg) {
|
|
47
|
+
case '--url':
|
|
48
|
+
case '-u':
|
|
49
|
+
result.url = next;
|
|
50
|
+
i++;
|
|
51
|
+
break;
|
|
52
|
+
case '--out':
|
|
53
|
+
case '-o':
|
|
54
|
+
result.out = next;
|
|
55
|
+
i++;
|
|
56
|
+
break;
|
|
57
|
+
case '--schema':
|
|
58
|
+
case '-s':
|
|
59
|
+
result.schema = next;
|
|
60
|
+
i++;
|
|
61
|
+
break;
|
|
62
|
+
case '--include':
|
|
63
|
+
result.include = next?.split(',');
|
|
64
|
+
i++;
|
|
65
|
+
break;
|
|
66
|
+
case '--exclude':
|
|
67
|
+
result.exclude = next?.split(',');
|
|
68
|
+
i++;
|
|
69
|
+
break;
|
|
70
|
+
case '--step':
|
|
71
|
+
case '-n':
|
|
72
|
+
result.step = next ? parseInt(next, 10) : undefined;
|
|
73
|
+
i++;
|
|
74
|
+
break;
|
|
75
|
+
case '--dry-run':
|
|
76
|
+
result.dryRun = true;
|
|
77
|
+
break;
|
|
78
|
+
case '--force':
|
|
79
|
+
case '-f':
|
|
80
|
+
result.force = true;
|
|
81
|
+
break;
|
|
82
|
+
case '--verbose':
|
|
83
|
+
case '-v':
|
|
84
|
+
result.verbose = true;
|
|
85
|
+
break;
|
|
86
|
+
default:
|
|
87
|
+
if (!arg.startsWith('-')) {
|
|
88
|
+
result.positional.push(arg);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Helpers
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
function requireUrl(config) {
|
|
99
|
+
if (!config.url) {
|
|
100
|
+
error('No database URL provided.');
|
|
101
|
+
newline();
|
|
102
|
+
console.log(` ${dim('Set it in one of these ways:')}`);
|
|
103
|
+
console.log(` ${dim('1.')} Add ${cyan('url')} to ${cyan('turbine.config.ts')}`);
|
|
104
|
+
console.log(` ${dim('2.')} Set ${cyan('DATABASE_URL')} environment variable`);
|
|
105
|
+
console.log(` ${dim('3.')} Pass ${cyan('--url')} flag`);
|
|
106
|
+
newline();
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
return config.url;
|
|
110
|
+
}
|
|
111
|
+
async function loadSchemaFile(schemaFile) {
|
|
112
|
+
const absPath = resolve(schemaFile);
|
|
113
|
+
if (!existsSync(absPath)) {
|
|
114
|
+
error(`Schema file not found: ${schemaFile}`);
|
|
115
|
+
console.log(` ${dim('Create one with:')} ${cyan('turbine init')}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const fileUrl = pathToFileURL(absPath).href;
|
|
120
|
+
const mod = await import(fileUrl);
|
|
121
|
+
const schema = mod.default ?? mod;
|
|
122
|
+
if (!schema.tables) {
|
|
123
|
+
error('Schema file must export a SchemaDef with a "tables" property.');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
return schema;
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
error(`Failed to load schema file: ${schemaFile}`);
|
|
130
|
+
if (err instanceof Error) {
|
|
131
|
+
console.log(` ${dim(err.message)}`);
|
|
132
|
+
}
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Command: init
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
async function cmdInit(args, config) {
|
|
140
|
+
banner();
|
|
141
|
+
header('Initializing Turbine project');
|
|
142
|
+
// Detect environment
|
|
143
|
+
const envUrl = process.env['DATABASE_URL'];
|
|
144
|
+
const hasEnvFile = existsSync('.env');
|
|
145
|
+
const hasEnvLocal = existsSync('.env.local');
|
|
146
|
+
if (envUrl) {
|
|
147
|
+
success(`Detected ${cyan('DATABASE_URL')} in environment`);
|
|
148
|
+
}
|
|
149
|
+
else if (hasEnvLocal) {
|
|
150
|
+
info(`Found ${cyan('.env.local')} — Turbine will use ${cyan('DATABASE_URL')} from it if set`);
|
|
151
|
+
}
|
|
152
|
+
else if (hasEnvFile) {
|
|
153
|
+
info(`Found ${cyan('.env')} — Turbine will use ${cyan('DATABASE_URL')} from it if set`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
info(`No ${cyan('DATABASE_URL')} found in environment`);
|
|
157
|
+
}
|
|
158
|
+
newline();
|
|
159
|
+
const configPath = findConfigFile();
|
|
160
|
+
// Create config file
|
|
161
|
+
if (configPath && !args.force) {
|
|
162
|
+
warn(`Config file already exists: ${dim(configPath)}`);
|
|
163
|
+
console.log(` ${dim('Run with')} ${cyan('--force')} ${dim('to overwrite')}`);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const urlForConfig = args.url ?? undefined;
|
|
167
|
+
const configContent = configTemplate(urlForConfig);
|
|
168
|
+
writeFileSync('turbine.config.ts', configContent, 'utf-8');
|
|
169
|
+
if (configPath) {
|
|
170
|
+
success(`Overwrote ${cyan('turbine.config.ts')}`);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
success(`Created ${cyan('turbine.config.ts')}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Create migrations directory
|
|
177
|
+
const migrDir = config.migrationsDir;
|
|
178
|
+
if (!existsSync(migrDir)) {
|
|
179
|
+
mkdirSync(migrDir, { recursive: true });
|
|
180
|
+
// Create .gitkeep
|
|
181
|
+
writeFileSync(`${migrDir}/.gitkeep`, '', 'utf-8');
|
|
182
|
+
success(`Created ${cyan(migrDir + '/')}`);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
info(`Migrations dir already exists: ${dim(migrDir)}`);
|
|
186
|
+
}
|
|
187
|
+
// Create output directory
|
|
188
|
+
if (!existsSync(config.out)) {
|
|
189
|
+
mkdirSync(config.out, { recursive: true });
|
|
190
|
+
success(`Created ${cyan(config.out + '/')}`);
|
|
191
|
+
}
|
|
192
|
+
// Create seed file template
|
|
193
|
+
const seedDir = config.seedFile.substring(0, config.seedFile.lastIndexOf('/'));
|
|
194
|
+
if (!existsSync(config.seedFile)) {
|
|
195
|
+
if (!existsSync(seedDir)) {
|
|
196
|
+
mkdirSync(seedDir, { recursive: true });
|
|
197
|
+
}
|
|
198
|
+
writeFileSync(config.seedFile, `/**
|
|
199
|
+
* Turbine seed file
|
|
200
|
+
*
|
|
201
|
+
* Run with: npx turbine seed
|
|
202
|
+
*/
|
|
203
|
+
|
|
204
|
+
// import { turbine } from '${config.out.replace('./', '')}';
|
|
205
|
+
//
|
|
206
|
+
// const db = turbine({ connectionString: process.env.DATABASE_URL });
|
|
207
|
+
//
|
|
208
|
+
// async function seed() {
|
|
209
|
+
// console.log('Seeding database...');
|
|
210
|
+
//
|
|
211
|
+
// // Add your seed data here:
|
|
212
|
+
// // await db.users.create({ data: { email: 'admin@example.com', name: 'Admin' } });
|
|
213
|
+
//
|
|
214
|
+
// console.log('Done!');
|
|
215
|
+
// await db.disconnect();
|
|
216
|
+
// }
|
|
217
|
+
//
|
|
218
|
+
// seed();
|
|
219
|
+
`, 'utf-8');
|
|
220
|
+
success(`Created ${cyan(config.seedFile)}`);
|
|
221
|
+
}
|
|
222
|
+
// Create schema builder template
|
|
223
|
+
if (!existsSync(config.schemaFile)) {
|
|
224
|
+
const schemaDir = config.schemaFile.substring(0, config.schemaFile.lastIndexOf('/'));
|
|
225
|
+
if (!existsSync(schemaDir)) {
|
|
226
|
+
mkdirSync(schemaDir, { recursive: true });
|
|
227
|
+
}
|
|
228
|
+
writeFileSync(config.schemaFile, `/**
|
|
229
|
+
* Turbine schema definition
|
|
230
|
+
*
|
|
231
|
+
* Define your database schema in TypeScript.
|
|
232
|
+
* Use \`npx turbine push\` to sync it to your database.
|
|
233
|
+
*
|
|
234
|
+
* @see https://batadata.com/docs/turbine/schema
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
import { defineSchema, table, column } from '@batadata/turbine';
|
|
238
|
+
|
|
239
|
+
export default defineSchema({
|
|
240
|
+
// Example:
|
|
241
|
+
// users: table({
|
|
242
|
+
// id: column.serial().primaryKey(),
|
|
243
|
+
// email: column.text().unique().notNull(),
|
|
244
|
+
// name: column.text().notNull(),
|
|
245
|
+
// createdAt: column.timestamp().default('now()'),
|
|
246
|
+
// }),
|
|
247
|
+
});
|
|
248
|
+
`, 'utf-8');
|
|
249
|
+
success(`Created ${cyan(config.schemaFile)}`);
|
|
250
|
+
}
|
|
251
|
+
// Add .gitignore entry for generated output
|
|
252
|
+
const gitignorePath = '.gitignore';
|
|
253
|
+
if (existsSync(gitignorePath)) {
|
|
254
|
+
const gitignoreContent = readFileSync(gitignorePath, 'utf-8');
|
|
255
|
+
if (!gitignoreContent.includes('generated/turbine')) {
|
|
256
|
+
appendFileSync(gitignorePath, '\n# Turbine generated client\ngenerated/turbine/\n');
|
|
257
|
+
success(`Added ${cyan('generated/turbine/')} to ${cyan('.gitignore')}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// If we have a URL, run initial generate
|
|
261
|
+
const url = args.url ?? envUrl ?? config.url;
|
|
262
|
+
if (url) {
|
|
263
|
+
newline();
|
|
264
|
+
divider();
|
|
265
|
+
newline();
|
|
266
|
+
const spinner = new Spinner('Introspecting database').start();
|
|
267
|
+
try {
|
|
268
|
+
const schema = await introspect({
|
|
269
|
+
connectionString: url,
|
|
270
|
+
schema: config.schema,
|
|
271
|
+
include: config.include.length ? config.include : undefined,
|
|
272
|
+
exclude: config.exclude.length ? config.exclude : undefined,
|
|
273
|
+
});
|
|
274
|
+
const tableCount = Object.keys(schema.tables).length;
|
|
275
|
+
spinner.succeed(`Found ${bold(String(tableCount))} tables`);
|
|
276
|
+
const genSpinner = new Spinner('Generating TypeScript client').start();
|
|
277
|
+
const result = generate({ schema, outDir: config.out, connectionString: url });
|
|
278
|
+
genSpinner.succeed(`Generated ${bold(String(result.files.length))} files to ${cyan(config.out + '/')}`);
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
spinner.fail('Could not connect to database');
|
|
282
|
+
if (err instanceof Error) {
|
|
283
|
+
console.log(` ${dim(err.message)}`);
|
|
284
|
+
}
|
|
285
|
+
newline();
|
|
286
|
+
info('You can run generation later with: ' + cyan('npx turbine generate'));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Next steps
|
|
290
|
+
newline();
|
|
291
|
+
divider();
|
|
292
|
+
newline();
|
|
293
|
+
console.log(` ${bold('Next steps:')}`);
|
|
294
|
+
newline();
|
|
295
|
+
if (!url) {
|
|
296
|
+
console.log(` ${dim('1.')} Set your database URL in ${cyan('turbine.config.ts')}`);
|
|
297
|
+
if (!hasEnvFile && !hasEnvLocal) {
|
|
298
|
+
console.log(` ${dim('or create a')} ${cyan('.env')} ${dim('file with')} ${cyan('DATABASE_URL=postgres://...')}`);
|
|
299
|
+
}
|
|
300
|
+
console.log(` ${dim('2.')} Run ${cyan('npx turbine generate')} to introspect your DB`);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
console.log(` ${dim('1.')} Import the generated client:`);
|
|
304
|
+
console.log(` ${cyan(`import { turbine } from './${config.out.replace('./', '')}';`)}`);
|
|
305
|
+
newline();
|
|
306
|
+
console.log(` ${dim('2.')} Create a connection and query:`);
|
|
307
|
+
console.log(` ${dim('const db = turbine();')}`);
|
|
308
|
+
console.log(` ${dim('const users = await db.users.findMany();')}`);
|
|
309
|
+
}
|
|
310
|
+
newline();
|
|
311
|
+
console.log(` ${dim('3.')} Create migrations: ${cyan('npx turbine migrate create <name>')}`);
|
|
312
|
+
console.log(` ${dim('4.')} Run migrations: ${cyan('npx turbine migrate up')}`);
|
|
313
|
+
console.log(` ${dim('5.')} Seed your database: ${cyan('npx turbine seed')}`);
|
|
314
|
+
newline();
|
|
315
|
+
}
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Command: generate (pull)
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
async function cmdGenerate(args, config) {
|
|
320
|
+
banner();
|
|
321
|
+
const url = requireUrl(config);
|
|
322
|
+
const startTime = performance.now();
|
|
323
|
+
label('Database', redactUrl(url));
|
|
324
|
+
label('Schema', config.schema);
|
|
325
|
+
label('Output', config.out);
|
|
326
|
+
newline();
|
|
327
|
+
// Introspect
|
|
328
|
+
const spinner = new Spinner('Introspecting database schema').start();
|
|
329
|
+
const schema = await introspect({
|
|
330
|
+
connectionString: url,
|
|
331
|
+
schema: config.schema,
|
|
332
|
+
include: config.include.length ? config.include : undefined,
|
|
333
|
+
exclude: config.exclude.length ? config.exclude : undefined,
|
|
334
|
+
});
|
|
335
|
+
const tableNames = Object.keys(schema.tables);
|
|
336
|
+
const totalColumns = Object.values(schema.tables).reduce((sum, t) => sum + t.columns.length, 0);
|
|
337
|
+
const totalRelations = Object.values(schema.tables).reduce((sum, t) => sum + Object.keys(t.relations).length, 0);
|
|
338
|
+
spinner.succeed(`Found ${bold(String(tableNames.length))} tables, ${bold(String(totalColumns))} columns, ${bold(String(totalRelations))} relations`);
|
|
339
|
+
// Print table summary
|
|
340
|
+
if (args.verbose) {
|
|
341
|
+
newline();
|
|
342
|
+
for (const tbl of Object.values(schema.tables)) {
|
|
343
|
+
const relCount = Object.keys(tbl.relations).length;
|
|
344
|
+
const pk = tbl.primaryKey.join(', ') || '(none)';
|
|
345
|
+
console.log(` ${symbols.tee} ${bold(tbl.name)} ${dim(`${tbl.columns.length} cols, PK: ${pk}`)}${relCount > 0 ? dim(`, ${relCount} rels`) : ''}`);
|
|
346
|
+
}
|
|
347
|
+
newline();
|
|
348
|
+
}
|
|
349
|
+
if (Object.keys(schema.enums).length > 0) {
|
|
350
|
+
info(`Enums: ${Object.keys(schema.enums).join(', ')}`);
|
|
351
|
+
}
|
|
352
|
+
// Generate
|
|
353
|
+
const genSpinner = new Spinner('Generating TypeScript client').start();
|
|
354
|
+
const result = generate({
|
|
355
|
+
schema,
|
|
356
|
+
outDir: config.out,
|
|
357
|
+
connectionString: url,
|
|
358
|
+
});
|
|
359
|
+
genSpinner.succeed(`Generated ${bold(String(result.files.length))} files in ${elapsed(startTime)}`);
|
|
360
|
+
// List files
|
|
361
|
+
for (const file of result.files) {
|
|
362
|
+
console.log(` ${dim(symbols.teeEnd)} ${cyan(result.outDir + '/' + file)}`);
|
|
363
|
+
}
|
|
364
|
+
// Usage hint
|
|
365
|
+
newline();
|
|
366
|
+
divider();
|
|
367
|
+
newline();
|
|
368
|
+
console.log(` ${bold('Usage:')}`);
|
|
369
|
+
newline();
|
|
370
|
+
console.log(` ${cyan(`import { turbine } from './${config.out.replace('./', '')}';`)}`);
|
|
371
|
+
console.log(` ${dim('const db = turbine({ connectionString: process.env.DATABASE_URL });')}`);
|
|
372
|
+
console.log(` ${dim('const user = await db.users.findUnique({ where: { id: 1 } });')}`);
|
|
373
|
+
newline();
|
|
374
|
+
}
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// Command: push
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
async function cmdPush(args, config) {
|
|
379
|
+
banner();
|
|
380
|
+
const url = requireUrl(config);
|
|
381
|
+
label('Database', redactUrl(url));
|
|
382
|
+
label('Schema file', config.schemaFile);
|
|
383
|
+
newline();
|
|
384
|
+
const schemaDef = await loadSchemaFile(config.schemaFile);
|
|
385
|
+
const tableCount = Object.keys(schemaDef.tables).length;
|
|
386
|
+
info(`Schema defines ${bold(String(tableCount))} tables`);
|
|
387
|
+
// Compute diff
|
|
388
|
+
const diffSpinner = new Spinner('Computing schema diff').start();
|
|
389
|
+
const diff = await schemaDiff(schemaDef, url);
|
|
390
|
+
if (diff.statements.length === 0 && diff.drop.length === 0) {
|
|
391
|
+
diffSpinner.succeed('Database is already in sync');
|
|
392
|
+
newline();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
diffSpinner.succeed('Found changes');
|
|
396
|
+
newline();
|
|
397
|
+
// Show what will happen
|
|
398
|
+
if (diff.create.length > 0) {
|
|
399
|
+
console.log(` ${green('+ Create')} ${bold(String(diff.create.length))} table(s):`);
|
|
400
|
+
for (const t of diff.create) {
|
|
401
|
+
console.log(` ${green(symbols.arrowRight)} ${t.name}`);
|
|
402
|
+
}
|
|
403
|
+
newline();
|
|
404
|
+
}
|
|
405
|
+
if (diff.alter.length > 0) {
|
|
406
|
+
console.log(` ${yellow('~ Alter')} ${bold(String(diff.alter.length))} table(s):`);
|
|
407
|
+
for (const a of diff.alter) {
|
|
408
|
+
console.log(` ${yellow(symbols.arrowRight)} ${a.table}`);
|
|
409
|
+
for (const col of a.columns) {
|
|
410
|
+
const actionLabel = col.action === 'add' ? green('+ add') :
|
|
411
|
+
col.action === 'drop' ? red('- drop') :
|
|
412
|
+
yellow('~ ' + col.action.replace('_', ' '));
|
|
413
|
+
console.log(` ${actionLabel} ${col.column}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
newline();
|
|
417
|
+
}
|
|
418
|
+
if (diff.drop.length > 0) {
|
|
419
|
+
console.log(` ${red('- Extra tables')} in database (not in schema):`);
|
|
420
|
+
for (const t of diff.drop) {
|
|
421
|
+
console.log(` ${dim(symbols.arrowRight)} ${t} ${dim('(not dropped automatically)')}`);
|
|
422
|
+
}
|
|
423
|
+
newline();
|
|
424
|
+
}
|
|
425
|
+
// Show SQL
|
|
426
|
+
if (diff.statements.length > 0) {
|
|
427
|
+
console.log(` ${bold('SQL to execute:')}`);
|
|
428
|
+
newline();
|
|
429
|
+
for (const stmt of diff.statements) {
|
|
430
|
+
for (const line of stmt.split('\n')) {
|
|
431
|
+
console.log(` ${dim(symbols.vertLine)} ${cyan(line)}`);
|
|
432
|
+
}
|
|
433
|
+
console.log(` ${dim(symbols.vertLine)}`);
|
|
434
|
+
}
|
|
435
|
+
newline();
|
|
436
|
+
}
|
|
437
|
+
if (args.dryRun) {
|
|
438
|
+
info('Dry run — no changes applied.');
|
|
439
|
+
newline();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
// Execute
|
|
443
|
+
const pushSpinner = new Spinner('Applying changes').start();
|
|
444
|
+
const result = await schemaPush(schemaDef, url);
|
|
445
|
+
pushSpinner.succeed(`Applied ${bold(String(result.statementsExecuted))} statement(s)`);
|
|
446
|
+
if (result.tablesCreated.length > 0) {
|
|
447
|
+
success(`Created: ${result.tablesCreated.join(', ')}`);
|
|
448
|
+
}
|
|
449
|
+
if (result.tablesAltered.length > 0) {
|
|
450
|
+
success(`Altered: ${result.tablesAltered.join(', ')}`);
|
|
451
|
+
}
|
|
452
|
+
newline();
|
|
453
|
+
info(`Run ${cyan('npx turbine generate')} to update your TypeScript types.`);
|
|
454
|
+
newline();
|
|
455
|
+
}
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
// Command: migrate
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
async function cmdMigrate(args, config) {
|
|
460
|
+
const sub = args.subcommand;
|
|
461
|
+
if (!sub || sub === 'help') {
|
|
462
|
+
banner();
|
|
463
|
+
console.log(` ${bold('turbine migrate')} ${dim('— SQL-first migration system')}`);
|
|
464
|
+
newline();
|
|
465
|
+
console.log(` ${bold('Commands:')}`);
|
|
466
|
+
console.log(` ${cyan('create <name>')} Create a new migration file`);
|
|
467
|
+
console.log(` ${cyan('up')} Apply pending migrations`);
|
|
468
|
+
console.log(` ${cyan('down')} Rollback last migration`);
|
|
469
|
+
console.log(` ${cyan('status')} Show migration status`);
|
|
470
|
+
newline();
|
|
471
|
+
console.log(` ${bold('Options:')}`);
|
|
472
|
+
console.log(` ${cyan('--step, -n')} Number of migrations to apply/rollback`);
|
|
473
|
+
console.log(` ${cyan('--dry-run')} Show SQL without executing`);
|
|
474
|
+
newline();
|
|
475
|
+
console.log(` ${bold('Examples:')}`);
|
|
476
|
+
console.log(` ${dim('npx turbine migrate create add_users_table')}`);
|
|
477
|
+
console.log(` ${dim('npx turbine migrate up')}`);
|
|
478
|
+
console.log(` ${dim('npx turbine migrate down --step 2')}`);
|
|
479
|
+
newline();
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
switch (sub) {
|
|
483
|
+
case 'create':
|
|
484
|
+
await cmdMigrateCreate(args, config);
|
|
485
|
+
break;
|
|
486
|
+
case 'up':
|
|
487
|
+
await cmdMigrateUp(args, config);
|
|
488
|
+
break;
|
|
489
|
+
case 'down':
|
|
490
|
+
await cmdMigrateDown(args, config);
|
|
491
|
+
break;
|
|
492
|
+
case 'status':
|
|
493
|
+
case 'list':
|
|
494
|
+
await cmdMigrateStatus(args, config);
|
|
495
|
+
break;
|
|
496
|
+
default:
|
|
497
|
+
error(`Unknown migrate subcommand: ${sub}`);
|
|
498
|
+
console.log(` ${dim('Run')} ${cyan('npx turbine migrate help')} ${dim('for usage.')}`);
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async function cmdMigrateCreate(args, config) {
|
|
503
|
+
banner();
|
|
504
|
+
const name = args.positional[0];
|
|
505
|
+
if (!name) {
|
|
506
|
+
error('Migration name is required.');
|
|
507
|
+
newline();
|
|
508
|
+
console.log(` ${dim('Usage:')} ${cyan('npx turbine migrate create <name>')}`);
|
|
509
|
+
console.log(` ${dim('Example:')} ${cyan('npx turbine migrate create add_users_table')}`);
|
|
510
|
+
newline();
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
const file = createMigration(config.migrationsDir, name);
|
|
514
|
+
const relPath = relative(process.cwd(), file.path);
|
|
515
|
+
success(`Created migration: ${bold(file.filename)}`);
|
|
516
|
+
newline();
|
|
517
|
+
console.log(` ${dim('File:')} ${cyan(relPath)}`);
|
|
518
|
+
newline();
|
|
519
|
+
console.log(` ${dim('Edit the file to add your SQL, then run:')}`);
|
|
520
|
+
console.log(` ${cyan('npx turbine migrate up')}`);
|
|
521
|
+
newline();
|
|
522
|
+
}
|
|
523
|
+
async function cmdMigrateUp(args, config) {
|
|
524
|
+
banner();
|
|
525
|
+
const url = requireUrl(config);
|
|
526
|
+
label('Database', redactUrl(url));
|
|
527
|
+
label('Migrations', config.migrationsDir);
|
|
528
|
+
newline();
|
|
529
|
+
const allFiles = listMigrationFiles(config.migrationsDir);
|
|
530
|
+
if (allFiles.length === 0) {
|
|
531
|
+
warn('No migration files found.');
|
|
532
|
+
console.log(` ${dim('Create one with:')} ${cyan('npx turbine migrate create <name>')}`);
|
|
533
|
+
newline();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const spinner = new Spinner('Applying migrations').start();
|
|
537
|
+
const result = await migrateUp(url, config.migrationsDir, {
|
|
538
|
+
step: args.step,
|
|
539
|
+
});
|
|
540
|
+
if (result.applied.length === 0 && result.errors.length === 0) {
|
|
541
|
+
spinner.succeed('All migrations are up to date');
|
|
542
|
+
newline();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (result.applied.length > 0) {
|
|
546
|
+
spinner.succeed(`Applied ${bold(String(result.applied.length))} migration(s)`);
|
|
547
|
+
for (const file of result.applied) {
|
|
548
|
+
console.log(` ${green(symbols.check)} ${file.filename}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (result.errors.length > 0) {
|
|
552
|
+
spinner.fail('Migration failed');
|
|
553
|
+
for (const { file, error: msg } of result.errors) {
|
|
554
|
+
console.log(` ${red(symbols.cross)} ${file.filename}`);
|
|
555
|
+
console.log(` ${dim(msg)}`);
|
|
556
|
+
}
|
|
557
|
+
newline();
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
newline();
|
|
561
|
+
}
|
|
562
|
+
async function cmdMigrateDown(args, config) {
|
|
563
|
+
banner();
|
|
564
|
+
const url = requireUrl(config);
|
|
565
|
+
label('Database', redactUrl(url));
|
|
566
|
+
label('Migrations', config.migrationsDir);
|
|
567
|
+
newline();
|
|
568
|
+
const spinner = new Spinner('Rolling back migration(s)').start();
|
|
569
|
+
const result = await migrateDown(url, config.migrationsDir, {
|
|
570
|
+
step: args.step ?? 1,
|
|
571
|
+
});
|
|
572
|
+
if (result.rolledBack.length === 0 && result.errors.length === 0) {
|
|
573
|
+
spinner.succeed('No migrations to roll back');
|
|
574
|
+
newline();
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (result.rolledBack.length > 0) {
|
|
578
|
+
spinner.succeed(`Rolled back ${bold(String(result.rolledBack.length))} migration(s)`);
|
|
579
|
+
for (const file of result.rolledBack) {
|
|
580
|
+
console.log(` ${yellow(symbols.arrowRight)} ${file.filename}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (result.errors.length > 0) {
|
|
584
|
+
spinner.fail('Rollback failed');
|
|
585
|
+
for (const { file, error: msg } of result.errors) {
|
|
586
|
+
console.log(` ${red(symbols.cross)} ${file.filename}`);
|
|
587
|
+
console.log(` ${dim(msg)}`);
|
|
588
|
+
}
|
|
589
|
+
newline();
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
newline();
|
|
593
|
+
}
|
|
594
|
+
async function cmdMigrateStatus(args, config) {
|
|
595
|
+
banner();
|
|
596
|
+
const url = requireUrl(config);
|
|
597
|
+
label('Database', redactUrl(url));
|
|
598
|
+
label('Migrations', config.migrationsDir);
|
|
599
|
+
newline();
|
|
600
|
+
const allFiles = listMigrationFiles(config.migrationsDir);
|
|
601
|
+
if (allFiles.length === 0) {
|
|
602
|
+
warn('No migration files found.');
|
|
603
|
+
console.log(` ${dim('Create one with:')} ${cyan('npx turbine migrate create <name>')}`);
|
|
604
|
+
newline();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const statuses = await migrateStatus(url, config.migrationsDir);
|
|
608
|
+
const appliedCount = statuses.filter((s) => s.applied).length;
|
|
609
|
+
const pendingCount = statuses.filter((s) => !s.applied).length;
|
|
610
|
+
info(`${bold(String(appliedCount))} applied, ${pendingCount > 0 ? yellow(bold(String(pendingCount))) : bold(String(pendingCount))} pending`);
|
|
611
|
+
newline();
|
|
612
|
+
// Check for checksum mismatches
|
|
613
|
+
const driftCount = statuses.filter((s) => s.checksumValid === false).length;
|
|
614
|
+
if (driftCount > 0) {
|
|
615
|
+
warn(`${bold(String(driftCount))} migration(s) have been modified after application!`);
|
|
616
|
+
console.log(` ${dim('Applied migrations should be immutable. Modifying them can cause drift.')}`);
|
|
617
|
+
newline();
|
|
618
|
+
}
|
|
619
|
+
// Format as table
|
|
620
|
+
const headers = ['Status', 'Migration', 'Applied at'];
|
|
621
|
+
const rows = statuses.map((s) => {
|
|
622
|
+
let status;
|
|
623
|
+
if (s.applied && s.checksumValid === false) {
|
|
624
|
+
status = red(symbols.warning + ' Drifted');
|
|
625
|
+
}
|
|
626
|
+
else if (s.applied) {
|
|
627
|
+
status = green(symbols.check + ' Applied');
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
status = yellow(symbols.dot + ' Pending');
|
|
631
|
+
}
|
|
632
|
+
return [
|
|
633
|
+
status,
|
|
634
|
+
s.file.filename,
|
|
635
|
+
s.appliedAt
|
|
636
|
+
? dim(s.appliedAt.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'))
|
|
637
|
+
: dim('—'),
|
|
638
|
+
];
|
|
639
|
+
});
|
|
640
|
+
console.log(formatTable(headers, rows));
|
|
641
|
+
newline();
|
|
642
|
+
if (pendingCount > 0) {
|
|
643
|
+
console.log(` ${dim('Run')} ${cyan('npx turbine migrate up')} ${dim('to apply pending migrations.')}`);
|
|
644
|
+
newline();
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
// ---------------------------------------------------------------------------
|
|
648
|
+
// Command: seed
|
|
649
|
+
// ---------------------------------------------------------------------------
|
|
650
|
+
async function cmdSeed(args, config) {
|
|
651
|
+
banner();
|
|
652
|
+
const seedFile = resolve(config.seedFile);
|
|
653
|
+
label('Seed file', config.seedFile);
|
|
654
|
+
newline();
|
|
655
|
+
if (!existsSync(seedFile)) {
|
|
656
|
+
error(`Seed file not found: ${config.seedFile}`);
|
|
657
|
+
newline();
|
|
658
|
+
console.log(` ${dim('Create one with:')} ${cyan('npx turbine init')}`);
|
|
659
|
+
console.log(` ${dim('Or set a custom path in')} ${cyan('turbine.config.ts')}`);
|
|
660
|
+
newline();
|
|
661
|
+
process.exit(1);
|
|
662
|
+
}
|
|
663
|
+
const spinner = new Spinner('Running seed file').start();
|
|
664
|
+
try {
|
|
665
|
+
// Use child_process to run the seed file via tsx or node
|
|
666
|
+
const { execSync } = await import('node:child_process');
|
|
667
|
+
// Try tsx first (most compatible with .ts files), fall back to node --experimental-strip-types
|
|
668
|
+
const runners = [
|
|
669
|
+
{ cmd: 'npx tsx', name: 'tsx' },
|
|
670
|
+
{ cmd: 'node --experimental-strip-types', name: 'node' },
|
|
671
|
+
];
|
|
672
|
+
let ran = false;
|
|
673
|
+
for (const runner of runners) {
|
|
674
|
+
try {
|
|
675
|
+
execSync(`${runner.cmd} ${seedFile}`, {
|
|
676
|
+
stdio: 'inherit',
|
|
677
|
+
env: {
|
|
678
|
+
...process.env,
|
|
679
|
+
DATABASE_URL: config.url || process.env['DATABASE_URL'],
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
ran = true;
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
catch (err) {
|
|
686
|
+
// If tsx not found, try next runner
|
|
687
|
+
if (err instanceof Error && 'status' in err && err.status === null) {
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
throw err;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (!ran) {
|
|
694
|
+
throw new Error('Could not find tsx or compatible Node.js version to run .ts files');
|
|
695
|
+
}
|
|
696
|
+
spinner.succeed('Seed completed');
|
|
697
|
+
}
|
|
698
|
+
catch (err) {
|
|
699
|
+
spinner.fail('Seed failed');
|
|
700
|
+
if (err instanceof Error) {
|
|
701
|
+
console.log(` ${dim(err.message)}`);
|
|
702
|
+
}
|
|
703
|
+
newline();
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|
|
706
|
+
newline();
|
|
707
|
+
}
|
|
708
|
+
// ---------------------------------------------------------------------------
|
|
709
|
+
// Command: status
|
|
710
|
+
// ---------------------------------------------------------------------------
|
|
711
|
+
async function cmdStatus(args, config) {
|
|
712
|
+
banner();
|
|
713
|
+
const url = requireUrl(config);
|
|
714
|
+
label('Database', redactUrl(url));
|
|
715
|
+
label('Schema', config.schema);
|
|
716
|
+
newline();
|
|
717
|
+
const spinner = new Spinner('Introspecting database').start();
|
|
718
|
+
const schema = await introspect({
|
|
719
|
+
connectionString: url,
|
|
720
|
+
schema: config.schema,
|
|
721
|
+
include: config.include.length ? config.include : undefined,
|
|
722
|
+
exclude: config.exclude.length ? config.exclude : undefined,
|
|
723
|
+
});
|
|
724
|
+
const tableNames = Object.keys(schema.tables);
|
|
725
|
+
spinner.succeed(`Found ${bold(String(tableNames.length))} tables`);
|
|
726
|
+
newline();
|
|
727
|
+
for (const tbl of Object.values(schema.tables)) {
|
|
728
|
+
const relCount = Object.keys(tbl.relations).length;
|
|
729
|
+
const pk = tbl.primaryKey.join(', ') || dim('(none)');
|
|
730
|
+
console.log(` ${bold(cyan(tbl.name))}`);
|
|
731
|
+
for (let i = 0; i < tbl.columns.length; i++) {
|
|
732
|
+
const col = tbl.columns[i];
|
|
733
|
+
const isLast = i === tbl.columns.length - 1 && relCount === 0;
|
|
734
|
+
const prefix = isLast ? symbols.teeEnd : symbols.tee;
|
|
735
|
+
const nullable = col.nullable ? dim('?') : '';
|
|
736
|
+
const def = col.hasDefault ? dim(' (default)') : '';
|
|
737
|
+
const pkLabel = tbl.primaryKey.includes(col.name) ? ` ${magenta('PK')}` : '';
|
|
738
|
+
console.log(` ${dim(prefix)} ${col.field}${nullable}: ${green(col.tsType)}${pkLabel}${def} ${gray(symbols.arrow + ' ' + col.pgType)}`);
|
|
739
|
+
}
|
|
740
|
+
const rels = Object.entries(tbl.relations);
|
|
741
|
+
if (rels.length > 0) {
|
|
742
|
+
for (let i = 0; i < rels.length; i++) {
|
|
743
|
+
const [relName, rel] = rels[i];
|
|
744
|
+
const isLast = i === rels.length - 1;
|
|
745
|
+
const prefix = isLast ? symbols.teeEnd : symbols.tee;
|
|
746
|
+
const relColor = rel.type === 'hasMany' ? blue : yellow;
|
|
747
|
+
console.log(` ${dim(prefix)} ${relColor(relName)} ${dim(symbols.arrow)} ${rel.to} ${dim(`(${rel.type}, FK: ${rel.foreignKey})`)}`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
newline();
|
|
751
|
+
}
|
|
752
|
+
if (Object.keys(schema.enums).length > 0) {
|
|
753
|
+
console.log(` ${bold('Enums:')}`);
|
|
754
|
+
for (const [enumName, labels] of Object.entries(schema.enums)) {
|
|
755
|
+
console.log(` ${cyan(enumName)}: ${labels.map((l) => green(`'${l}'`)).join(dim(' | '))}`);
|
|
756
|
+
}
|
|
757
|
+
newline();
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// ---------------------------------------------------------------------------
|
|
761
|
+
// Command: studio (scaffold)
|
|
762
|
+
// ---------------------------------------------------------------------------
|
|
763
|
+
async function cmdStudio(_args, _config) {
|
|
764
|
+
banner();
|
|
765
|
+
console.log(box([
|
|
766
|
+
`${bold('Turbine Studio')} ${dim('— coming soon')}`,
|
|
767
|
+
'',
|
|
768
|
+
'A local web UI for browsing your database,',
|
|
769
|
+
'exploring relations, and managing data.',
|
|
770
|
+
'',
|
|
771
|
+
`Follow ${cyan('@batadata')} for updates.`,
|
|
772
|
+
].join('\n'), { title: bold(cyan('Studio')), padding: 2 }));
|
|
773
|
+
newline();
|
|
774
|
+
}
|
|
775
|
+
// ---------------------------------------------------------------------------
|
|
776
|
+
// Help
|
|
777
|
+
// ---------------------------------------------------------------------------
|
|
778
|
+
function showHelp() {
|
|
779
|
+
banner();
|
|
780
|
+
console.log(` ${bold('Usage:')}`);
|
|
781
|
+
console.log(` npx turbine ${cyan('<command>')} ${dim('[options]')}`);
|
|
782
|
+
newline();
|
|
783
|
+
console.log(` ${bold('Commands:')}`);
|
|
784
|
+
console.log(` ${cyan('init')} Initialize a Turbine project`);
|
|
785
|
+
console.log(` ${cyan('generate')} ${dim('| pull')} Introspect database ${symbols.arrow} generate types`);
|
|
786
|
+
console.log(` ${cyan('push')} Apply schema definitions to database`);
|
|
787
|
+
console.log(` ${cyan('migrate')} ${dim('<sub>')} SQL migration management`);
|
|
788
|
+
console.log(` ${dim('create <name>')} Create a new migration file`);
|
|
789
|
+
console.log(` ${dim('up')} Apply pending migrations`);
|
|
790
|
+
console.log(` ${dim('down')} Rollback last migration`);
|
|
791
|
+
console.log(` ${dim('status')} Show applied/pending migrations`);
|
|
792
|
+
console.log(` ${cyan('seed')} Run seed file`);
|
|
793
|
+
console.log(` ${cyan('status')} ${dim('| info')} Show schema summary`);
|
|
794
|
+
console.log(` ${cyan('studio')} Launch web UI (coming soon)`);
|
|
795
|
+
newline();
|
|
796
|
+
console.log(` ${bold('Options:')}`);
|
|
797
|
+
console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
|
|
798
|
+
console.log(` ${cyan('--out, -o')} ${dim('<dir>')} Output directory ${dim('(default: ./generated/turbine)')}`);
|
|
799
|
+
console.log(` ${cyan('--schema, -s')} ${dim('<name>')} Postgres schema ${dim('(default: public)')}`);
|
|
800
|
+
console.log(` ${cyan('--include')} ${dim('<tables>')} Comma-separated tables to include`);
|
|
801
|
+
console.log(` ${cyan('--exclude')} ${dim('<tables>')} Comma-separated tables to exclude`);
|
|
802
|
+
console.log(` ${cyan('--dry-run')} Show SQL without executing`);
|
|
803
|
+
console.log(` ${cyan('--verbose, -v')} Show detailed output`);
|
|
804
|
+
console.log(` ${cyan('--force, -f')} Overwrite existing files`);
|
|
805
|
+
newline();
|
|
806
|
+
console.log(` ${bold('Config file:')}`);
|
|
807
|
+
console.log(` ${dim('Create')} ${cyan('turbine.config.ts')} ${dim('with')} ${cyan('npx turbine init')}`);
|
|
808
|
+
console.log(` ${dim('CLI flags override config file values.')}`);
|
|
809
|
+
newline();
|
|
810
|
+
console.log(` ${bold('Examples:')}`);
|
|
811
|
+
console.log(` ${dim('$')} npx turbine init --url postgres://user:pass@host/db`);
|
|
812
|
+
console.log(` ${dim('$')} DATABASE_URL=postgres://... npx turbine generate`);
|
|
813
|
+
console.log(` ${dim('$')} npx turbine migrate create add_users_table`);
|
|
814
|
+
console.log(` ${dim('$')} npx turbine migrate up`);
|
|
815
|
+
console.log(` ${dim('$')} npx turbine push --dry-run`);
|
|
816
|
+
newline();
|
|
817
|
+
}
|
|
818
|
+
// ---------------------------------------------------------------------------
|
|
819
|
+
// Version
|
|
820
|
+
// ---------------------------------------------------------------------------
|
|
821
|
+
function showVersion() {
|
|
822
|
+
// Read version from package.json at build time
|
|
823
|
+
console.log(`@batadata/turbine v0.2.0`);
|
|
824
|
+
}
|
|
825
|
+
// ---------------------------------------------------------------------------
|
|
826
|
+
// Main
|
|
827
|
+
// ---------------------------------------------------------------------------
|
|
828
|
+
async function main() {
|
|
829
|
+
const args = parseArgs();
|
|
830
|
+
// Quick exits that don't need config
|
|
831
|
+
if (args.command === 'help' || args.command === '--help' || args.command === '-h') {
|
|
832
|
+
showHelp();
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (args.command === 'version' || args.command === '--version' || args.command === '-V') {
|
|
836
|
+
showVersion();
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
// Load config file
|
|
840
|
+
let fileConfig = {};
|
|
841
|
+
try {
|
|
842
|
+
fileConfig = await loadConfig();
|
|
843
|
+
}
|
|
844
|
+
catch (err) {
|
|
845
|
+
if (args.command !== 'init') {
|
|
846
|
+
warn(`Could not load config: ${err instanceof Error ? err.message : String(err)}`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
const overrides = {
|
|
850
|
+
url: args.url,
|
|
851
|
+
out: args.out,
|
|
852
|
+
schema: args.schema,
|
|
853
|
+
include: args.include,
|
|
854
|
+
exclude: args.exclude,
|
|
855
|
+
};
|
|
856
|
+
const config = resolveConfig(fileConfig, overrides);
|
|
857
|
+
try {
|
|
858
|
+
switch (args.command) {
|
|
859
|
+
case 'init':
|
|
860
|
+
await cmdInit(args, config);
|
|
861
|
+
break;
|
|
862
|
+
case 'generate':
|
|
863
|
+
case 'gen':
|
|
864
|
+
case 'g':
|
|
865
|
+
case 'pull':
|
|
866
|
+
await cmdGenerate(args, config);
|
|
867
|
+
break;
|
|
868
|
+
case 'push':
|
|
869
|
+
await cmdPush(args, config);
|
|
870
|
+
break;
|
|
871
|
+
case 'migrate':
|
|
872
|
+
case 'migration':
|
|
873
|
+
case 'm':
|
|
874
|
+
await cmdMigrate(args, config);
|
|
875
|
+
break;
|
|
876
|
+
case 'seed':
|
|
877
|
+
case 's':
|
|
878
|
+
await cmdSeed(args, config);
|
|
879
|
+
break;
|
|
880
|
+
case 'status':
|
|
881
|
+
case 'info':
|
|
882
|
+
await cmdStatus(args, config);
|
|
883
|
+
break;
|
|
884
|
+
case 'studio':
|
|
885
|
+
await cmdStudio(args, config);
|
|
886
|
+
break;
|
|
887
|
+
default:
|
|
888
|
+
error(`Unknown command: ${bold(args.command)}`);
|
|
889
|
+
newline();
|
|
890
|
+
console.log(` ${dim('Run')} ${cyan('npx turbine help')} ${dim('for available commands.')}`);
|
|
891
|
+
newline();
|
|
892
|
+
process.exit(1);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
catch (err) {
|
|
896
|
+
if (err instanceof Error) {
|
|
897
|
+
if (err.message.includes('ECONNREFUSED') || err.message.includes('connection')) {
|
|
898
|
+
newline();
|
|
899
|
+
error(`Could not connect to database`);
|
|
900
|
+
console.log(` ${dim(err.message)}`);
|
|
901
|
+
newline();
|
|
902
|
+
console.log(` ${dim('Check that:')}`);
|
|
903
|
+
console.log(` ${dim('1.')} Your database is running`);
|
|
904
|
+
console.log(` ${dim('2.')} The connection string is correct`);
|
|
905
|
+
console.log(` ${dim('3.')} Network/firewall allows the connection`);
|
|
906
|
+
}
|
|
907
|
+
else if (err.message.includes('authentication')) {
|
|
908
|
+
newline();
|
|
909
|
+
error(`Authentication failed`);
|
|
910
|
+
console.log(` ${dim(err.message)}`);
|
|
911
|
+
}
|
|
912
|
+
else if (err.message.includes('does not exist')) {
|
|
913
|
+
newline();
|
|
914
|
+
error(`Database or schema not found`);
|
|
915
|
+
console.log(` ${dim(err.message)}`);
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
newline();
|
|
919
|
+
error(err.message);
|
|
920
|
+
if (args.verbose && err.stack) {
|
|
921
|
+
newline();
|
|
922
|
+
console.log(dim(err.stack));
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
newline();
|
|
928
|
+
error(`Unexpected error: ${String(err)}`);
|
|
929
|
+
}
|
|
930
|
+
newline();
|
|
931
|
+
process.exit(1);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
main();
|
|
935
|
+
//# sourceMappingURL=index.js.map
|