@strav/cli 0.1.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/package.json +51 -0
- package/src/cli/bootstrap.ts +79 -0
- package/src/cli/command_loader.ts +180 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/strav.ts +13 -0
- package/src/commands/db_seed.ts +77 -0
- package/src/commands/generate_api.ts +74 -0
- package/src/commands/generate_key.ts +47 -0
- package/src/commands/generate_models.ts +117 -0
- package/src/commands/generate_seeder.ts +68 -0
- package/src/commands/migration_compare.ts +153 -0
- package/src/commands/migration_fresh.ts +149 -0
- package/src/commands/migration_generate.ts +93 -0
- package/src/commands/migration_rollback.ts +66 -0
- package/src/commands/migration_run.ts +57 -0
- package/src/commands/package_install.ts +161 -0
- package/src/commands/queue_flush.ts +35 -0
- package/src/commands/queue_retry.ts +34 -0
- package/src/commands/queue_work.ts +40 -0
- package/src/commands/scheduler_work.ts +46 -0
- package/src/config/loader.ts +39 -0
- package/src/generators/api_generator.ts +1036 -0
- package/src/generators/config.ts +128 -0
- package/src/generators/doc_generator.ts +986 -0
- package/src/generators/index.ts +11 -0
- package/src/generators/model_generator.ts +621 -0
- package/src/generators/route_generator.ts +188 -0
- package/src/generators/test_generator.ts +1668 -0
- package/src/index.ts +2 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import type { Command } from 'commander'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import { toSnakeCase } from '@stravigor/kernel/helpers/strings'
|
|
6
|
+
import { formatAndWrite } from '../generators/config.ts'
|
|
7
|
+
|
|
8
|
+
const SEEDERS_PATH = 'database/seeders'
|
|
9
|
+
|
|
10
|
+
export function register(program: Command): void {
|
|
11
|
+
program
|
|
12
|
+
.command('generate:seeder <name>')
|
|
13
|
+
.alias('g:seeder')
|
|
14
|
+
.description('Generate a database seeder class')
|
|
15
|
+
.option('-f, --force', 'Overwrite existing file')
|
|
16
|
+
.action(async (name: string, { force }: { force?: boolean }) => {
|
|
17
|
+
try {
|
|
18
|
+
// Normalize: "UserSeeder" | "User" → class UserSeeder, file user_seeder.ts
|
|
19
|
+
const className = name.endsWith('Seeder') ? name : `${name}Seeder`
|
|
20
|
+
const baseName = className.replace(/Seeder$/, '') || 'Database'
|
|
21
|
+
const fileName = toSnakeCase(baseName) + '_seeder'
|
|
22
|
+
const filePath = join(SEEDERS_PATH, `${fileName}.ts`)
|
|
23
|
+
|
|
24
|
+
if (existsSync(filePath) && !force) {
|
|
25
|
+
console.log(chalk.yellow(`File already exists: `) + chalk.dim(filePath))
|
|
26
|
+
console.log(chalk.dim(' Use --force to overwrite.'))
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const isDatabaseSeeder = baseName === 'Database'
|
|
31
|
+
|
|
32
|
+
const content = isDatabaseSeeder
|
|
33
|
+
? generateDatabaseSeeder(className)
|
|
34
|
+
: generateSeeder(className)
|
|
35
|
+
|
|
36
|
+
await formatAndWrite([{ path: filePath, content }])
|
|
37
|
+
|
|
38
|
+
console.log(chalk.green(`Created: `) + chalk.dim(filePath))
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function generateDatabaseSeeder(className: string): string {
|
|
47
|
+
return `import { Seeder } from '@stravigor/database/database'
|
|
48
|
+
|
|
49
|
+
export default class ${className} extends Seeder {
|
|
50
|
+
async run(): Promise<void> {
|
|
51
|
+
// Call sub-seeders:
|
|
52
|
+
// await this.call(UserSeeder)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function generateSeeder(className: string): string {
|
|
59
|
+
return `import { Seeder } from '@stravigor/database/database'
|
|
60
|
+
|
|
61
|
+
export default class ${className} extends Seeder {
|
|
62
|
+
async run(): Promise<void> {
|
|
63
|
+
// Use factories or direct inserts to seed data:
|
|
64
|
+
// await UserFactory.createMany(10)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
`
|
|
68
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
4
|
+
import SchemaDiffer from '@stravigor/database/database/migration/differ'
|
|
5
|
+
|
|
6
|
+
export function register(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command('compare')
|
|
9
|
+
.alias('migration:compare')
|
|
10
|
+
.description('Compare schema with database and report differences')
|
|
11
|
+
.action(async () => {
|
|
12
|
+
let db
|
|
13
|
+
try {
|
|
14
|
+
const { db: database, registry, introspector } = await bootstrap()
|
|
15
|
+
db = database
|
|
16
|
+
|
|
17
|
+
console.log(chalk.cyan('Comparing schema with database...\n'))
|
|
18
|
+
|
|
19
|
+
const desired = registry.buildRepresentation()
|
|
20
|
+
const actual = await introspector.introspect()
|
|
21
|
+
const diff = new SchemaDiffer().diff(desired, actual)
|
|
22
|
+
|
|
23
|
+
const hasChanges =
|
|
24
|
+
diff.enums.length > 0 ||
|
|
25
|
+
diff.tables.length > 0 ||
|
|
26
|
+
diff.constraints.length > 0 ||
|
|
27
|
+
diff.indexes.length > 0
|
|
28
|
+
|
|
29
|
+
if (!hasChanges) {
|
|
30
|
+
console.log(chalk.green('Schema is in sync with the database.'))
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// --- Enum changes ---
|
|
35
|
+
if (diff.enums.length > 0) {
|
|
36
|
+
console.log(chalk.bold('Enum changes:'))
|
|
37
|
+
for (const e of diff.enums) {
|
|
38
|
+
if (e.kind === 'create') {
|
|
39
|
+
console.log(chalk.green(` + CREATE ${e.name} (${e.values.join(', ')})`))
|
|
40
|
+
} else if (e.kind === 'drop') {
|
|
41
|
+
console.log(chalk.red(` - DROP ${e.name}`))
|
|
42
|
+
} else if (e.kind === 'modify') {
|
|
43
|
+
console.log(chalk.yellow(` ~ MODIFY ${e.name} (add: ${e.addedValues.join(', ')})`))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
console.log()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- Table changes ---
|
|
50
|
+
if (diff.tables.length > 0) {
|
|
51
|
+
console.log(chalk.bold('Table changes:'))
|
|
52
|
+
for (const t of diff.tables) {
|
|
53
|
+
if (t.kind === 'create') {
|
|
54
|
+
console.log(
|
|
55
|
+
chalk.green(` + CREATE ${t.table.name}`) +
|
|
56
|
+
chalk.dim(` (${t.table.columns.length} columns)`)
|
|
57
|
+
)
|
|
58
|
+
} else if (t.kind === 'drop') {
|
|
59
|
+
console.log(chalk.red(` - DROP ${t.table.name}`))
|
|
60
|
+
} else if (t.kind === 'modify') {
|
|
61
|
+
console.log(chalk.yellow(` ~ MODIFY ${t.tableName}`))
|
|
62
|
+
for (const c of t.columns) {
|
|
63
|
+
if (c.kind === 'add') {
|
|
64
|
+
console.log(
|
|
65
|
+
chalk.green(
|
|
66
|
+
` + ADD COLUMN ${c.column.name} (${typeof c.column.pgType === 'string' ? c.column.pgType : 'custom'})`
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
} else if (c.kind === 'drop') {
|
|
70
|
+
console.log(chalk.red(` - DROP COLUMN ${c.column.name}`))
|
|
71
|
+
} else if (c.kind === 'alter') {
|
|
72
|
+
const changes: string[] = []
|
|
73
|
+
if (c.typeChange) changes.push(`type: ${c.typeChange.from} -> ${c.typeChange.to}`)
|
|
74
|
+
if (c.nullableChange)
|
|
75
|
+
changes.push(c.nullableChange.to ? 'set NOT NULL' : 'drop NOT NULL')
|
|
76
|
+
if (c.defaultChange) changes.push('default changed')
|
|
77
|
+
console.log(
|
|
78
|
+
chalk.yellow(` ~ ALTER COLUMN ${c.columnName} (${changes.join(', ')})`)
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
console.log()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Constraint changes ---
|
|
88
|
+
if (diff.constraints.length > 0) {
|
|
89
|
+
console.log(chalk.bold('Constraint changes:'))
|
|
90
|
+
for (const c of diff.constraints) {
|
|
91
|
+
if (c.kind === 'add_fk') {
|
|
92
|
+
console.log(
|
|
93
|
+
chalk.green(
|
|
94
|
+
` + ADD FK ${c.tableName}(${c.constraint.columns.join(',')}) -> ${c.constraint.referencedTable}`
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
} else if (c.kind === 'drop_fk') {
|
|
98
|
+
console.log(
|
|
99
|
+
chalk.red(
|
|
100
|
+
` - DROP FK ${c.tableName}(${c.constraint.columns.join(',')}) -> ${c.constraint.referencedTable}`
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
} else if (c.kind === 'add_unique') {
|
|
104
|
+
console.log(
|
|
105
|
+
chalk.green(` + ADD UQ ${c.tableName}(${c.constraint.columns.join(',')})`)
|
|
106
|
+
)
|
|
107
|
+
} else if (c.kind === 'drop_unique') {
|
|
108
|
+
console.log(
|
|
109
|
+
chalk.red(` - DROP UQ ${c.tableName}(${c.constraint.columns.join(',')})`)
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
console.log()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- Index changes ---
|
|
117
|
+
if (diff.indexes.length > 0) {
|
|
118
|
+
console.log(chalk.bold('Index changes:'))
|
|
119
|
+
for (const i of diff.indexes) {
|
|
120
|
+
if (i.kind === 'add') {
|
|
121
|
+
const unique = i.index.unique ? 'UNIQUE ' : ''
|
|
122
|
+
console.log(
|
|
123
|
+
chalk.green(
|
|
124
|
+
` + CREATE ${unique}INDEX ${i.tableName}(${i.index.columns.join(',')})`
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
} else if (i.kind === 'drop') {
|
|
128
|
+
console.log(
|
|
129
|
+
chalk.red(` - DROP INDEX ${i.tableName}(${i.index.columns.join(',')})`)
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
console.log()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- Summary ---
|
|
137
|
+
const creates = diff.tables.filter(t => t.kind === 'create').length
|
|
138
|
+
const drops = diff.tables.filter(t => t.kind === 'drop').length
|
|
139
|
+
const modifies = diff.tables.filter(t => t.kind === 'modify').length
|
|
140
|
+
console.log(
|
|
141
|
+
chalk.bold('Summary: ') +
|
|
142
|
+
`${creates} table(s) to create, ${drops} to drop, ${modifies} to modify, ` +
|
|
143
|
+
`${diff.enums.length} enum change(s), ${diff.constraints.length} constraint change(s), ${diff.indexes.length} index change(s)`
|
|
144
|
+
)
|
|
145
|
+
console.log(chalk.dim('\nRun "bun strav generate:migration" to create migration files.'))
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
148
|
+
process.exit(1)
|
|
149
|
+
} finally {
|
|
150
|
+
if (db) await shutdown(db)
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { createInterface } from 'node:readline'
|
|
4
|
+
import { rmSync } from 'node:fs'
|
|
5
|
+
import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
6
|
+
import type Database from '@stravigor/database/database/database'
|
|
7
|
+
import type SchemaRegistry from '@stravigor/database/schema/registry'
|
|
8
|
+
import type DatabaseIntrospector from '@stravigor/database/database/introspector'
|
|
9
|
+
import SchemaDiffer from '@stravigor/database/database/migration/differ'
|
|
10
|
+
import SqlGenerator from '@stravigor/database/database/migration/sql_generator'
|
|
11
|
+
import MigrationFileGenerator from '@stravigor/database/database/migration/file_generator'
|
|
12
|
+
import MigrationTracker from '@stravigor/database/database/migration/tracker'
|
|
13
|
+
import MigrationRunner from '@stravigor/database/database/migration/runner'
|
|
14
|
+
import { getDatabasePaths } from '../config/loader.ts'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Drop all tables and enum types, regenerate a single migration from
|
|
18
|
+
* the current schema definitions, and run it.
|
|
19
|
+
*
|
|
20
|
+
* Shared by `fresh` and `seed --fresh`.
|
|
21
|
+
*/
|
|
22
|
+
export async function freshDatabase(
|
|
23
|
+
db: Database,
|
|
24
|
+
registry: SchemaRegistry,
|
|
25
|
+
introspector: DatabaseIntrospector,
|
|
26
|
+
migrationsPath: string = 'database/migrations'
|
|
27
|
+
): Promise<number> {
|
|
28
|
+
// Drop all tables in public schema
|
|
29
|
+
console.log(chalk.cyan('\nDropping all tables and types...'))
|
|
30
|
+
|
|
31
|
+
const tables = await db.sql`
|
|
32
|
+
SELECT table_name FROM information_schema.tables
|
|
33
|
+
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
34
|
+
`
|
|
35
|
+
for (const row of tables) {
|
|
36
|
+
await db.sql.unsafe(`DROP TABLE IF EXISTS "${row.table_name}" CASCADE`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Drop all enum types in public schema
|
|
40
|
+
const types = await db.sql`
|
|
41
|
+
SELECT t.typname
|
|
42
|
+
FROM pg_type t
|
|
43
|
+
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
44
|
+
WHERE n.nspname = 'public'
|
|
45
|
+
AND t.typtype = 'e'
|
|
46
|
+
`
|
|
47
|
+
for (const row of types) {
|
|
48
|
+
await db.sql.unsafe(`DROP TYPE IF EXISTS "${row.typname}" CASCADE`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Delete existing migration files
|
|
52
|
+
console.log(chalk.cyan('Clearing migration directory...'))
|
|
53
|
+
rmSync(migrationsPath, { recursive: true, force: true })
|
|
54
|
+
|
|
55
|
+
// Generate new migration
|
|
56
|
+
console.log(chalk.cyan('Generating fresh migration...'))
|
|
57
|
+
|
|
58
|
+
const desired = registry.buildRepresentation()
|
|
59
|
+
const actual = await introspector.introspect()
|
|
60
|
+
const diff = new SchemaDiffer().diff(desired, actual)
|
|
61
|
+
|
|
62
|
+
const sql = new SqlGenerator().generate(diff)
|
|
63
|
+
const version = Date.now().toString()
|
|
64
|
+
const tableOrder = desired.tables.map(t => t.name)
|
|
65
|
+
|
|
66
|
+
const fileGen = new MigrationFileGenerator(migrationsPath)
|
|
67
|
+
await fileGen.generate(version, 'fresh', sql, diff, tableOrder)
|
|
68
|
+
|
|
69
|
+
// Run the migration
|
|
70
|
+
console.log(chalk.cyan('Running migration...'))
|
|
71
|
+
|
|
72
|
+
const tracker = new MigrationTracker(db)
|
|
73
|
+
const runner = new MigrationRunner(db, tracker, migrationsPath)
|
|
74
|
+
const result = await runner.run()
|
|
75
|
+
|
|
76
|
+
return result.applied.length
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Guard that ensures APP_ENV is "local". Exits the process if not.
|
|
81
|
+
*/
|
|
82
|
+
export function requireLocalEnv(commandName: string): void {
|
|
83
|
+
const appEnv = process.env.APP_ENV
|
|
84
|
+
if (appEnv !== 'local') {
|
|
85
|
+
console.error(
|
|
86
|
+
chalk.red('REJECTED: ') + `${commandName} can only run when APP_ENV is set to "local".`
|
|
87
|
+
)
|
|
88
|
+
if (!appEnv) {
|
|
89
|
+
console.error(chalk.dim(' APP_ENV is not defined in .env'))
|
|
90
|
+
} else {
|
|
91
|
+
console.error(chalk.dim(` Current APP_ENV: "${appEnv}"`))
|
|
92
|
+
}
|
|
93
|
+
process.exit(1)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function register(program: Command): void {
|
|
98
|
+
program
|
|
99
|
+
.command('fresh')
|
|
100
|
+
.alias('migration:fresh')
|
|
101
|
+
.description('Reset database and migrations, regenerate and run from scratch')
|
|
102
|
+
.action(async () => {
|
|
103
|
+
requireLocalEnv('fresh')
|
|
104
|
+
|
|
105
|
+
// 6-digit challenge
|
|
106
|
+
const challenge = String(Math.floor(100000 + Math.random() * 900000))
|
|
107
|
+
console.log(
|
|
108
|
+
chalk.red('WARNING: ') +
|
|
109
|
+
'This will ' +
|
|
110
|
+
chalk.red('destroy ALL data') +
|
|
111
|
+
' in the database and recreate everything from schemas.'
|
|
112
|
+
)
|
|
113
|
+
console.log(`\n Type ${chalk.yellow(challenge)} to confirm:\n`)
|
|
114
|
+
|
|
115
|
+
const answer = await prompt(' > ')
|
|
116
|
+
if (answer.trim() !== challenge) {
|
|
117
|
+
console.error(chalk.red('\nChallenge code does not match. Operation cancelled.'))
|
|
118
|
+
process.exit(1)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let db
|
|
122
|
+
try {
|
|
123
|
+
// Get configured database paths
|
|
124
|
+
const dbPaths = await getDatabasePaths()
|
|
125
|
+
|
|
126
|
+
const { db: database, registry, introspector } = await bootstrap()
|
|
127
|
+
db = database
|
|
128
|
+
|
|
129
|
+
const applied = await freshDatabase(db, registry, introspector, dbPaths.migrations)
|
|
130
|
+
|
|
131
|
+
console.log(chalk.green(`\nFresh migration complete. Applied ${applied} migration(s).`))
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
134
|
+
process.exit(1)
|
|
135
|
+
} finally {
|
|
136
|
+
if (db) await shutdown(db)
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function prompt(question: string): Promise<string> {
|
|
142
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
143
|
+
return new Promise(resolve => {
|
|
144
|
+
rl.question(question, answer => {
|
|
145
|
+
rl.close()
|
|
146
|
+
resolve(answer)
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
4
|
+
import SchemaDiffer from '@stravigor/database/database/migration/differ'
|
|
5
|
+
import SqlGenerator from '@stravigor/database/database/migration/sql_generator'
|
|
6
|
+
import MigrationFileGenerator from '@stravigor/database/database/migration/file_generator'
|
|
7
|
+
import { discoverDomains } from '@stravigor/database'
|
|
8
|
+
import { getDatabasePaths } from '../config/loader.ts'
|
|
9
|
+
|
|
10
|
+
export function register(program: Command): void {
|
|
11
|
+
program
|
|
12
|
+
.command('generate:migration')
|
|
13
|
+
.aliases(['migration:generate', 'g:migration'])
|
|
14
|
+
.description('Generate migration files from schema changes')
|
|
15
|
+
.option('-m, --message <message>', 'Migration message', 'migration')
|
|
16
|
+
.option('-s, --scope <scope>', 'Domain (e.g., public, tenant, factory, marketing)', 'public')
|
|
17
|
+
.action(async (opts: { message: string; scope: string }) => {
|
|
18
|
+
let db
|
|
19
|
+
try {
|
|
20
|
+
// Get configured database paths
|
|
21
|
+
const dbPaths = await getDatabasePaths()
|
|
22
|
+
|
|
23
|
+
// Validate scope against available domains
|
|
24
|
+
const availableDomains = discoverDomains(dbPaths.schemas)
|
|
25
|
+
if (!availableDomains.includes(opts.scope)) {
|
|
26
|
+
throw new Error(`Invalid domain: ${opts.scope}. Available domains: ${availableDomains.join(', ')}`)
|
|
27
|
+
}
|
|
28
|
+
const scope = opts.scope
|
|
29
|
+
|
|
30
|
+
const { db: database, registry, introspector } = await bootstrap(scope)
|
|
31
|
+
db = database
|
|
32
|
+
|
|
33
|
+
console.log(chalk.cyan(`Comparing ${scope} schema with database...`))
|
|
34
|
+
|
|
35
|
+
const desired = registry.buildRepresentation()
|
|
36
|
+
const actual = await introspector.introspect()
|
|
37
|
+
const diff = new SchemaDiffer().diff(desired, actual)
|
|
38
|
+
|
|
39
|
+
const hasChanges =
|
|
40
|
+
diff.enums.length > 0 ||
|
|
41
|
+
diff.tables.length > 0 ||
|
|
42
|
+
diff.constraints.length > 0 ||
|
|
43
|
+
diff.indexes.length > 0
|
|
44
|
+
|
|
45
|
+
if (!hasChanges) {
|
|
46
|
+
console.log(chalk.green('No changes detected. Schema is in sync with the database.'))
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const sql = new SqlGenerator().generate(diff)
|
|
51
|
+
const version = Date.now().toString()
|
|
52
|
+
|
|
53
|
+
// Use the table order from the desired representation (already dependency-ordered)
|
|
54
|
+
const tableOrder = desired.tables.map(t => t.name)
|
|
55
|
+
|
|
56
|
+
// Use scoped migrations path from configuration
|
|
57
|
+
const scopedPath = `${dbPaths.migrations}/${scope}`
|
|
58
|
+
const fileGen = new MigrationFileGenerator(scopedPath)
|
|
59
|
+
const dir = await fileGen.generate(version, opts.message, sql, diff, tableOrder)
|
|
60
|
+
|
|
61
|
+
// Print summary
|
|
62
|
+
console.log(chalk.green(`\nMigration generated: ${version}`))
|
|
63
|
+
console.log(chalk.dim(` Directory: ${dir}`))
|
|
64
|
+
console.log(chalk.dim(` Message: ${opts.message}\n`))
|
|
65
|
+
|
|
66
|
+
const counts: string[] = []
|
|
67
|
+
const creates = diff.tables.filter(t => t.kind === 'create').length
|
|
68
|
+
const drops = diff.tables.filter(t => t.kind === 'drop').length
|
|
69
|
+
const modifies = diff.tables.filter(t => t.kind === 'modify').length
|
|
70
|
+
if (creates > 0) counts.push(chalk.green(`${creates} table(s) to create`))
|
|
71
|
+
if (drops > 0) counts.push(chalk.red(`${drops} table(s) to drop`))
|
|
72
|
+
if (modifies > 0) counts.push(chalk.yellow(`${modifies} table(s) to modify`))
|
|
73
|
+
|
|
74
|
+
const enumCreates = diff.enums.filter(e => e.kind === 'create').length
|
|
75
|
+
const enumDrops = diff.enums.filter(e => e.kind === 'drop').length
|
|
76
|
+
const enumModifies = diff.enums.filter(e => e.kind === 'modify').length
|
|
77
|
+
if (enumCreates > 0) counts.push(chalk.green(`${enumCreates} enum(s) to create`))
|
|
78
|
+
if (enumDrops > 0) counts.push(chalk.red(`${enumDrops} enum(s) to drop`))
|
|
79
|
+
if (enumModifies > 0) counts.push(chalk.yellow(`${enumModifies} enum(s) to modify`))
|
|
80
|
+
|
|
81
|
+
if (diff.constraints.length > 0)
|
|
82
|
+
counts.push(`${diff.constraints.length} constraint change(s)`)
|
|
83
|
+
if (diff.indexes.length > 0) counts.push(`${diff.indexes.length} index change(s)`)
|
|
84
|
+
|
|
85
|
+
console.log(' Summary: ' + counts.join(', '))
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
88
|
+
process.exit(1)
|
|
89
|
+
} finally {
|
|
90
|
+
if (db) await shutdown(db)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
4
|
+
import MigrationTracker from '@stravigor/database/database/migration/tracker'
|
|
5
|
+
import MigrationRunner from '@stravigor/database/database/migration/runner'
|
|
6
|
+
import { discoverDomains } from '@stravigor/database'
|
|
7
|
+
import { getDatabasePaths } from '../config/loader.ts'
|
|
8
|
+
|
|
9
|
+
export function register(program: Command): void {
|
|
10
|
+
program
|
|
11
|
+
.command('rollback')
|
|
12
|
+
.alias('migration:rollback')
|
|
13
|
+
.description('Rollback migrations by batch')
|
|
14
|
+
.option('--batch <number>', 'Specific batch number to rollback')
|
|
15
|
+
.option('-s, --scope <scope>', 'Domain (e.g., public, tenant, factory, marketing)', 'public')
|
|
16
|
+
.action(async (opts: { batch?: string; scope: string }) => {
|
|
17
|
+
let db
|
|
18
|
+
try {
|
|
19
|
+
// Get configured database paths
|
|
20
|
+
const dbPaths = await getDatabasePaths()
|
|
21
|
+
|
|
22
|
+
// Validate scope against available domains
|
|
23
|
+
const availableDomains = discoverDomains(dbPaths.schemas)
|
|
24
|
+
if (!availableDomains.includes(opts.scope)) {
|
|
25
|
+
throw new Error(`Invalid domain: ${opts.scope}. Available domains: ${availableDomains.join(', ')}`)
|
|
26
|
+
}
|
|
27
|
+
const scope = opts.scope
|
|
28
|
+
|
|
29
|
+
const { db: database } = await bootstrap()
|
|
30
|
+
db = database
|
|
31
|
+
|
|
32
|
+
const scopedPath = `${dbPaths.migrations}/${scope}`
|
|
33
|
+
const tracker = new MigrationTracker(db, scope)
|
|
34
|
+
const runner = new MigrationRunner(db, tracker, scopedPath, scope)
|
|
35
|
+
|
|
36
|
+
const batchNum = opts.batch ? parseInt(opts.batch, 10) : undefined
|
|
37
|
+
|
|
38
|
+
console.log(
|
|
39
|
+
batchNum
|
|
40
|
+
? chalk.cyan(`Rolling back batch ${batchNum}...`)
|
|
41
|
+
: chalk.cyan('Rolling back last batch...')
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const result = await runner.rollback(batchNum)
|
|
45
|
+
|
|
46
|
+
if (result.rolledBack.length === 0) {
|
|
47
|
+
console.log(chalk.yellow('Nothing to rollback.'))
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(
|
|
52
|
+
chalk.green(
|
|
53
|
+
`\nRolled back ${result.rolledBack.length} migration(s) from batch ${result.batch}:`
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
for (const version of result.rolledBack) {
|
|
57
|
+
console.log(chalk.dim(` - ${version}`))
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
61
|
+
process.exit(1)
|
|
62
|
+
} finally {
|
|
63
|
+
if (db) await shutdown(db)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
4
|
+
import MigrationTracker from '@stravigor/database/database/migration/tracker'
|
|
5
|
+
import MigrationRunner from '@stravigor/database/database/migration/runner'
|
|
6
|
+
import { discoverDomains } from '@stravigor/database'
|
|
7
|
+
import { getDatabasePaths } from '../config/loader.ts'
|
|
8
|
+
|
|
9
|
+
export function register(program: Command): void {
|
|
10
|
+
program
|
|
11
|
+
.command('migrate')
|
|
12
|
+
.alias('migration:run')
|
|
13
|
+
.description('Run pending migrations')
|
|
14
|
+
.option('-s, --scope <scope>', 'Domain (e.g., public, tenant, factory, marketing)', 'public')
|
|
15
|
+
.action(async (opts: { scope: string }) => {
|
|
16
|
+
let db
|
|
17
|
+
try {
|
|
18
|
+
// Get configured database paths
|
|
19
|
+
const dbPaths = await getDatabasePaths()
|
|
20
|
+
|
|
21
|
+
// Validate scope against available domains
|
|
22
|
+
const availableDomains = discoverDomains(dbPaths.schemas)
|
|
23
|
+
if (!availableDomains.includes(opts.scope)) {
|
|
24
|
+
throw new Error(`Invalid domain: ${opts.scope}. Available domains: ${availableDomains.join(', ')}`)
|
|
25
|
+
}
|
|
26
|
+
const scope = opts.scope
|
|
27
|
+
|
|
28
|
+
const { db: database } = await bootstrap()
|
|
29
|
+
db = database
|
|
30
|
+
|
|
31
|
+
const scopedPath = `${dbPaths.migrations}/${scope}`
|
|
32
|
+
const tracker = new MigrationTracker(db, scope)
|
|
33
|
+
const runner = new MigrationRunner(db, tracker, scopedPath, scope)
|
|
34
|
+
|
|
35
|
+
console.log(chalk.cyan('Running pending migrations...'))
|
|
36
|
+
|
|
37
|
+
const result = await runner.run()
|
|
38
|
+
|
|
39
|
+
if (result.applied.length === 0) {
|
|
40
|
+
console.log(chalk.green('Nothing to migrate. All migrations are up to date.'))
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(
|
|
45
|
+
chalk.green(`\nApplied ${result.applied.length} migration(s) in batch ${result.batch}:`)
|
|
46
|
+
)
|
|
47
|
+
for (const version of result.applied) {
|
|
48
|
+
console.log(chalk.dim(` - ${version}`))
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
52
|
+
process.exit(1)
|
|
53
|
+
} finally {
|
|
54
|
+
if (db) await shutdown(db)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
}
|