@strav/cli 0.3.33 → 0.4.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 +6 -6
- package/src/cli/bootstrap.ts +2 -19
- package/src/commands/db_setup_roles.ts +91 -0
- package/src/commands/generate_models.ts +11 -87
- package/src/commands/migration_fresh.ts +16 -33
- package/src/commands/migration_generate.ts +4 -20
- package/src/commands/migration_rollback.ts +3 -15
- package/src/commands/migration_run.ts +3 -15
- package/src/commands/tenant_create.ts +35 -0
- package/src/commands/tenant_delete.ts +64 -0
- package/src/commands/tenant_list.ts +39 -0
- package/src/config/loader.ts +4 -11
- package/src/generators/config.ts +3 -41
- package/src/generators/model_generator.ts +7 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strav/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI framework and code generators for the Strav framework",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
"strav": "./src/cli/strav.ts"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@strav/kernel": "0.
|
|
37
|
-
"@strav/http": "0.
|
|
38
|
-
"@strav/database": "0.
|
|
39
|
-
"@strav/queue": "0.
|
|
40
|
-
"@strav/signal": "0.
|
|
36
|
+
"@strav/kernel": "0.4.0",
|
|
37
|
+
"@strav/http": "0.4.0",
|
|
38
|
+
"@strav/database": "0.4.0",
|
|
39
|
+
"@strav/queue": "0.4.0",
|
|
40
|
+
"@strav/signal": "0.4.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"chalk": "^5.6.2",
|
package/src/cli/bootstrap.ts
CHANGED
|
@@ -4,7 +4,6 @@ import SchemaRegistry from '@strav/database/schema/registry'
|
|
|
4
4
|
import DatabaseIntrospector from '@strav/database/database/introspector'
|
|
5
5
|
import Application from '@strav/kernel/core/application'
|
|
6
6
|
import type ServiceProvider from '@strav/kernel/core/service_provider'
|
|
7
|
-
import { discoverDomains } from '@strav/database'
|
|
8
7
|
import { getDatabasePaths } from '../config/loader.ts'
|
|
9
8
|
|
|
10
9
|
export interface BootstrapResult {
|
|
@@ -19,32 +18,16 @@ export interface BootstrapResult {
|
|
|
19
18
|
*
|
|
20
19
|
* Loads configuration, connects to the database, discovers and validates
|
|
21
20
|
* schemas, and creates an introspector instance.
|
|
22
|
-
*
|
|
23
|
-
* @param scope - Optional domain to discover schemas from (e.g., 'public', 'tenant', 'factory')
|
|
24
21
|
*/
|
|
25
|
-
export async function bootstrap(
|
|
22
|
+
export async function bootstrap(): Promise<BootstrapResult> {
|
|
26
23
|
const config = new Configuration('./config')
|
|
27
24
|
await config.load()
|
|
28
25
|
|
|
29
26
|
const db = new Database(config)
|
|
30
27
|
|
|
31
28
|
const registry = new SchemaRegistry()
|
|
32
|
-
|
|
33
|
-
// Get the configured database paths
|
|
34
29
|
const dbPaths = await getDatabasePaths()
|
|
35
|
-
|
|
36
|
-
if (scope && scope !== 'public') {
|
|
37
|
-
// For non-public domains, we need to load public schemas first since they may reference them
|
|
38
|
-
await registry.discover(dbPaths.schemas, 'public')
|
|
39
|
-
await registry.discover(dbPaths.schemas, scope)
|
|
40
|
-
} else if (scope === 'public') {
|
|
41
|
-
// For public schemas, only load public
|
|
42
|
-
await registry.discover(dbPaths.schemas, 'public')
|
|
43
|
-
} else {
|
|
44
|
-
// Default: discover all schemas (backward compatibility)
|
|
45
|
-
await registry.discover(dbPaths.schemas)
|
|
46
|
-
}
|
|
47
|
-
|
|
30
|
+
await registry.discover(dbPaths.schemas)
|
|
48
31
|
registry.validate()
|
|
49
32
|
|
|
50
33
|
const introspector = new DatabaseIntrospector(db)
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "${appUser}", "${bypassUser}";`,
|
|
48
|
+
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "${appUser}", "${bypassUser}";`,
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
if (!opts.apply) {
|
|
52
|
+
console.log(chalk.cyan('\n-- Run as a PostgreSQL superuser:\n'))
|
|
53
|
+
for (const s of stmts) console.log(s)
|
|
54
|
+
console.log(
|
|
55
|
+
chalk.dim('\n(Use --apply to execute these against the configured database.)')
|
|
56
|
+
)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const password = opts.superuserPassword ?? env('DB_SUPERUSER_PASSWORD', '')
|
|
61
|
+
const sql = new SQL({
|
|
62
|
+
hostname: config.get('database.host') ?? env('DB_HOST', '127.0.0.1'),
|
|
63
|
+
port: config.get('database.port') ?? env.int('DB_PORT', 5432),
|
|
64
|
+
username: opts.superuser,
|
|
65
|
+
password,
|
|
66
|
+
database: dbName,
|
|
67
|
+
max: 1,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
console.log(chalk.cyan(`Applying role setup as superuser "${opts.superuser}"...`))
|
|
71
|
+
for (const stmt of stmts) {
|
|
72
|
+
try {
|
|
73
|
+
await sql.unsafe(stmt)
|
|
74
|
+
console.log(chalk.dim(` ok: ${stmt}`))
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
77
|
+
if (/already exists/i.test(msg)) {
|
|
78
|
+
console.log(chalk.dim(` skip (exists): ${stmt}`))
|
|
79
|
+
} else {
|
|
80
|
+
throw err
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
await sql.close()
|
|
85
|
+
console.log(chalk.green('\nRoles set up.'))
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
88
|
+
process.exit(1)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { join } from 'node:path'
|
|
2
1
|
import type { Command } from 'commander'
|
|
3
2
|
import chalk from 'chalk'
|
|
4
3
|
import SchemaRegistry from '@strav/database/schema/registry'
|
|
5
4
|
import ModelGenerator from '../generators/model_generator.ts'
|
|
6
|
-
import type { GeneratorConfig } from '../generators/config.ts'
|
|
7
|
-
import { discoverDomains } from '@strav/database'
|
|
8
5
|
import { loadGeneratorConfig, getDatabasePaths } from '../config/loader.ts'
|
|
9
6
|
|
|
10
7
|
export function register(program: Command): void {
|
|
@@ -12,101 +9,28 @@ export function register(program: Command): void {
|
|
|
12
9
|
.command('generate:models')
|
|
13
10
|
.alias('g:models')
|
|
14
11
|
.description('Generate model classes and enums from schema definitions')
|
|
15
|
-
.
|
|
16
|
-
.action(async (options) => {
|
|
12
|
+
.action(async () => {
|
|
17
13
|
try {
|
|
18
|
-
const scope = options.scope as string
|
|
19
|
-
|
|
20
|
-
// Get configured database paths
|
|
21
14
|
const dbPaths = await getDatabasePaths()
|
|
22
|
-
|
|
23
|
-
// Validate scope against available domains or 'all'
|
|
24
|
-
const availableDomains = discoverDomains(dbPaths.schemas)
|
|
25
|
-
if (scope !== 'all' && !availableDomains.includes(scope)) {
|
|
26
|
-
console.error(chalk.red(`Invalid domain: ${scope}. Available domains: ${availableDomains.join(', ')}, all`))
|
|
27
|
-
process.exit(1)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
console.log(chalk.cyan(`Generating models from ${scope === 'all' ? 'all' : scope} schemas...`))
|
|
31
|
-
|
|
32
|
-
// Load generator config (if available)
|
|
33
15
|
const config = await loadGeneratorConfig()
|
|
34
16
|
|
|
35
|
-
|
|
36
|
-
const scopesToProcess = scope === 'all' ? availableDomains : [scope]
|
|
37
|
-
|
|
38
|
-
// When generating models for specific domains, we need all schemas loaded
|
|
39
|
-
// to handle cross-domain references properly
|
|
40
|
-
const fullRegistry = new SchemaRegistry()
|
|
41
|
-
|
|
42
|
-
// Always load public schemas first (base schemas)
|
|
43
|
-
await fullRegistry.discover(dbPaths.schemas, 'public')
|
|
44
|
-
|
|
45
|
-
// Load schemas from all other domains if needed for validation or if generating models for those domains
|
|
46
|
-
for (const domain of availableDomains) {
|
|
47
|
-
if (domain !== 'public' && (scope === 'all' || scope === domain)) {
|
|
48
|
-
await fullRegistry.discover(dbPaths.schemas, domain)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
17
|
+
console.log(chalk.cyan('Generating models from schemas...'))
|
|
51
18
|
|
|
52
|
-
|
|
53
|
-
|
|
19
|
+
const registry = new SchemaRegistry()
|
|
20
|
+
await registry.discover(dbPaths.schemas)
|
|
21
|
+
registry.validate()
|
|
54
22
|
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
// Build a map of schema name -> domain for cross-domain reference resolution
|
|
59
|
-
const allSchemasMap = new Map<string, string>()
|
|
60
|
-
|
|
61
|
-
// Load public schemas to identify which ones are public
|
|
62
|
-
const publicRegistry = new SchemaRegistry()
|
|
63
|
-
await publicRegistry.discover(dbPaths.schemas, 'public')
|
|
64
|
-
const publicSchemaNames = new Set(publicRegistry.all().map(s => s.name))
|
|
65
|
-
|
|
66
|
-
// Map schemas to their respective domains by checking which domain directory they came from
|
|
67
|
-
for (const schema of fullRegistry.all()) {
|
|
68
|
-
if (publicSchemaNames.has(schema.name)) {
|
|
69
|
-
allSchemasMap.set(schema.name, 'public')
|
|
70
|
-
} else {
|
|
71
|
-
// For non-public schemas, find which domain they belong to
|
|
72
|
-
// by checking which domains were loaded
|
|
73
|
-
for (const domain of availableDomains) {
|
|
74
|
-
if (domain !== 'public') {
|
|
75
|
-
const domainRegistry = new SchemaRegistry()
|
|
76
|
-
await domainRegistry.discover(dbPaths.schemas, domain)
|
|
77
|
-
const domainSchemaNames = new Set(domainRegistry.all().map(s => s.name))
|
|
78
|
-
if (domainSchemaNames.has(schema.name)) {
|
|
79
|
-
allSchemasMap.set(schema.name, domain)
|
|
80
|
-
break
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
for (const currentScope of scopesToProcess) {
|
|
88
|
-
// Get just the schemas for the current scope
|
|
89
|
-
const scopeRegistry = new SchemaRegistry()
|
|
90
|
-
await scopeRegistry.discover(dbPaths.schemas, currentScope)
|
|
91
|
-
|
|
92
|
-
if (scopeRegistry.all().length === 0) {
|
|
93
|
-
console.log(chalk.yellow(`No schemas found for domain: ${currentScope}`))
|
|
94
|
-
continue
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const scopeSchemas = scopeRegistry.all()
|
|
98
|
-
const generator = new ModelGenerator(scopeSchemas, fullRepresentation, config, currentScope, allSchemasMap)
|
|
99
|
-
const files = await generator.writeAll()
|
|
100
|
-
allFiles.push(...files)
|
|
101
|
-
}
|
|
23
|
+
const representation = registry.buildRepresentation()
|
|
24
|
+
const generator = new ModelGenerator(registry.all(), representation, config)
|
|
25
|
+
const files = await generator.writeAll()
|
|
102
26
|
|
|
103
|
-
if (
|
|
27
|
+
if (files.length === 0) {
|
|
104
28
|
console.log(chalk.yellow('No models to generate.'))
|
|
105
29
|
return
|
|
106
30
|
}
|
|
107
31
|
|
|
108
|
-
console.log(chalk.green(`\nGenerated ${
|
|
109
|
-
for (const file of
|
|
32
|
+
console.log(chalk.green(`\nGenerated ${files.length} file(s):`))
|
|
33
|
+
for (const file of files) {
|
|
110
34
|
console.log(chalk.dim(` ${file.path}`))
|
|
111
35
|
}
|
|
112
36
|
} catch (err) {
|
|
@@ -11,7 +11,6 @@ import SqlGenerator from '@strav/database/database/migration/sql_generator'
|
|
|
11
11
|
import MigrationFileGenerator from '@strav/database/database/migration/file_generator'
|
|
12
12
|
import MigrationTracker from '@strav/database/database/migration/tracker'
|
|
13
13
|
import MigrationRunner from '@strav/database/database/migration/runner'
|
|
14
|
-
import { discoverDomains } from '@strav/database'
|
|
15
14
|
import { getDatabasePaths } from '../config/loader.ts'
|
|
16
15
|
|
|
17
16
|
/**
|
|
@@ -24,37 +23,34 @@ export async function freshDatabase(
|
|
|
24
23
|
db: Database,
|
|
25
24
|
registry: SchemaRegistry,
|
|
26
25
|
introspector: DatabaseIntrospector,
|
|
27
|
-
migrationsPath: string = 'database/migrations'
|
|
28
|
-
scope: string = 'public'
|
|
26
|
+
migrationsPath: string = 'database/migrations'
|
|
29
27
|
): Promise<number> {
|
|
30
|
-
// Drop all tables in public schema
|
|
31
28
|
console.log(chalk.cyan('\nDropping all tables and types...'))
|
|
32
29
|
|
|
33
|
-
const
|
|
30
|
+
const conn = db.isMultiTenant ? db.bypass : db.sql
|
|
31
|
+
|
|
32
|
+
const tables = await conn.unsafe(`
|
|
34
33
|
SELECT table_name FROM information_schema.tables
|
|
35
34
|
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
36
|
-
`
|
|
37
|
-
for (const row of tables) {
|
|
38
|
-
await
|
|
35
|
+
`)
|
|
36
|
+
for (const row of tables as Array<{ table_name: string }>) {
|
|
37
|
+
await conn.unsafe(`DROP TABLE IF EXISTS "${row.table_name}" CASCADE`)
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
const types = await db.sql`
|
|
40
|
+
const types = await conn.unsafe(`
|
|
43
41
|
SELECT t.typname
|
|
44
42
|
FROM pg_type t
|
|
45
43
|
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
46
44
|
WHERE n.nspname = 'public'
|
|
47
45
|
AND t.typtype = 'e'
|
|
48
|
-
`
|
|
49
|
-
for (const row of types) {
|
|
50
|
-
await
|
|
46
|
+
`)
|
|
47
|
+
for (const row of types as Array<{ typname: string }>) {
|
|
48
|
+
await conn.unsafe(`DROP TYPE IF EXISTS "${row.typname}" CASCADE`)
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
// Delete existing migration files
|
|
54
51
|
console.log(chalk.cyan('Clearing migration directory...'))
|
|
55
52
|
rmSync(migrationsPath, { recursive: true, force: true })
|
|
56
53
|
|
|
57
|
-
// Generate new migration
|
|
58
54
|
console.log(chalk.cyan('Generating fresh migration...'))
|
|
59
55
|
|
|
60
56
|
const desired = registry.buildRepresentation()
|
|
@@ -68,11 +64,10 @@ export async function freshDatabase(
|
|
|
68
64
|
const fileGen = new MigrationFileGenerator(migrationsPath)
|
|
69
65
|
await fileGen.generate(version, 'fresh', sql, diff, tableOrder)
|
|
70
66
|
|
|
71
|
-
// Run the migration
|
|
72
67
|
console.log(chalk.cyan('Running migration...'))
|
|
73
68
|
|
|
74
|
-
const tracker = new MigrationTracker(db
|
|
75
|
-
const runner = new MigrationRunner(db, tracker, migrationsPath
|
|
69
|
+
const tracker = new MigrationTracker(db)
|
|
70
|
+
const runner = new MigrationRunner(db, tracker, migrationsPath)
|
|
76
71
|
const result = await runner.run()
|
|
77
72
|
|
|
78
73
|
return result.applied.length
|
|
@@ -101,8 +96,7 @@ export function register(program: Command): void {
|
|
|
101
96
|
.command('fresh')
|
|
102
97
|
.alias('migration:fresh')
|
|
103
98
|
.description('Reset database and migrations, regenerate and run from scratch')
|
|
104
|
-
.
|
|
105
|
-
.action(async (opts: { scope: string }) => {
|
|
99
|
+
.action(async () => {
|
|
106
100
|
requireLocalEnv('fresh')
|
|
107
101
|
|
|
108
102
|
// 6-digit challenge
|
|
@@ -123,22 +117,11 @@ export function register(program: Command): void {
|
|
|
123
117
|
|
|
124
118
|
let db
|
|
125
119
|
try {
|
|
126
|
-
// Get configured database paths
|
|
127
120
|
const dbPaths = await getDatabasePaths()
|
|
128
|
-
|
|
129
|
-
// Validate scope against available domains
|
|
130
|
-
const availableDomains = discoverDomains(dbPaths.schemas)
|
|
131
|
-
if (!availableDomains.includes(opts.scope)) {
|
|
132
|
-
throw new Error(`Invalid domain: ${opts.scope}. Available domains: ${availableDomains.join(', ')}`)
|
|
133
|
-
}
|
|
134
|
-
const scope = opts.scope
|
|
135
|
-
|
|
136
|
-
const { db: database, registry, introspector } = await bootstrap(scope)
|
|
121
|
+
const { db: database, registry, introspector } = await bootstrap()
|
|
137
122
|
db = database
|
|
138
123
|
|
|
139
|
-
|
|
140
|
-
const scopedPath = `${dbPaths.migrations}/${scope}`
|
|
141
|
-
const applied = await freshDatabase(db, registry, introspector, scopedPath, scope)
|
|
124
|
+
const applied = await freshDatabase(db, registry, introspector, dbPaths.migrations)
|
|
142
125
|
|
|
143
126
|
console.log(chalk.green(`\nFresh migration complete. Applied ${applied} migration(s).`))
|
|
144
127
|
} catch (err) {
|
|
@@ -4,7 +4,6 @@ import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
|
4
4
|
import SchemaDiffer from '@strav/database/database/migration/differ'
|
|
5
5
|
import SqlGenerator from '@strav/database/database/migration/sql_generator'
|
|
6
6
|
import MigrationFileGenerator from '@strav/database/database/migration/file_generator'
|
|
7
|
-
import { discoverDomains } from '@strav/database'
|
|
8
7
|
import { getDatabasePaths } from '../config/loader.ts'
|
|
9
8
|
|
|
10
9
|
export function register(program: Command): void {
|
|
@@ -13,24 +12,14 @@ export function register(program: Command): void {
|
|
|
13
12
|
.aliases(['migration:generate', 'g:migration'])
|
|
14
13
|
.description('Generate migration files from schema changes')
|
|
15
14
|
.option('-m, --message <message>', 'Migration message', 'migration')
|
|
16
|
-
.
|
|
17
|
-
.action(async (opts: { message: string; scope: string }) => {
|
|
15
|
+
.action(async (opts: { message: string }) => {
|
|
18
16
|
let db
|
|
19
17
|
try {
|
|
20
|
-
// Get configured database paths
|
|
21
18
|
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)
|
|
19
|
+
const { db: database, registry, introspector } = await bootstrap()
|
|
31
20
|
db = database
|
|
32
21
|
|
|
33
|
-
console.log(chalk.cyan(
|
|
22
|
+
console.log(chalk.cyan('Comparing schema with database...'))
|
|
34
23
|
|
|
35
24
|
const desired = registry.buildRepresentation()
|
|
36
25
|
const actual = await introspector.introspect()
|
|
@@ -49,16 +38,11 @@ export function register(program: Command): void {
|
|
|
49
38
|
|
|
50
39
|
const sql = new SqlGenerator().generate(diff)
|
|
51
40
|
const version = Date.now().toString()
|
|
52
|
-
|
|
53
|
-
// Use the table order from the desired representation (already dependency-ordered)
|
|
54
41
|
const tableOrder = desired.tables.map(t => t.name)
|
|
55
42
|
|
|
56
|
-
|
|
57
|
-
const scopedPath = `${dbPaths.migrations}/${scope}`
|
|
58
|
-
const fileGen = new MigrationFileGenerator(scopedPath)
|
|
43
|
+
const fileGen = new MigrationFileGenerator(dbPaths.migrations)
|
|
59
44
|
const dir = await fileGen.generate(version, opts.message, sql, diff, tableOrder)
|
|
60
45
|
|
|
61
|
-
// Print summary
|
|
62
46
|
console.log(chalk.green(`\nMigration generated: ${version}`))
|
|
63
47
|
console.log(chalk.dim(` Directory: ${dir}`))
|
|
64
48
|
console.log(chalk.dim(` Message: ${opts.message}\n`))
|
|
@@ -3,7 +3,6 @@ import chalk from 'chalk'
|
|
|
3
3
|
import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
4
4
|
import MigrationTracker from '@strav/database/database/migration/tracker'
|
|
5
5
|
import MigrationRunner from '@strav/database/database/migration/runner'
|
|
6
|
-
import { discoverDomains } from '@strav/database'
|
|
7
6
|
import { getDatabasePaths } from '../config/loader.ts'
|
|
8
7
|
|
|
9
8
|
export function register(program: Command): void {
|
|
@@ -12,26 +11,15 @@ export function register(program: Command): void {
|
|
|
12
11
|
.alias('migration:rollback')
|
|
13
12
|
.description('Rollback migrations by batch')
|
|
14
13
|
.option('--batch <number>', 'Specific batch number to rollback')
|
|
15
|
-
.
|
|
16
|
-
.action(async (opts: { batch?: string; scope: string }) => {
|
|
14
|
+
.action(async (opts: { batch?: string }) => {
|
|
17
15
|
let db
|
|
18
16
|
try {
|
|
19
|
-
// Get configured database paths
|
|
20
17
|
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
18
|
const { db: database } = await bootstrap()
|
|
30
19
|
db = database
|
|
31
20
|
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const runner = new MigrationRunner(db, tracker, scopedPath, scope)
|
|
21
|
+
const tracker = new MigrationTracker(db)
|
|
22
|
+
const runner = new MigrationRunner(db, tracker, dbPaths.migrations)
|
|
35
23
|
|
|
36
24
|
const batchNum = opts.batch ? parseInt(opts.batch, 10) : undefined
|
|
37
25
|
|
|
@@ -3,7 +3,6 @@ import chalk from 'chalk'
|
|
|
3
3
|
import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
4
4
|
import MigrationTracker from '@strav/database/database/migration/tracker'
|
|
5
5
|
import MigrationRunner from '@strav/database/database/migration/runner'
|
|
6
|
-
import { discoverDomains } from '@strav/database'
|
|
7
6
|
import { getDatabasePaths } from '../config/loader.ts'
|
|
8
7
|
|
|
9
8
|
export function register(program: Command): void {
|
|
@@ -11,26 +10,15 @@ export function register(program: Command): void {
|
|
|
11
10
|
.command('migrate')
|
|
12
11
|
.alias('migration:run')
|
|
13
12
|
.description('Run pending migrations')
|
|
14
|
-
.
|
|
15
|
-
.action(async (opts: { scope: string }) => {
|
|
13
|
+
.action(async () => {
|
|
16
14
|
let db
|
|
17
15
|
try {
|
|
18
|
-
// Get configured database paths
|
|
19
16
|
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
17
|
const { db: database } = await bootstrap()
|
|
29
18
|
db = database
|
|
30
19
|
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const runner = new MigrationRunner(db, tracker, scopedPath, scope)
|
|
20
|
+
const tracker = new MigrationTracker(db)
|
|
21
|
+
const runner = new MigrationRunner(db, tracker, dbPaths.migrations)
|
|
34
22
|
|
|
35
23
|
console.log(chalk.cyan('Running pending migrations...'))
|
|
36
24
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
4
|
+
import TenantManager from '@strav/database/database/tenant/manager'
|
|
5
|
+
import { ensureTenantTable } from '@strav/database/database/tenant/seed'
|
|
6
|
+
|
|
7
|
+
export function register(program: Command): void {
|
|
8
|
+
program
|
|
9
|
+
.command('tenant:create')
|
|
10
|
+
.description('Create a new tenant')
|
|
11
|
+
.requiredOption('--slug <slug>', 'Unique tenant slug (used for subdomain/URLs)')
|
|
12
|
+
.requiredOption('--name <name>', 'Tenant display name')
|
|
13
|
+
.action(async (opts: { slug: string; name: string }) => {
|
|
14
|
+
let db
|
|
15
|
+
try {
|
|
16
|
+
const { db: database } = await bootstrap()
|
|
17
|
+
db = database
|
|
18
|
+
|
|
19
|
+
await ensureTenantTable(db.bypass)
|
|
20
|
+
const manager = new TenantManager(db)
|
|
21
|
+
|
|
22
|
+
const tenant = await manager.create({ slug: opts.slug, name: opts.name })
|
|
23
|
+
|
|
24
|
+
console.log(chalk.green('\nTenant created:'))
|
|
25
|
+
console.log(chalk.dim(` id: ${tenant.id}`))
|
|
26
|
+
console.log(chalk.dim(` slug: ${tenant.slug}`))
|
|
27
|
+
console.log(chalk.dim(` name: ${tenant.name}`))
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
30
|
+
process.exit(1)
|
|
31
|
+
} finally {
|
|
32
|
+
if (db) await shutdown(db)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { createInterface } from 'node:readline'
|
|
4
|
+
import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
5
|
+
import TenantManager from '@strav/database/database/tenant/manager'
|
|
6
|
+
import { ensureTenantTable } from '@strav/database/database/tenant/seed'
|
|
7
|
+
|
|
8
|
+
export function register(program: Command): void {
|
|
9
|
+
program
|
|
10
|
+
.command('tenant:delete <id>')
|
|
11
|
+
.description('Delete a tenant and cascade-delete all their rows')
|
|
12
|
+
.option('-f, --force', 'Skip the confirmation prompt')
|
|
13
|
+
.action(async (id: string, opts: { force?: boolean }) => {
|
|
14
|
+
let db
|
|
15
|
+
try {
|
|
16
|
+
const { db: database } = await bootstrap()
|
|
17
|
+
db = database
|
|
18
|
+
|
|
19
|
+
await ensureTenantTable(db.bypass)
|
|
20
|
+
const manager = new TenantManager(db)
|
|
21
|
+
|
|
22
|
+
const tenant = await manager.find(id)
|
|
23
|
+
if (!tenant) {
|
|
24
|
+
console.error(chalk.red(`Tenant not found: ${id}`))
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!opts.force) {
|
|
29
|
+
console.log(
|
|
30
|
+
chalk.red('WARNING: ') +
|
|
31
|
+
`This will delete tenant "${tenant.slug}" and ` +
|
|
32
|
+
chalk.red('cascade-delete all their data') +
|
|
33
|
+
' across every tenant-scoped table.'
|
|
34
|
+
)
|
|
35
|
+
const challenge = tenant.slug
|
|
36
|
+
console.log(`\n Type ${chalk.yellow(challenge)} to confirm:\n`)
|
|
37
|
+
|
|
38
|
+
const answer = await prompt(' > ')
|
|
39
|
+
if (answer.trim() !== challenge) {
|
|
40
|
+
console.error(chalk.red('\nConfirmation does not match. Operation cancelled.'))
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await manager.delete(id)
|
|
46
|
+
console.log(chalk.green(`\nTenant "${tenant.slug}" deleted.`))
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
49
|
+
process.exit(1)
|
|
50
|
+
} finally {
|
|
51
|
+
if (db) await shutdown(db)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function prompt(question: string): Promise<string> {
|
|
57
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
58
|
+
return new Promise(resolve => {
|
|
59
|
+
rl.question(question, answer => {
|
|
60
|
+
rl.close()
|
|
61
|
+
resolve(answer)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
4
|
+
import TenantManager from '@strav/database/database/tenant/manager'
|
|
5
|
+
import { ensureTenantTable } from '@strav/database/database/tenant/seed'
|
|
6
|
+
|
|
7
|
+
export function register(program: Command): void {
|
|
8
|
+
program
|
|
9
|
+
.command('tenant:list')
|
|
10
|
+
.description('List all tenants')
|
|
11
|
+
.action(async () => {
|
|
12
|
+
let db
|
|
13
|
+
try {
|
|
14
|
+
const { db: database } = await bootstrap()
|
|
15
|
+
db = database
|
|
16
|
+
|
|
17
|
+
await ensureTenantTable(db.bypass)
|
|
18
|
+
const manager = new TenantManager(db)
|
|
19
|
+
const tenants = await manager.list()
|
|
20
|
+
|
|
21
|
+
if (tenants.length === 0) {
|
|
22
|
+
console.log(chalk.yellow('No tenants found.'))
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(chalk.cyan(`\n${tenants.length} tenant(s):\n`))
|
|
27
|
+
for (const t of tenants) {
|
|
28
|
+
console.log(` ${chalk.green(t.slug)}`)
|
|
29
|
+
console.log(chalk.dim(` id: ${t.id}`))
|
|
30
|
+
console.log(chalk.dim(` name: ${t.name}`))
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
34
|
+
process.exit(1)
|
|
35
|
+
} finally {
|
|
36
|
+
if (db) await shutdown(db)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
}
|
package/src/config/loader.ts
CHANGED
|
@@ -10,30 +10,23 @@ export async function loadGeneratorConfig(): Promise<GeneratorConfig | undefined
|
|
|
10
10
|
try {
|
|
11
11
|
return (await import(join(process.cwd(), 'config/generators.ts'))).default
|
|
12
12
|
} catch {
|
|
13
|
-
// No config/generators.ts — use defaults
|
|
14
13
|
return undefined
|
|
15
14
|
}
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* Get the fully resolved database paths from the configuration.
|
|
20
|
-
* This includes the schemas and migrations paths with defaults.
|
|
21
19
|
*/
|
|
22
20
|
export async function getDatabasePaths(): Promise<{ schemas: string; migrations: string }> {
|
|
23
21
|
const config = await loadGeneratorConfig()
|
|
24
22
|
const paths = resolvePaths(config)
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
schemas: paths.schemas,
|
|
28
|
-
migrations: paths.migrations,
|
|
29
|
-
}
|
|
23
|
+
return { schemas: paths.schemas, migrations: paths.migrations }
|
|
30
24
|
}
|
|
31
25
|
|
|
32
26
|
/**
|
|
33
27
|
* Get all resolved paths from the configuration.
|
|
34
|
-
* Useful for commands that need multiple paths.
|
|
35
28
|
*/
|
|
36
|
-
export async function getAllPaths(
|
|
29
|
+
export async function getAllPaths(): Promise<GeneratorPaths> {
|
|
37
30
|
const config = await loadGeneratorConfig()
|
|
38
|
-
return resolvePaths(config
|
|
39
|
-
}
|
|
31
|
+
return resolvePaths(config)
|
|
32
|
+
}
|
package/src/generators/config.ts
CHANGED
|
@@ -22,13 +22,8 @@ export interface GeneratorPaths {
|
|
|
22
22
|
migrations: string
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export interface ModelNaming {
|
|
26
|
-
[domain: string]: string | null | undefined
|
|
27
|
-
}
|
|
28
|
-
|
|
29
25
|
export interface GeneratorConfig {
|
|
30
26
|
paths?: Partial<GeneratorPaths>
|
|
31
|
-
modelNaming?: ModelNaming
|
|
32
27
|
}
|
|
33
28
|
|
|
34
29
|
// ---------------------------------------------------------------------------
|
|
@@ -47,7 +42,6 @@ const DEFAULT_PATHS: GeneratorPaths = {
|
|
|
47
42
|
routes: 'start',
|
|
48
43
|
tests: 'tests/api',
|
|
49
44
|
docs: 'public/_docs',
|
|
50
|
-
// Database paths
|
|
51
45
|
schemas: 'database/schemas',
|
|
52
46
|
migrations: 'database/migrations',
|
|
53
47
|
}
|
|
@@ -57,19 +51,8 @@ const DEFAULT_PATHS: GeneratorPaths = {
|
|
|
57
51
|
// ---------------------------------------------------------------------------
|
|
58
52
|
|
|
59
53
|
/** Merge user config with defaults, returning fully resolved paths. */
|
|
60
|
-
export function resolvePaths(config?: GeneratorConfig
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// If a domain is provided, append it to models and enums paths
|
|
64
|
-
if (scope) {
|
|
65
|
-
return {
|
|
66
|
-
...basePaths,
|
|
67
|
-
models: `${basePaths.models}/${scope}`,
|
|
68
|
-
enums: `${basePaths.enums}/${scope}`
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return basePaths
|
|
54
|
+
export function resolvePaths(config?: GeneratorConfig): GeneratorPaths {
|
|
55
|
+
return config?.paths ? { ...DEFAULT_PATHS, ...config.paths } : { ...DEFAULT_PATHS }
|
|
73
56
|
}
|
|
74
57
|
|
|
75
58
|
/**
|
|
@@ -79,34 +62,13 @@ export function resolvePaths(config?: GeneratorConfig, scope?: string): Generato
|
|
|
79
62
|
*/
|
|
80
63
|
export function relativeImport(fromDir: string, toDir: string): string {
|
|
81
64
|
let rel = relative(fromDir, toDir)
|
|
82
|
-
// Normalize Windows separators for import paths
|
|
83
65
|
rel = rel.split('\\').join('/')
|
|
84
66
|
return rel.startsWith('.') ? rel : './' + rel
|
|
85
67
|
}
|
|
86
68
|
|
|
87
|
-
/** Get the model name prefix for a given domain. Returns empty string if no prefix. */
|
|
88
|
-
export function getModelPrefix(config: GeneratorConfig | undefined, domain: string | undefined): string {
|
|
89
|
-
if (!domain) return ''
|
|
90
|
-
|
|
91
|
-
// Public domain has no prefix by default
|
|
92
|
-
if (domain === 'public') {
|
|
93
|
-
return config?.modelNaming?.[domain] ?? ''
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// For other domains, use configured prefix or default to PascalCase
|
|
97
|
-
if (config?.modelNaming?.[domain] !== undefined) {
|
|
98
|
-
return config.modelNaming[domain] || ''
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Default prefix: PascalCase the domain name
|
|
102
|
-
return domain.charAt(0).toUpperCase() + domain.slice(1).toLowerCase()
|
|
103
|
-
}
|
|
104
|
-
|
|
105
69
|
/**
|
|
106
70
|
* Format generated files with Prettier and write them to disk.
|
|
107
|
-
*
|
|
108
|
-
* `.prettierrc` is picked up automatically). Falls back to writing
|
|
109
|
-
* unformatted content if Prettier is not installed.
|
|
71
|
+
* Falls back to writing unformatted content if Prettier is not installed.
|
|
110
72
|
*/
|
|
111
73
|
export async function formatAndWrite(files: GeneratedFile[]): Promise<void> {
|
|
112
74
|
let prettier: typeof import('prettier') | null = null
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
import type { PostgreSQLCustomType } from '@strav/database/schema/postgres'
|
|
11
11
|
import { toSnakeCase, toCamelCase, toPascalCase } from '@strav/kernel/helpers/strings'
|
|
12
12
|
import type { GeneratorConfig, GeneratorPaths } from './config.ts'
|
|
13
|
-
import { resolvePaths, relativeImport, formatAndWrite
|
|
13
|
+
import { resolvePaths, relativeImport, formatAndWrite } from './config.ts'
|
|
14
14
|
|
|
15
15
|
export interface GeneratedFile {
|
|
16
16
|
path: string
|
|
@@ -20,19 +20,15 @@ export interface GeneratedFile {
|
|
|
20
20
|
export default class ModelGenerator {
|
|
21
21
|
private schemaMap: Map<string, SchemaDefinition>
|
|
22
22
|
private paths: GeneratorPaths
|
|
23
|
-
private modelPrefix: string
|
|
24
23
|
private config?: GeneratorConfig
|
|
25
24
|
|
|
26
25
|
constructor(
|
|
27
26
|
private schemas: SchemaDefinition[],
|
|
28
27
|
private representation: DatabaseRepresentation,
|
|
29
|
-
config?: GeneratorConfig
|
|
30
|
-
private domain?: string,
|
|
31
|
-
private allSchemasMap?: Map<string, string>
|
|
28
|
+
config?: GeneratorConfig
|
|
32
29
|
) {
|
|
33
30
|
this.schemaMap = new Map(schemas.map(s => [s.name, s]))
|
|
34
|
-
this.paths = resolvePaths(config
|
|
35
|
-
this.modelPrefix = getModelPrefix(config, domain)
|
|
31
|
+
this.paths = resolvePaths(config)
|
|
36
32
|
this.config = config
|
|
37
33
|
}
|
|
38
34
|
|
|
@@ -299,7 +295,7 @@ export default class ModelGenerator {
|
|
|
299
295
|
lines.push('// Generated by Strav — DO NOT EDIT')
|
|
300
296
|
lines.push(...importLines)
|
|
301
297
|
lines.push('')
|
|
302
|
-
lines.push(`export default class ${
|
|
298
|
+
lines.push(`export default class ${(className)} extends BaseModel {`)
|
|
303
299
|
|
|
304
300
|
if (hasSoftDeletes) {
|
|
305
301
|
lines.push(' static override softDeletes = true')
|
|
@@ -560,7 +556,7 @@ export default class ModelGenerator {
|
|
|
560
556
|
if (mode === 'named') {
|
|
561
557
|
lines.push(`export * from './${basename}'`)
|
|
562
558
|
} else {
|
|
563
|
-
const className =
|
|
559
|
+
const className = (toPascalCase(basename))
|
|
564
560
|
lines.push(`export { default as ${className} } from './${basename}'`)
|
|
565
561
|
}
|
|
566
562
|
}
|
|
@@ -577,29 +573,9 @@ export default class ModelGenerator {
|
|
|
577
573
|
// Helpers
|
|
578
574
|
// ---------------------------------------------------------------------------
|
|
579
575
|
|
|
580
|
-
/**
|
|
581
|
-
private prefixClassName(className: string): string {
|
|
582
|
-
return `${this.modelPrefix}${className}`
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
/** Apply appropriate prefix to a model name based on which domain it belongs to. */
|
|
576
|
+
/** Convert a schema name to its generated model class name. */
|
|
586
577
|
private prefixModelName(modelName: string): string {
|
|
587
|
-
|
|
588
|
-
if (!this.allSchemasMap) {
|
|
589
|
-
return this.prefixClassName(toPascalCase(modelName))
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// Check which domain this model belongs to
|
|
593
|
-
const modelDomain = this.allSchemasMap.get(modelName)
|
|
594
|
-
|
|
595
|
-
if (modelDomain) {
|
|
596
|
-
// Get the appropriate prefix for that domain
|
|
597
|
-
const prefix = getModelPrefix(this.config, modelDomain)
|
|
598
|
-
return `${prefix}${toPascalCase(modelName)}`
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Fallback to current domain prefix
|
|
602
|
-
return this.prefixClassName(toPascalCase(modelName))
|
|
578
|
+
return toPascalCase(modelName)
|
|
603
579
|
}
|
|
604
580
|
|
|
605
581
|
private isForeignKey(columnName: string, table: TableDefinition): boolean {
|