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