@strav/cli 0.4.30 → 1.0.0-alpha.4
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 +17 -41
- package/src/binder.ts +88 -0
- package/src/command.ts +297 -0
- package/src/config_list.ts +42 -0
- package/src/config_show.ts +50 -0
- package/src/console_provider.ts +46 -0
- package/src/exit_codes.ts +26 -0
- package/src/index.ts +60 -2
- package/src/key_generate.ts +66 -0
- package/src/make/index.ts +17 -0
- package/src/make/make_command_file.ts +27 -0
- package/src/make/make_controller.ts +24 -0
- package/src/make/make_factory.ts +25 -0
- package/src/make/make_job.ts +25 -0
- package/src/make/make_mail.ts +27 -0
- package/src/make/make_middleware.ts +23 -0
- package/src/make/make_migration.ts +48 -0
- package/src/make/make_model.ts +91 -0
- package/src/make/make_notification.ts +23 -0
- package/src/make/make_policy.ts +24 -0
- package/src/make/make_provider.ts +29 -0
- package/src/make/make_repository.ts +30 -0
- package/src/make/make_request.ts +24 -0
- package/src/make/make_seeder.ts +23 -0
- package/src/make/make_test.ts +22 -0
- package/src/make_command.ts +69 -0
- package/src/run_cli.ts +121 -0
- package/src/scaffold_console_provider.ts +45 -0
- package/src/signature.ts +171 -0
- package/src/subset_boot.ts +51 -0
- package/src/util_console_provider.ts +18 -0
- package/src/cli/bootstrap.ts +0 -77
- package/src/cli/command_loader.ts +0 -180
- package/src/cli/index.ts +0 -3
- package/src/cli/strav.ts +0 -13
- package/src/commands/db_seed.ts +0 -77
- package/src/commands/db_setup_roles.ts +0 -101
- package/src/commands/generate_api.ts +0 -93
- package/src/commands/generate_key.ts +0 -47
- package/src/commands/generate_models.ts +0 -49
- package/src/commands/generate_seeder.ts +0 -68
- package/src/commands/migration_compare.ts +0 -167
- package/src/commands/migration_fresh.ts +0 -148
- package/src/commands/migration_generate.ts +0 -84
- package/src/commands/migration_rollback.ts +0 -54
- package/src/commands/migration_run.ts +0 -45
- package/src/commands/package_install.ts +0 -161
- package/src/commands/queue_flush.ts +0 -35
- package/src/commands/queue_retry.ts +0 -34
- package/src/commands/queue_work.ts +0 -101
- package/src/commands/scheduler_work.ts +0 -46
- package/src/commands/tenant_create.ts +0 -35
- package/src/commands/tenant_delete.ts +0 -64
- package/src/commands/tenant_list.ts +0 -39
- package/src/config/loader.ts +0 -50
- package/src/generators/api_generator.ts +0 -1035
- package/src/generators/config.ts +0 -113
- package/src/generators/doc_generator.ts +0 -996
- package/src/generators/index.ts +0 -11
- package/src/generators/model_generator.ts +0 -596
- package/src/generators/route_generator.ts +0 -187
- package/src/generators/test_generator.ts +0 -1667
- package/tsconfig.json +0 -5
package/src/commands/db_seed.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
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 { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
6
|
-
import { freshDatabase, requireLocalEnv } from './migration_fresh.ts'
|
|
7
|
-
import { toSnakeCase } from '@strav/kernel/helpers/strings'
|
|
8
|
-
import BaseModel from '@strav/database/orm/base_model'
|
|
9
|
-
import { Seeder } from '@strav/database/database/seeder'
|
|
10
|
-
|
|
11
|
-
const SEEDERS_PATH = 'database/seeders'
|
|
12
|
-
|
|
13
|
-
export function register(program: Command): void {
|
|
14
|
-
program
|
|
15
|
-
.command('seed')
|
|
16
|
-
.alias('db:seed')
|
|
17
|
-
.description('Seed the database with records')
|
|
18
|
-
.option('-c, --class <name>', 'Run a specific seeder class')
|
|
19
|
-
.option('--fresh', 'Drop all tables and re-migrate before seeding')
|
|
20
|
-
.action(async ({ class: className, fresh }: { class?: string; fresh?: boolean }) => {
|
|
21
|
-
let db
|
|
22
|
-
try {
|
|
23
|
-
const { db: database, registry, introspector } = await bootstrap()
|
|
24
|
-
db = database
|
|
25
|
-
|
|
26
|
-
// Wire BaseModel so factories / seeders can use the ORM
|
|
27
|
-
new BaseModel(db)
|
|
28
|
-
|
|
29
|
-
// --fresh: reset the database first
|
|
30
|
-
if (fresh) {
|
|
31
|
-
requireLocalEnv('seed --fresh')
|
|
32
|
-
|
|
33
|
-
const applied = await freshDatabase(db, registry, introspector)
|
|
34
|
-
console.log(chalk.green(`\nFresh migration complete. Applied ${applied} migration(s).`))
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Resolve the seeder file
|
|
38
|
-
const fileName = className
|
|
39
|
-
? toSnakeCase(className.replace(/Seeder$/i, '') || 'database') + '_seeder'
|
|
40
|
-
: 'database_seeder'
|
|
41
|
-
|
|
42
|
-
const seederPath = join(process.cwd(), SEEDERS_PATH, `${fileName}.ts`)
|
|
43
|
-
|
|
44
|
-
if (!existsSync(seederPath)) {
|
|
45
|
-
console.error(chalk.red(`Seeder not found: `) + chalk.dim(seederPath))
|
|
46
|
-
console.error(
|
|
47
|
-
chalk.dim(
|
|
48
|
-
` Run ${chalk.cyan(`strav generate:seeder ${className ?? 'DatabaseSeeder'}`)} to create it.`
|
|
49
|
-
)
|
|
50
|
-
)
|
|
51
|
-
process.exit(1)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
console.log(chalk.cyan(`\nSeeding database...`))
|
|
55
|
-
|
|
56
|
-
const mod = await import(seederPath)
|
|
57
|
-
const SeederClass = mod.default
|
|
58
|
-
|
|
59
|
-
if (!SeederClass || !(SeederClass.prototype instanceof Seeder)) {
|
|
60
|
-
console.error(
|
|
61
|
-
chalk.red(`Error: `) + `Default export of ${fileName}.ts must extend Seeder.`
|
|
62
|
-
)
|
|
63
|
-
process.exit(1)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const seeder = new SeederClass(db) as Seeder
|
|
67
|
-
await seeder.run()
|
|
68
|
-
|
|
69
|
-
console.log(chalk.green('Database seeding complete.'))
|
|
70
|
-
} catch (err) {
|
|
71
|
-
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
72
|
-
process.exit(1)
|
|
73
|
-
} finally {
|
|
74
|
-
if (db) await shutdown(db)
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import type { Command } from 'commander'
|
|
2
|
-
import chalk from 'chalk'
|
|
3
|
-
import Configuration from '@strav/kernel/config/configuration'
|
|
4
|
-
import { SQL } from 'bun'
|
|
5
|
-
import { env } from '@strav/kernel/helpers/env'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Emit (or apply) the SQL needed to set up the two PostgreSQL roles
|
|
9
|
-
* required by the multi-tenant RLS workflow:
|
|
10
|
-
*
|
|
11
|
-
* - app role: NOBYPASSRLS — used by the application; RLS policies apply.
|
|
12
|
-
* - bypass role: BYPASSRLS — used by migrations, TenantManager, withoutTenant().
|
|
13
|
-
*
|
|
14
|
-
* Reads the desired role names/passwords from the loaded Configuration
|
|
15
|
-
* (`database.username`, `database.password`, `database.tenant.bypass.username`,
|
|
16
|
-
* `database.tenant.bypass.password`).
|
|
17
|
-
*
|
|
18
|
-
* Must be run by a PostgreSQL superuser (only the superuser can grant the
|
|
19
|
-
* BYPASSRLS attribute). Pass --apply to execute against the configured
|
|
20
|
-
* database; otherwise the SQL is printed for manual review.
|
|
21
|
-
*/
|
|
22
|
-
export function register(program: Command): void {
|
|
23
|
-
program
|
|
24
|
-
.command('db:setup-roles')
|
|
25
|
-
.description('Print or apply the SQL to create the app + bypass PostgreSQL roles')
|
|
26
|
-
.option('--apply', 'Execute the SQL against the database (requires superuser credentials)')
|
|
27
|
-
.option('--superuser <name>', 'Superuser to connect as when --apply is set', 'postgres')
|
|
28
|
-
.option('--superuser-password <password>', 'Superuser password (or read from $DB_SUPERUSER_PASSWORD)')
|
|
29
|
-
.action(async (opts: { apply?: boolean; superuser: string; superuserPassword?: string }) => {
|
|
30
|
-
try {
|
|
31
|
-
const config = new Configuration('./config')
|
|
32
|
-
await config.load()
|
|
33
|
-
|
|
34
|
-
const dbName = config.get('database.database') ?? env('DB_DATABASE', 'strav')
|
|
35
|
-
const appUser = config.get('database.username') ?? env('DB_USER', 'strav_app')
|
|
36
|
-
const appPassword = config.get('database.password') ?? env('DB_PASSWORD', 'changeme')
|
|
37
|
-
const bypassUser =
|
|
38
|
-
config.get('database.tenant.bypass.username') ?? env('DB_BYPASS_USER', 'strav_admin')
|
|
39
|
-
const bypassPassword =
|
|
40
|
-
config.get('database.tenant.bypass.password') ?? env('DB_BYPASS_PASSWORD', 'changeme')
|
|
41
|
-
|
|
42
|
-
const stmts = [
|
|
43
|
-
`CREATE ROLE "${appUser}" LOGIN PASSWORD '${appPassword}' NOBYPASSRLS;`,
|
|
44
|
-
`CREATE ROLE "${bypassUser}" LOGIN PASSWORD '${bypassPassword}' BYPASSRLS;`,
|
|
45
|
-
`GRANT ALL ON DATABASE "${dbName}" TO "${appUser}", "${bypassUser}";`,
|
|
46
|
-
`GRANT ALL ON SCHEMA public TO "${appUser}", "${bypassUser}";`,
|
|
47
|
-
// Default privileges for objects the *current connection* will create.
|
|
48
|
-
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "${appUser}", "${bypassUser}";`,
|
|
49
|
-
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "${appUser}", "${bypassUser}";`,
|
|
50
|
-
// ALTER DEFAULT PRIVILEGES is per-role-of-the-creator. Migrations
|
|
51
|
-
// run as the bypass role, so without these the app role never
|
|
52
|
-
// sees grants on tables/sequences the bypass role creates and
|
|
53
|
-
// any non-bypass query (seeders, request-time inserts) fails
|
|
54
|
-
// with `permission denied for table …`. Setting them here means
|
|
55
|
-
// the demo flow `db:setup-roles → fresh → seed` works without
|
|
56
|
-
// any extra GRANT plumbing.
|
|
57
|
-
`ALTER DEFAULT PRIVILEGES FOR ROLE "${bypassUser}" IN SCHEMA public GRANT ALL ON TABLES TO "${appUser}";`,
|
|
58
|
-
`ALTER DEFAULT PRIVILEGES FOR ROLE "${bypassUser}" IN SCHEMA public GRANT ALL ON SEQUENCES TO "${appUser}";`,
|
|
59
|
-
]
|
|
60
|
-
|
|
61
|
-
if (!opts.apply) {
|
|
62
|
-
console.log(chalk.cyan('\n-- Run as a PostgreSQL superuser:\n'))
|
|
63
|
-
for (const s of stmts) console.log(s)
|
|
64
|
-
console.log(
|
|
65
|
-
chalk.dim('\n(Use --apply to execute these against the configured database.)')
|
|
66
|
-
)
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const password = opts.superuserPassword ?? env('DB_SUPERUSER_PASSWORD', '')
|
|
71
|
-
const sql = new SQL({
|
|
72
|
-
hostname: config.get('database.host') ?? env('DB_HOST', '127.0.0.1'),
|
|
73
|
-
port: config.get('database.port') ?? env.int('DB_PORT', 5432),
|
|
74
|
-
username: opts.superuser,
|
|
75
|
-
password,
|
|
76
|
-
database: dbName,
|
|
77
|
-
max: 1,
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
console.log(chalk.cyan(`Applying role setup as superuser "${opts.superuser}"...`))
|
|
81
|
-
for (const stmt of stmts) {
|
|
82
|
-
try {
|
|
83
|
-
await sql.unsafe(stmt)
|
|
84
|
-
console.log(chalk.dim(` ok: ${stmt}`))
|
|
85
|
-
} catch (err) {
|
|
86
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
87
|
-
if (/already exists/i.test(msg)) {
|
|
88
|
-
console.log(chalk.dim(` skip (exists): ${stmt}`))
|
|
89
|
-
} else {
|
|
90
|
-
throw err
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
await sql.close()
|
|
95
|
-
console.log(chalk.green('\nRoles set up.'))
|
|
96
|
-
} catch (err) {
|
|
97
|
-
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
98
|
-
process.exit(1)
|
|
99
|
-
}
|
|
100
|
-
})
|
|
101
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path'
|
|
2
|
-
import type { Command } from 'commander'
|
|
3
|
-
import chalk from 'chalk'
|
|
4
|
-
import SchemaRegistry from '@strav/database/schema/registry'
|
|
5
|
-
import ApiGenerator from '../generators/api_generator.ts'
|
|
6
|
-
import RouteGenerator from '../generators/route_generator.ts'
|
|
7
|
-
import TestGenerator from '../generators/test_generator.ts'
|
|
8
|
-
import DocGenerator from '../generators/doc_generator.ts'
|
|
9
|
-
import type { ApiRoutingConfig } from '../generators/route_generator.ts'
|
|
10
|
-
import type { GeneratorConfig } from '../generators/config.ts'
|
|
11
|
-
import { loadGeneratorConfig, getDatabasePaths, loadTenantIdType } from '../config/loader.ts'
|
|
12
|
-
|
|
13
|
-
export function register(program: Command): void {
|
|
14
|
-
program
|
|
15
|
-
.command('generate:api')
|
|
16
|
-
.alias('g:api')
|
|
17
|
-
.description(
|
|
18
|
-
'Generate services, controllers, policies, validators, events, and routes from schemas'
|
|
19
|
-
)
|
|
20
|
-
.option('-f, --force', 'Overwrite existing generated files')
|
|
21
|
-
.action(async ({ force }: { force?: boolean }) => {
|
|
22
|
-
try {
|
|
23
|
-
console.log(chalk.cyan('Generating API layer from schemas...'))
|
|
24
|
-
|
|
25
|
-
// Get configured database paths
|
|
26
|
-
const dbPaths = await getDatabasePaths()
|
|
27
|
-
|
|
28
|
-
const registry = new SchemaRegistry()
|
|
29
|
-
await registry.discover(dbPaths.schemas)
|
|
30
|
-
registry.validate()
|
|
31
|
-
|
|
32
|
-
const schemas = registry.resolve()
|
|
33
|
-
const tenantIdType = await loadTenantIdType()
|
|
34
|
-
const representation = registry.buildRepresentation(tenantIdType)
|
|
35
|
-
|
|
36
|
-
// Load generator config (if available)
|
|
37
|
-
const config = await loadGeneratorConfig()
|
|
38
|
-
|
|
39
|
-
const apiGen = new ApiGenerator(schemas, representation, config)
|
|
40
|
-
const apiResult = await apiGen.writeAll(force)
|
|
41
|
-
|
|
42
|
-
// Load API routing config from config/http.ts (if available)
|
|
43
|
-
let apiConfig: Partial<ApiRoutingConfig> | undefined
|
|
44
|
-
try {
|
|
45
|
-
const httpConfig = (await import(join(process.cwd(), 'config/http.ts'))).default
|
|
46
|
-
apiConfig = httpConfig.api
|
|
47
|
-
} catch {
|
|
48
|
-
// No config/http.ts or no api section — use defaults
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const routeGen = new RouteGenerator(schemas, config, apiConfig)
|
|
52
|
-
const routeResult = await routeGen.writeAll(force)
|
|
53
|
-
|
|
54
|
-
const testGen = new TestGenerator(schemas, representation, config, apiConfig)
|
|
55
|
-
const testResult = await testGen.writeAll(force)
|
|
56
|
-
|
|
57
|
-
const docGen = new DocGenerator(schemas, representation, config, apiConfig)
|
|
58
|
-
const docResult = await docGen.writeAll(force)
|
|
59
|
-
|
|
60
|
-
const written = [
|
|
61
|
-
...apiResult.written,
|
|
62
|
-
...routeResult.written,
|
|
63
|
-
...testResult.written,
|
|
64
|
-
...docResult.written,
|
|
65
|
-
]
|
|
66
|
-
const skipped = [
|
|
67
|
-
...apiResult.skipped,
|
|
68
|
-
...routeResult.skipped,
|
|
69
|
-
...testResult.skipped,
|
|
70
|
-
...docResult.skipped,
|
|
71
|
-
]
|
|
72
|
-
|
|
73
|
-
if (written.length === 0 && skipped.length === 0) {
|
|
74
|
-
console.log(chalk.yellow('No API files to generate.'))
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
for (const file of written) {
|
|
79
|
-
console.log(chalk.green(` CREATE `) + chalk.dim(file.path))
|
|
80
|
-
}
|
|
81
|
-
for (const file of skipped) {
|
|
82
|
-
console.log(chalk.yellow(` SKIP `) + chalk.dim(file.path) + chalk.dim(' (already exists)'))
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (skipped.length > 0) {
|
|
86
|
-
console.log(chalk.dim(`\nSkipped ${skipped.length} existing file(s). Use --force to overwrite.`))
|
|
87
|
-
}
|
|
88
|
-
} catch (err) {
|
|
89
|
-
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
90
|
-
process.exit(1)
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path'
|
|
2
|
-
import type { Command } from 'commander'
|
|
3
|
-
import chalk from 'chalk'
|
|
4
|
-
|
|
5
|
-
export function register(program: Command): void {
|
|
6
|
-
program
|
|
7
|
-
.command('generate:key')
|
|
8
|
-
.alias('g:key')
|
|
9
|
-
.description('Generate an APP_KEY and write it to the .env file')
|
|
10
|
-
.option('-f, --force', 'Overwrite existing APP_KEY if present')
|
|
11
|
-
.action(async ({ force }: { force?: boolean }) => {
|
|
12
|
-
try {
|
|
13
|
-
const key = crypto.randomUUID()
|
|
14
|
-
const envPath = join(process.cwd(), '.env')
|
|
15
|
-
const file = Bun.file(envPath)
|
|
16
|
-
|
|
17
|
-
if (await file.exists()) {
|
|
18
|
-
const contents = await file.text()
|
|
19
|
-
const hasKey = /^APP_KEY\s*=/m.test(contents)
|
|
20
|
-
|
|
21
|
-
if (hasKey && !force) {
|
|
22
|
-
const current = contents.match(/^APP_KEY\s*=\s*(.*)$/m)?.[1] ?? ''
|
|
23
|
-
if (current) {
|
|
24
|
-
console.log(chalk.yellow('APP_KEY already exists in .env. Use --force to overwrite.'))
|
|
25
|
-
return
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (hasKey) {
|
|
30
|
-
const updated = contents.replace(/^APP_KEY\s*=.*$/m, `APP_KEY=${key}`)
|
|
31
|
-
await Bun.write(envPath, updated)
|
|
32
|
-
} else {
|
|
33
|
-
const separator = contents.endsWith('\n') ? '' : '\n'
|
|
34
|
-
await Bun.write(envPath, contents + separator + `APP_KEY=${key}\n`)
|
|
35
|
-
}
|
|
36
|
-
} else {
|
|
37
|
-
await Bun.write(envPath, `APP_KEY=${key}\n`)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
console.log(chalk.green('APP_KEY generated successfully.'))
|
|
41
|
-
console.log(chalk.dim(` ${key}`))
|
|
42
|
-
} catch (err) {
|
|
43
|
-
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
44
|
-
process.exit(1)
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { Command } from 'commander'
|
|
2
|
-
import chalk from 'chalk'
|
|
3
|
-
import SchemaRegistry from '@strav/database/schema/registry'
|
|
4
|
-
import ModelGenerator from '../generators/model_generator.ts'
|
|
5
|
-
import { loadGeneratorConfig, getDatabasePaths, loadTenantIdType } from '../config/loader.ts'
|
|
6
|
-
|
|
7
|
-
export function register(program: Command): void {
|
|
8
|
-
program
|
|
9
|
-
.command('generate:models')
|
|
10
|
-
.alias('g:models')
|
|
11
|
-
.description('Generate model classes and enums from schema definitions')
|
|
12
|
-
.option('-f, --force', 'Overwrite existing generated files')
|
|
13
|
-
.action(async ({ force }: { force?: boolean }) => {
|
|
14
|
-
try {
|
|
15
|
-
const dbPaths = await getDatabasePaths()
|
|
16
|
-
const config = await loadGeneratorConfig()
|
|
17
|
-
|
|
18
|
-
console.log(chalk.cyan('Generating models from schemas...'))
|
|
19
|
-
|
|
20
|
-
const registry = new SchemaRegistry()
|
|
21
|
-
await registry.discover(dbPaths.schemas)
|
|
22
|
-
registry.validate()
|
|
23
|
-
|
|
24
|
-
const tenantIdType = await loadTenantIdType()
|
|
25
|
-
const representation = registry.buildRepresentation(tenantIdType)
|
|
26
|
-
const generator = new ModelGenerator(registry.all(), representation, config)
|
|
27
|
-
const { written, skipped } = await generator.writeAll(force)
|
|
28
|
-
|
|
29
|
-
if (written.length === 0 && skipped.length === 0) {
|
|
30
|
-
console.log(chalk.yellow('No models to generate.'))
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
for (const file of written) {
|
|
35
|
-
console.log(chalk.green(` CREATE `) + chalk.dim(file.path))
|
|
36
|
-
}
|
|
37
|
-
for (const file of skipped) {
|
|
38
|
-
console.log(chalk.yellow(` SKIP `) + chalk.dim(file.path) + chalk.dim(' (already exists)'))
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (skipped.length > 0) {
|
|
42
|
-
console.log(chalk.dim(`\nSkipped ${skipped.length} existing file(s). Use --force to overwrite.`))
|
|
43
|
-
}
|
|
44
|
-
} catch (err) {
|
|
45
|
-
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
46
|
-
process.exit(1)
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
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 '@strav/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 '@strav/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 '@strav/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
|
-
}
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import type { Command } from 'commander'
|
|
2
|
-
import chalk from 'chalk'
|
|
3
|
-
import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
4
|
-
import SchemaDiffer from '@strav/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(database.tenantIdType)
|
|
20
|
-
const actual = await introspector.introspect()
|
|
21
|
-
const diff = new SchemaDiffer().diff(desired, actual)
|
|
22
|
-
|
|
23
|
-
const hasChanges =
|
|
24
|
-
diff.extensions.length > 0 ||
|
|
25
|
-
diff.enums.length > 0 ||
|
|
26
|
-
diff.tables.length > 0 ||
|
|
27
|
-
diff.constraints.length > 0 ||
|
|
28
|
-
diff.indexes.length > 0
|
|
29
|
-
|
|
30
|
-
if (!hasChanges) {
|
|
31
|
-
console.log(chalk.green('Schema is in sync with the database.'))
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// --- Extension changes ---
|
|
36
|
-
if (diff.extensions.length > 0) {
|
|
37
|
-
console.log(chalk.bold('Extension changes:'))
|
|
38
|
-
for (const x of diff.extensions) {
|
|
39
|
-
if (x.kind === 'create') {
|
|
40
|
-
console.log(chalk.green(` + CREATE EXTENSION ${x.name}`))
|
|
41
|
-
} else if (x.kind === 'drop') {
|
|
42
|
-
console.log(chalk.red(` - DROP EXTENSION ${x.name}`))
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
console.log()
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// --- Enum changes ---
|
|
49
|
-
if (diff.enums.length > 0) {
|
|
50
|
-
console.log(chalk.bold('Enum changes:'))
|
|
51
|
-
for (const e of diff.enums) {
|
|
52
|
-
if (e.kind === 'create') {
|
|
53
|
-
console.log(chalk.green(` + CREATE ${e.name} (${e.values.join(', ')})`))
|
|
54
|
-
} else if (e.kind === 'drop') {
|
|
55
|
-
console.log(chalk.red(` - DROP ${e.name}`))
|
|
56
|
-
} else if (e.kind === 'modify') {
|
|
57
|
-
console.log(chalk.yellow(` ~ MODIFY ${e.name} (add: ${e.addedValues.join(', ')})`))
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
console.log()
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// --- Table changes ---
|
|
64
|
-
if (diff.tables.length > 0) {
|
|
65
|
-
console.log(chalk.bold('Table changes:'))
|
|
66
|
-
for (const t of diff.tables) {
|
|
67
|
-
if (t.kind === 'create') {
|
|
68
|
-
console.log(
|
|
69
|
-
chalk.green(` + CREATE ${t.table.name}`) +
|
|
70
|
-
chalk.dim(` (${t.table.columns.length} columns)`)
|
|
71
|
-
)
|
|
72
|
-
} else if (t.kind === 'drop') {
|
|
73
|
-
console.log(chalk.red(` - DROP ${t.table.name}`))
|
|
74
|
-
} else if (t.kind === 'modify') {
|
|
75
|
-
console.log(chalk.yellow(` ~ MODIFY ${t.tableName}`))
|
|
76
|
-
for (const c of t.columns) {
|
|
77
|
-
if (c.kind === 'add') {
|
|
78
|
-
console.log(
|
|
79
|
-
chalk.green(
|
|
80
|
-
` + ADD COLUMN ${c.column.name} (${typeof c.column.pgType === 'string' ? c.column.pgType : 'custom'})`
|
|
81
|
-
)
|
|
82
|
-
)
|
|
83
|
-
} else if (c.kind === 'drop') {
|
|
84
|
-
console.log(chalk.red(` - DROP COLUMN ${c.column.name}`))
|
|
85
|
-
} else if (c.kind === 'alter') {
|
|
86
|
-
const changes: string[] = []
|
|
87
|
-
if (c.typeChange) changes.push(`type: ${c.typeChange.from} -> ${c.typeChange.to}`)
|
|
88
|
-
if (c.nullableChange)
|
|
89
|
-
changes.push(c.nullableChange.to ? 'set NOT NULL' : 'drop NOT NULL')
|
|
90
|
-
if (c.defaultChange) changes.push('default changed')
|
|
91
|
-
console.log(
|
|
92
|
-
chalk.yellow(` ~ ALTER COLUMN ${c.columnName} (${changes.join(', ')})`)
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
console.log()
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// --- Constraint changes ---
|
|
102
|
-
if (diff.constraints.length > 0) {
|
|
103
|
-
console.log(chalk.bold('Constraint changes:'))
|
|
104
|
-
for (const c of diff.constraints) {
|
|
105
|
-
if (c.kind === 'add_fk') {
|
|
106
|
-
console.log(
|
|
107
|
-
chalk.green(
|
|
108
|
-
` + ADD FK ${c.tableName}(${c.constraint.columns.join(',')}) -> ${c.constraint.referencedTable}`
|
|
109
|
-
)
|
|
110
|
-
)
|
|
111
|
-
} else if (c.kind === 'drop_fk') {
|
|
112
|
-
console.log(
|
|
113
|
-
chalk.red(
|
|
114
|
-
` - DROP FK ${c.tableName}(${c.constraint.columns.join(',')}) -> ${c.constraint.referencedTable}`
|
|
115
|
-
)
|
|
116
|
-
)
|
|
117
|
-
} else if (c.kind === 'add_unique') {
|
|
118
|
-
console.log(
|
|
119
|
-
chalk.green(` + ADD UQ ${c.tableName}(${c.constraint.columns.join(',')})`)
|
|
120
|
-
)
|
|
121
|
-
} else if (c.kind === 'drop_unique') {
|
|
122
|
-
console.log(
|
|
123
|
-
chalk.red(` - DROP UQ ${c.tableName}(${c.constraint.columns.join(',')})`)
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
console.log()
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// --- Index changes ---
|
|
131
|
-
if (diff.indexes.length > 0) {
|
|
132
|
-
console.log(chalk.bold('Index changes:'))
|
|
133
|
-
for (const i of diff.indexes) {
|
|
134
|
-
if (i.kind === 'add') {
|
|
135
|
-
const unique = i.index.unique ? 'UNIQUE ' : ''
|
|
136
|
-
console.log(
|
|
137
|
-
chalk.green(
|
|
138
|
-
` + CREATE ${unique}INDEX ${i.tableName}(${i.index.columns.join(',')})`
|
|
139
|
-
)
|
|
140
|
-
)
|
|
141
|
-
} else if (i.kind === 'drop') {
|
|
142
|
-
console.log(
|
|
143
|
-
chalk.red(` - DROP INDEX ${i.tableName}(${i.index.columns.join(',')})`)
|
|
144
|
-
)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
console.log()
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// --- Summary ---
|
|
151
|
-
const creates = diff.tables.filter(t => t.kind === 'create').length
|
|
152
|
-
const drops = diff.tables.filter(t => t.kind === 'drop').length
|
|
153
|
-
const modifies = diff.tables.filter(t => t.kind === 'modify').length
|
|
154
|
-
console.log(
|
|
155
|
-
chalk.bold('Summary: ') +
|
|
156
|
-
`${creates} table(s) to create, ${drops} to drop, ${modifies} to modify, ` +
|
|
157
|
-
`${diff.enums.length} enum change(s), ${diff.constraints.length} constraint change(s), ${diff.indexes.length} index change(s), ${diff.extensions.length} extension change(s)`
|
|
158
|
-
)
|
|
159
|
-
console.log(chalk.dim('\nRun "bun strav generate:migration" to create migration files.'))
|
|
160
|
-
} catch (err) {
|
|
161
|
-
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
162
|
-
process.exit(1)
|
|
163
|
-
} finally {
|
|
164
|
-
if (db) await shutdown(db)
|
|
165
|
-
}
|
|
166
|
-
})
|
|
167
|
-
}
|