@stravigor/core 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/README.md +45 -0
- package/package.json +83 -0
- package/src/auth/access_token.ts +122 -0
- package/src/auth/auth.ts +86 -0
- package/src/auth/index.ts +7 -0
- package/src/auth/middleware/authenticate.ts +64 -0
- package/src/auth/middleware/csrf.ts +62 -0
- package/src/auth/middleware/guest.ts +46 -0
- package/src/broadcast/broadcast_manager.ts +411 -0
- package/src/broadcast/client.ts +302 -0
- package/src/broadcast/index.ts +58 -0
- package/src/cache/cache_manager.ts +56 -0
- package/src/cache/cache_store.ts +31 -0
- package/src/cache/helpers.ts +74 -0
- package/src/cache/http_cache.ts +109 -0
- package/src/cache/index.ts +6 -0
- package/src/cache/memory_store.ts +63 -0
- package/src/cli/bootstrap.ts +37 -0
- package/src/cli/commands/generate_api.ts +74 -0
- package/src/cli/commands/generate_key.ts +46 -0
- package/src/cli/commands/generate_models.ts +48 -0
- package/src/cli/commands/migration_compare.ts +152 -0
- package/src/cli/commands/migration_fresh.ts +123 -0
- package/src/cli/commands/migration_generate.ts +79 -0
- package/src/cli/commands/migration_rollback.ts +53 -0
- package/src/cli/commands/migration_run.ts +44 -0
- package/src/cli/commands/queue_flush.ts +35 -0
- package/src/cli/commands/queue_retry.ts +34 -0
- package/src/cli/commands/queue_work.ts +40 -0
- package/src/cli/commands/scheduler_work.ts +45 -0
- package/src/cli/strav.ts +33 -0
- package/src/config/configuration.ts +105 -0
- package/src/config/loaders/base_loader.ts +69 -0
- package/src/config/loaders/env_loader.ts +112 -0
- package/src/config/loaders/typescript_loader.ts +56 -0
- package/src/config/types.ts +8 -0
- package/src/core/application.ts +4 -0
- package/src/core/container.ts +117 -0
- package/src/core/index.ts +3 -0
- package/src/core/inject.ts +39 -0
- package/src/database/database.ts +54 -0
- package/src/database/index.ts +30 -0
- package/src/database/introspector.ts +446 -0
- package/src/database/migration/differ.ts +308 -0
- package/src/database/migration/file_generator.ts +125 -0
- package/src/database/migration/index.ts +18 -0
- package/src/database/migration/runner.ts +133 -0
- package/src/database/migration/sql_generator.ts +378 -0
- package/src/database/migration/tracker.ts +76 -0
- package/src/database/migration/types.ts +189 -0
- package/src/database/query_builder.ts +474 -0
- package/src/encryption/encryption_manager.ts +209 -0
- package/src/encryption/helpers.ts +158 -0
- package/src/encryption/index.ts +3 -0
- package/src/encryption/types.ts +6 -0
- package/src/events/emitter.ts +101 -0
- package/src/events/index.ts +2 -0
- package/src/exceptions/errors.ts +75 -0
- package/src/exceptions/exception_handler.ts +126 -0
- package/src/exceptions/helpers.ts +25 -0
- package/src/exceptions/http_exception.ts +129 -0
- package/src/exceptions/index.ts +23 -0
- package/src/exceptions/strav_error.ts +11 -0
- package/src/generators/api_generator.ts +972 -0
- package/src/generators/config.ts +87 -0
- package/src/generators/doc_generator.ts +974 -0
- package/src/generators/index.ts +11 -0
- package/src/generators/model_generator.ts +586 -0
- package/src/generators/route_generator.ts +188 -0
- package/src/generators/test_generator.ts +1666 -0
- package/src/helpers/crypto.ts +4 -0
- package/src/helpers/env.ts +50 -0
- package/src/helpers/identity.ts +12 -0
- package/src/helpers/index.ts +4 -0
- package/src/helpers/strings.ts +67 -0
- package/src/http/context.ts +215 -0
- package/src/http/cookie.ts +59 -0
- package/src/http/cors.ts +163 -0
- package/src/http/index.ts +16 -0
- package/src/http/middleware.ts +39 -0
- package/src/http/rate_limit.ts +173 -0
- package/src/http/router.ts +556 -0
- package/src/http/server.ts +79 -0
- package/src/i18n/defaults/en/validation.json +20 -0
- package/src/i18n/helpers.ts +72 -0
- package/src/i18n/i18n_manager.ts +155 -0
- package/src/i18n/index.ts +4 -0
- package/src/i18n/middleware.ts +90 -0
- package/src/i18n/translator.ts +96 -0
- package/src/i18n/types.ts +17 -0
- package/src/logger/index.ts +6 -0
- package/src/logger/logger.ts +100 -0
- package/src/logger/request_logger.ts +19 -0
- package/src/logger/sinks/console_sink.ts +24 -0
- package/src/logger/sinks/file_sink.ts +24 -0
- package/src/logger/sinks/sink.ts +36 -0
- package/src/mail/css_inliner.ts +79 -0
- package/src/mail/helpers.ts +212 -0
- package/src/mail/index.ts +19 -0
- package/src/mail/mail_manager.ts +92 -0
- package/src/mail/transports/log_transport.ts +69 -0
- package/src/mail/transports/resend_transport.ts +59 -0
- package/src/mail/transports/sendgrid_transport.ts +77 -0
- package/src/mail/transports/smtp_transport.ts +48 -0
- package/src/mail/types.ts +80 -0
- package/src/notification/base_notification.ts +67 -0
- package/src/notification/channels/database_channel.ts +30 -0
- package/src/notification/channels/discord_channel.ts +43 -0
- package/src/notification/channels/email_channel.ts +37 -0
- package/src/notification/channels/webhook_channel.ts +45 -0
- package/src/notification/helpers.ts +214 -0
- package/src/notification/index.ts +20 -0
- package/src/notification/notification_manager.ts +126 -0
- package/src/notification/types.ts +122 -0
- package/src/orm/base_model.ts +351 -0
- package/src/orm/decorators.ts +127 -0
- package/src/orm/index.ts +4 -0
- package/src/policy/authorize.ts +44 -0
- package/src/policy/index.ts +3 -0
- package/src/policy/policy_result.ts +13 -0
- package/src/queue/index.ts +11 -0
- package/src/queue/queue.ts +338 -0
- package/src/queue/worker.ts +197 -0
- package/src/scheduler/cron.ts +140 -0
- package/src/scheduler/index.ts +7 -0
- package/src/scheduler/runner.ts +116 -0
- package/src/scheduler/schedule.ts +183 -0
- package/src/scheduler/scheduler.ts +47 -0
- package/src/schema/database_representation.ts +122 -0
- package/src/schema/define_association.ts +60 -0
- package/src/schema/define_schema.ts +46 -0
- package/src/schema/field_builder.ts +155 -0
- package/src/schema/field_definition.ts +66 -0
- package/src/schema/index.ts +21 -0
- package/src/schema/naming.ts +19 -0
- package/src/schema/postgres.ts +109 -0
- package/src/schema/registry.ts +157 -0
- package/src/schema/representation_builder.ts +479 -0
- package/src/schema/type_builder.ts +107 -0
- package/src/schema/types.ts +35 -0
- package/src/session/index.ts +4 -0
- package/src/session/middleware.ts +46 -0
- package/src/session/session.ts +308 -0
- package/src/session/session_manager.ts +81 -0
- package/src/storage/index.ts +13 -0
- package/src/storage/local_driver.ts +46 -0
- package/src/storage/s3_driver.ts +51 -0
- package/src/storage/storage.ts +43 -0
- package/src/storage/storage_manager.ts +59 -0
- package/src/storage/types.ts +42 -0
- package/src/storage/upload.ts +91 -0
- package/src/validation/index.ts +18 -0
- package/src/validation/rules.ts +170 -0
- package/src/validation/validate.ts +41 -0
- package/src/view/cache.ts +47 -0
- package/src/view/client/islands.ts +50 -0
- package/src/view/compiler.ts +185 -0
- package/src/view/engine.ts +139 -0
- package/src/view/escape.ts +14 -0
- package/src/view/index.ts +13 -0
- package/src/view/islands/island_builder.ts +161 -0
- package/src/view/islands/vue_plugin.ts +140 -0
- package/src/view/middleware/static.ts +35 -0
- package/src/view/tokenizer.ts +172 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '../bootstrap.ts'
|
|
4
|
+
import SchemaDiffer from '../../database/migration/differ.ts'
|
|
5
|
+
|
|
6
|
+
export function registerMigrationCompare(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command('migration:compare')
|
|
9
|
+
.description('Compare schema with database and report differences')
|
|
10
|
+
.action(async () => {
|
|
11
|
+
let db
|
|
12
|
+
try {
|
|
13
|
+
const { db: database, registry, introspector } = await bootstrap()
|
|
14
|
+
db = database
|
|
15
|
+
|
|
16
|
+
console.log(chalk.cyan('Comparing schema with database...\n'))
|
|
17
|
+
|
|
18
|
+
const desired = registry.buildRepresentation()
|
|
19
|
+
const actual = await introspector.introspect()
|
|
20
|
+
const diff = new SchemaDiffer().diff(desired, actual)
|
|
21
|
+
|
|
22
|
+
const hasChanges =
|
|
23
|
+
diff.enums.length > 0 ||
|
|
24
|
+
diff.tables.length > 0 ||
|
|
25
|
+
diff.constraints.length > 0 ||
|
|
26
|
+
diff.indexes.length > 0
|
|
27
|
+
|
|
28
|
+
if (!hasChanges) {
|
|
29
|
+
console.log(chalk.green('Schema is in sync with the database.'))
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- Enum changes ---
|
|
34
|
+
if (diff.enums.length > 0) {
|
|
35
|
+
console.log(chalk.bold('Enum changes:'))
|
|
36
|
+
for (const e of diff.enums) {
|
|
37
|
+
if (e.kind === 'create') {
|
|
38
|
+
console.log(chalk.green(` + CREATE ${e.name} (${e.values.join(', ')})`))
|
|
39
|
+
} else if (e.kind === 'drop') {
|
|
40
|
+
console.log(chalk.red(` - DROP ${e.name}`))
|
|
41
|
+
} else if (e.kind === 'modify') {
|
|
42
|
+
console.log(chalk.yellow(` ~ MODIFY ${e.name} (add: ${e.addedValues.join(', ')})`))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
console.log()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Table changes ---
|
|
49
|
+
if (diff.tables.length > 0) {
|
|
50
|
+
console.log(chalk.bold('Table changes:'))
|
|
51
|
+
for (const t of diff.tables) {
|
|
52
|
+
if (t.kind === 'create') {
|
|
53
|
+
console.log(
|
|
54
|
+
chalk.green(` + CREATE ${t.table.name}`) +
|
|
55
|
+
chalk.dim(` (${t.table.columns.length} columns)`)
|
|
56
|
+
)
|
|
57
|
+
} else if (t.kind === 'drop') {
|
|
58
|
+
console.log(chalk.red(` - DROP ${t.table.name}`))
|
|
59
|
+
} else if (t.kind === 'modify') {
|
|
60
|
+
console.log(chalk.yellow(` ~ MODIFY ${t.tableName}`))
|
|
61
|
+
for (const c of t.columns) {
|
|
62
|
+
if (c.kind === 'add') {
|
|
63
|
+
console.log(
|
|
64
|
+
chalk.green(
|
|
65
|
+
` + ADD COLUMN ${c.column.name} (${typeof c.column.pgType === 'string' ? c.column.pgType : 'custom'})`
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
} else if (c.kind === 'drop') {
|
|
69
|
+
console.log(chalk.red(` - DROP COLUMN ${c.column.name}`))
|
|
70
|
+
} else if (c.kind === 'alter') {
|
|
71
|
+
const changes: string[] = []
|
|
72
|
+
if (c.typeChange) changes.push(`type: ${c.typeChange.from} -> ${c.typeChange.to}`)
|
|
73
|
+
if (c.nullableChange)
|
|
74
|
+
changes.push(c.nullableChange.to ? 'set NOT NULL' : 'drop NOT NULL')
|
|
75
|
+
if (c.defaultChange) changes.push('default changed')
|
|
76
|
+
console.log(
|
|
77
|
+
chalk.yellow(` ~ ALTER COLUMN ${c.columnName} (${changes.join(', ')})`)
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
console.log()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Constraint changes ---
|
|
87
|
+
if (diff.constraints.length > 0) {
|
|
88
|
+
console.log(chalk.bold('Constraint changes:'))
|
|
89
|
+
for (const c of diff.constraints) {
|
|
90
|
+
if (c.kind === 'add_fk') {
|
|
91
|
+
console.log(
|
|
92
|
+
chalk.green(
|
|
93
|
+
` + ADD FK ${c.tableName}(${c.constraint.columns.join(',')}) -> ${c.constraint.referencedTable}`
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
} else if (c.kind === 'drop_fk') {
|
|
97
|
+
console.log(
|
|
98
|
+
chalk.red(
|
|
99
|
+
` - DROP FK ${c.tableName}(${c.constraint.columns.join(',')}) -> ${c.constraint.referencedTable}`
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
} else if (c.kind === 'add_unique') {
|
|
103
|
+
console.log(
|
|
104
|
+
chalk.green(` + ADD UQ ${c.tableName}(${c.constraint.columns.join(',')})`)
|
|
105
|
+
)
|
|
106
|
+
} else if (c.kind === 'drop_unique') {
|
|
107
|
+
console.log(
|
|
108
|
+
chalk.red(` - DROP UQ ${c.tableName}(${c.constraint.columns.join(',')})`)
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
console.log()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- Index changes ---
|
|
116
|
+
if (diff.indexes.length > 0) {
|
|
117
|
+
console.log(chalk.bold('Index changes:'))
|
|
118
|
+
for (const i of diff.indexes) {
|
|
119
|
+
if (i.kind === 'add') {
|
|
120
|
+
const unique = i.index.unique ? 'UNIQUE ' : ''
|
|
121
|
+
console.log(
|
|
122
|
+
chalk.green(
|
|
123
|
+
` + CREATE ${unique}INDEX ${i.tableName}(${i.index.columns.join(',')})`
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
} else if (i.kind === 'drop') {
|
|
127
|
+
console.log(
|
|
128
|
+
chalk.red(` - DROP INDEX ${i.tableName}(${i.index.columns.join(',')})`)
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
console.log()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Summary ---
|
|
136
|
+
const creates = diff.tables.filter(t => t.kind === 'create').length
|
|
137
|
+
const drops = diff.tables.filter(t => t.kind === 'drop').length
|
|
138
|
+
const modifies = diff.tables.filter(t => t.kind === 'modify').length
|
|
139
|
+
console.log(
|
|
140
|
+
chalk.bold('Summary: ') +
|
|
141
|
+
`${creates} table(s) to create, ${drops} to drop, ${modifies} to modify, ` +
|
|
142
|
+
`${diff.enums.length} enum change(s), ${diff.constraints.length} constraint change(s), ${diff.indexes.length} index change(s)`
|
|
143
|
+
)
|
|
144
|
+
console.log(chalk.dim('\nRun "bun strav migration:generate" to create migration files.'))
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
147
|
+
process.exit(1)
|
|
148
|
+
} finally {
|
|
149
|
+
if (db) await shutdown(db)
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
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 '../bootstrap.ts'
|
|
6
|
+
import SchemaDiffer from '../../database/migration/differ.ts'
|
|
7
|
+
import SqlGenerator from '../../database/migration/sql_generator.ts'
|
|
8
|
+
import MigrationFileGenerator from '../../database/migration/file_generator.ts'
|
|
9
|
+
import MigrationTracker from '../../database/migration/tracker.ts'
|
|
10
|
+
import MigrationRunner from '../../database/migration/runner.ts'
|
|
11
|
+
|
|
12
|
+
const MIGRATIONS_PATH = 'database/migrations'
|
|
13
|
+
|
|
14
|
+
export function registerMigrationFresh(program: Command): void {
|
|
15
|
+
program
|
|
16
|
+
.command('migration:fresh')
|
|
17
|
+
.description('Reset database and migrations, regenerate and run from scratch')
|
|
18
|
+
.action(async () => {
|
|
19
|
+
// --- Safety check 1: APP_ENV must be "local" ---
|
|
20
|
+
const appEnv = process.env.APP_ENV
|
|
21
|
+
if (appEnv !== 'local') {
|
|
22
|
+
console.error(
|
|
23
|
+
chalk.red('REJECTED: ') + 'migration:fresh can only run when APP_ENV is set to "local".'
|
|
24
|
+
)
|
|
25
|
+
if (!appEnv) {
|
|
26
|
+
console.error(chalk.dim(' APP_ENV is not defined in .env'))
|
|
27
|
+
} else {
|
|
28
|
+
console.error(chalk.dim(` Current APP_ENV: "${appEnv}"`))
|
|
29
|
+
}
|
|
30
|
+
process.exit(1)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- Safety check 2: 6-digit challenge ---
|
|
34
|
+
const challenge = String(Math.floor(100000 + Math.random() * 900000))
|
|
35
|
+
console.log(
|
|
36
|
+
chalk.red('WARNING: ') +
|
|
37
|
+
'This will ' +
|
|
38
|
+
chalk.red('destroy ALL data') +
|
|
39
|
+
' in the database and recreate everything from schemas.'
|
|
40
|
+
)
|
|
41
|
+
console.log(`\n Type ${chalk.yellow(challenge)} to confirm:\n`)
|
|
42
|
+
|
|
43
|
+
const answer = await prompt(' > ')
|
|
44
|
+
if (answer.trim() !== challenge) {
|
|
45
|
+
console.error(chalk.red('\nChallenge code does not match. Operation cancelled.'))
|
|
46
|
+
process.exit(1)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let db
|
|
50
|
+
try {
|
|
51
|
+
const { db: database, registry, introspector } = await bootstrap()
|
|
52
|
+
db = database
|
|
53
|
+
|
|
54
|
+
// --- Step 1: Drop all tables and enum types ---
|
|
55
|
+
console.log(chalk.cyan('\nDropping all tables and types...'))
|
|
56
|
+
|
|
57
|
+
// Drop all tables in public schema
|
|
58
|
+
const tables = await db.sql`
|
|
59
|
+
SELECT table_name FROM information_schema.tables
|
|
60
|
+
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
61
|
+
`
|
|
62
|
+
for (const row of tables) {
|
|
63
|
+
await db.sql.unsafe(`DROP TABLE IF EXISTS "${row.table_name}" CASCADE`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Drop all enum types in public schema
|
|
67
|
+
const types = await db.sql`
|
|
68
|
+
SELECT t.typname
|
|
69
|
+
FROM pg_type t
|
|
70
|
+
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
71
|
+
WHERE n.nspname = 'public'
|
|
72
|
+
AND t.typtype = 'e'
|
|
73
|
+
`
|
|
74
|
+
for (const row of types) {
|
|
75
|
+
await db.sql.unsafe(`DROP TYPE IF EXISTS "${row.typname}" CASCADE`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- Step 2: Delete existing migration files ---
|
|
79
|
+
console.log(chalk.cyan('Clearing migration directory...'))
|
|
80
|
+
rmSync(MIGRATIONS_PATH, { recursive: true, force: true })
|
|
81
|
+
|
|
82
|
+
// --- Step 3: Generate new migration ---
|
|
83
|
+
console.log(chalk.cyan('Generating fresh migration...'))
|
|
84
|
+
|
|
85
|
+
const desired = registry.buildRepresentation()
|
|
86
|
+
const actual = await introspector.introspect() // should be empty now
|
|
87
|
+
const diff = new SchemaDiffer().diff(desired, actual)
|
|
88
|
+
|
|
89
|
+
const sql = new SqlGenerator().generate(diff)
|
|
90
|
+
const version = Date.now().toString()
|
|
91
|
+
const tableOrder = desired.tables.map(t => t.name)
|
|
92
|
+
|
|
93
|
+
const fileGen = new MigrationFileGenerator(MIGRATIONS_PATH)
|
|
94
|
+
await fileGen.generate(version, 'fresh', sql, diff, tableOrder)
|
|
95
|
+
|
|
96
|
+
// --- Step 4: Run the migration ---
|
|
97
|
+
console.log(chalk.cyan('Running migration...'))
|
|
98
|
+
|
|
99
|
+
const tracker = new MigrationTracker(db)
|
|
100
|
+
const runner = new MigrationRunner(db, tracker, MIGRATIONS_PATH)
|
|
101
|
+
const result = await runner.run()
|
|
102
|
+
|
|
103
|
+
console.log(
|
|
104
|
+
chalk.green(`\nFresh migration complete. Applied ${result.applied.length} migration(s).`)
|
|
105
|
+
)
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
108
|
+
process.exit(1)
|
|
109
|
+
} finally {
|
|
110
|
+
if (db) await shutdown(db)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function prompt(question: string): Promise<string> {
|
|
116
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
117
|
+
return new Promise(resolve => {
|
|
118
|
+
rl.question(question, answer => {
|
|
119
|
+
rl.close()
|
|
120
|
+
resolve(answer)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '../bootstrap.ts'
|
|
4
|
+
import SchemaDiffer from '../../database/migration/differ.ts'
|
|
5
|
+
import SqlGenerator from '../../database/migration/sql_generator.ts'
|
|
6
|
+
import MigrationFileGenerator from '../../database/migration/file_generator.ts'
|
|
7
|
+
|
|
8
|
+
const MIGRATIONS_PATH = 'database/migrations'
|
|
9
|
+
|
|
10
|
+
export function registerMigrationGenerate(program: Command): void {
|
|
11
|
+
program
|
|
12
|
+
.command('migration:generate')
|
|
13
|
+
.description('Generate migration files from schema changes')
|
|
14
|
+
.option('-m, --message <message>', 'Migration message', 'migration')
|
|
15
|
+
.action(async (opts: { message: string }) => {
|
|
16
|
+
let db
|
|
17
|
+
try {
|
|
18
|
+
const { db: database, registry, introspector } = await bootstrap()
|
|
19
|
+
db = database
|
|
20
|
+
|
|
21
|
+
console.log(chalk.cyan('Comparing schema with database...'))
|
|
22
|
+
|
|
23
|
+
const desired = registry.buildRepresentation()
|
|
24
|
+
const actual = await introspector.introspect()
|
|
25
|
+
const diff = new SchemaDiffer().diff(desired, actual)
|
|
26
|
+
|
|
27
|
+
const hasChanges =
|
|
28
|
+
diff.enums.length > 0 ||
|
|
29
|
+
diff.tables.length > 0 ||
|
|
30
|
+
diff.constraints.length > 0 ||
|
|
31
|
+
diff.indexes.length > 0
|
|
32
|
+
|
|
33
|
+
if (!hasChanges) {
|
|
34
|
+
console.log(chalk.green('No changes detected. Schema is in sync with the database.'))
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const sql = new SqlGenerator().generate(diff)
|
|
39
|
+
const version = Date.now().toString()
|
|
40
|
+
|
|
41
|
+
// Use the table order from the desired representation (already dependency-ordered)
|
|
42
|
+
const tableOrder = desired.tables.map(t => t.name)
|
|
43
|
+
|
|
44
|
+
const fileGen = new MigrationFileGenerator(MIGRATIONS_PATH)
|
|
45
|
+
const dir = await fileGen.generate(version, opts.message, sql, diff, tableOrder)
|
|
46
|
+
|
|
47
|
+
// Print summary
|
|
48
|
+
console.log(chalk.green(`\nMigration generated: ${version}`))
|
|
49
|
+
console.log(chalk.dim(` Directory: ${dir}`))
|
|
50
|
+
console.log(chalk.dim(` Message: ${opts.message}\n`))
|
|
51
|
+
|
|
52
|
+
const counts: string[] = []
|
|
53
|
+
const creates = diff.tables.filter(t => t.kind === 'create').length
|
|
54
|
+
const drops = diff.tables.filter(t => t.kind === 'drop').length
|
|
55
|
+
const modifies = diff.tables.filter(t => t.kind === 'modify').length
|
|
56
|
+
if (creates > 0) counts.push(chalk.green(`${creates} table(s) to create`))
|
|
57
|
+
if (drops > 0) counts.push(chalk.red(`${drops} table(s) to drop`))
|
|
58
|
+
if (modifies > 0) counts.push(chalk.yellow(`${modifies} table(s) to modify`))
|
|
59
|
+
|
|
60
|
+
const enumCreates = diff.enums.filter(e => e.kind === 'create').length
|
|
61
|
+
const enumDrops = diff.enums.filter(e => e.kind === 'drop').length
|
|
62
|
+
const enumModifies = diff.enums.filter(e => e.kind === 'modify').length
|
|
63
|
+
if (enumCreates > 0) counts.push(chalk.green(`${enumCreates} enum(s) to create`))
|
|
64
|
+
if (enumDrops > 0) counts.push(chalk.red(`${enumDrops} enum(s) to drop`))
|
|
65
|
+
if (enumModifies > 0) counts.push(chalk.yellow(`${enumModifies} enum(s) to modify`))
|
|
66
|
+
|
|
67
|
+
if (diff.constraints.length > 0)
|
|
68
|
+
counts.push(`${diff.constraints.length} constraint change(s)`)
|
|
69
|
+
if (diff.indexes.length > 0) counts.push(`${diff.indexes.length} index change(s)`)
|
|
70
|
+
|
|
71
|
+
console.log(' Summary: ' + counts.join(', '))
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
74
|
+
process.exit(1)
|
|
75
|
+
} finally {
|
|
76
|
+
if (db) await shutdown(db)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '../bootstrap.ts'
|
|
4
|
+
import MigrationTracker from '../../database/migration/tracker.ts'
|
|
5
|
+
import MigrationRunner from '../../database/migration/runner.ts'
|
|
6
|
+
|
|
7
|
+
const MIGRATIONS_PATH = 'database/migrations'
|
|
8
|
+
|
|
9
|
+
export function registerMigrationRollback(program: Command): void {
|
|
10
|
+
program
|
|
11
|
+
.command('migration:rollback')
|
|
12
|
+
.description('Rollback migrations by batch')
|
|
13
|
+
.option('--batch <number>', 'Specific batch number to rollback')
|
|
14
|
+
.action(async (opts: { batch?: string }) => {
|
|
15
|
+
let db
|
|
16
|
+
try {
|
|
17
|
+
const { db: database } = await bootstrap()
|
|
18
|
+
db = database
|
|
19
|
+
|
|
20
|
+
const tracker = new MigrationTracker(db)
|
|
21
|
+
const runner = new MigrationRunner(db, tracker, MIGRATIONS_PATH)
|
|
22
|
+
|
|
23
|
+
const batchNum = opts.batch ? parseInt(opts.batch, 10) : undefined
|
|
24
|
+
|
|
25
|
+
console.log(
|
|
26
|
+
batchNum
|
|
27
|
+
? chalk.cyan(`Rolling back batch ${batchNum}...`)
|
|
28
|
+
: chalk.cyan('Rolling back last batch...')
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const result = await runner.rollback(batchNum)
|
|
32
|
+
|
|
33
|
+
if (result.rolledBack.length === 0) {
|
|
34
|
+
console.log(chalk.yellow('Nothing to rollback.'))
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(
|
|
39
|
+
chalk.green(
|
|
40
|
+
`\nRolled back ${result.rolledBack.length} migration(s) from batch ${result.batch}:`
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
for (const version of result.rolledBack) {
|
|
44
|
+
console.log(chalk.dim(` - ${version}`))
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
48
|
+
process.exit(1)
|
|
49
|
+
} finally {
|
|
50
|
+
if (db) await shutdown(db)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '../bootstrap.ts'
|
|
4
|
+
import MigrationTracker from '../../database/migration/tracker.ts'
|
|
5
|
+
import MigrationRunner from '../../database/migration/runner.ts'
|
|
6
|
+
|
|
7
|
+
const MIGRATIONS_PATH = 'database/migrations'
|
|
8
|
+
|
|
9
|
+
export function registerMigrationRun(program: Command): void {
|
|
10
|
+
program
|
|
11
|
+
.command('migration:run')
|
|
12
|
+
.description('Run pending migrations')
|
|
13
|
+
.action(async () => {
|
|
14
|
+
let db
|
|
15
|
+
try {
|
|
16
|
+
const { db: database } = await bootstrap()
|
|
17
|
+
db = database
|
|
18
|
+
|
|
19
|
+
const tracker = new MigrationTracker(db)
|
|
20
|
+
const runner = new MigrationRunner(db, tracker, MIGRATIONS_PATH)
|
|
21
|
+
|
|
22
|
+
console.log(chalk.cyan('Running pending migrations...'))
|
|
23
|
+
|
|
24
|
+
const result = await runner.run()
|
|
25
|
+
|
|
26
|
+
if (result.applied.length === 0) {
|
|
27
|
+
console.log(chalk.green('Nothing to migrate. All migrations are up to date.'))
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(
|
|
32
|
+
chalk.green(`\nApplied ${result.applied.length} migration(s) in batch ${result.batch}:`)
|
|
33
|
+
)
|
|
34
|
+
for (const version of result.applied) {
|
|
35
|
+
console.log(chalk.dim(` - ${version}`))
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
39
|
+
process.exit(1)
|
|
40
|
+
} finally {
|
|
41
|
+
if (db) await shutdown(db)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '../bootstrap.ts'
|
|
4
|
+
import Queue from '../../queue/queue.ts'
|
|
5
|
+
|
|
6
|
+
export function registerQueueFlush(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command('queue:flush')
|
|
9
|
+
.description('Delete all jobs from a queue')
|
|
10
|
+
.option('--queue <name>', 'Queue to flush', 'default')
|
|
11
|
+
.option('--failed', 'Also flush failed jobs')
|
|
12
|
+
.action(async options => {
|
|
13
|
+
let db
|
|
14
|
+
try {
|
|
15
|
+
const { db: database, config } = await bootstrap()
|
|
16
|
+
db = database
|
|
17
|
+
|
|
18
|
+
new Queue(db, config)
|
|
19
|
+
await Queue.ensureTables()
|
|
20
|
+
|
|
21
|
+
const cleared = await Queue.clear(options.queue)
|
|
22
|
+
console.log(chalk.green(`Cleared ${cleared} pending job(s) from "${options.queue}".`))
|
|
23
|
+
|
|
24
|
+
if (options.failed) {
|
|
25
|
+
const failedCleared = await Queue.clearFailed(options.queue)
|
|
26
|
+
console.log(chalk.green(`Cleared ${failedCleared} failed job(s).`))
|
|
27
|
+
}
|
|
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,34 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '../bootstrap.ts'
|
|
4
|
+
import Queue from '../../queue/queue.ts'
|
|
5
|
+
|
|
6
|
+
export function registerQueueRetry(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command('queue:retry')
|
|
9
|
+
.description('Retry failed jobs by moving them back to the queue')
|
|
10
|
+
.option('--queue <name>', 'Only retry jobs from this queue')
|
|
11
|
+
.action(async options => {
|
|
12
|
+
let db
|
|
13
|
+
try {
|
|
14
|
+
const { db: database, config } = await bootstrap()
|
|
15
|
+
db = database
|
|
16
|
+
|
|
17
|
+
new Queue(db, config)
|
|
18
|
+
await Queue.ensureTables()
|
|
19
|
+
|
|
20
|
+
const count = await Queue.retryFailed(options.queue)
|
|
21
|
+
|
|
22
|
+
if (count === 0) {
|
|
23
|
+
console.log(chalk.green('No failed jobs to retry.'))
|
|
24
|
+
} else {
|
|
25
|
+
console.log(chalk.green(`Moved ${count} failed job(s) back to the queue.`))
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
29
|
+
process.exit(1)
|
|
30
|
+
} finally {
|
|
31
|
+
if (db) await shutdown(db)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '../bootstrap.ts'
|
|
4
|
+
import Queue from '../../queue/queue.ts'
|
|
5
|
+
import Worker from '../../queue/worker.ts'
|
|
6
|
+
|
|
7
|
+
export function registerQueueWork(program: Command): void {
|
|
8
|
+
program
|
|
9
|
+
.command('queue:work')
|
|
10
|
+
.description('Start processing queued jobs')
|
|
11
|
+
.option('--queue <name>', 'Queue to process', 'default')
|
|
12
|
+
.option('--sleep <ms>', 'Poll interval in milliseconds', '1000')
|
|
13
|
+
.action(async options => {
|
|
14
|
+
let db
|
|
15
|
+
try {
|
|
16
|
+
const { db: database, config } = await bootstrap()
|
|
17
|
+
db = database
|
|
18
|
+
|
|
19
|
+
new Queue(db, config)
|
|
20
|
+
await Queue.ensureTables()
|
|
21
|
+
|
|
22
|
+
const queue = options.queue
|
|
23
|
+
const sleep = parseInt(options.sleep)
|
|
24
|
+
|
|
25
|
+
console.log(chalk.cyan(`Worker starting on queue "${queue}"...`))
|
|
26
|
+
console.log(chalk.dim(` poll interval: ${sleep}ms`))
|
|
27
|
+
console.log(chalk.dim(' Press Ctrl+C to stop.\n'))
|
|
28
|
+
|
|
29
|
+
const worker = new Worker({ queue, sleep })
|
|
30
|
+
await worker.start()
|
|
31
|
+
|
|
32
|
+
console.log(chalk.dim('\nWorker stopped.'))
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
35
|
+
process.exit(1)
|
|
36
|
+
} finally {
|
|
37
|
+
if (db) await shutdown(db)
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { bootstrap, shutdown } from '../bootstrap.ts'
|
|
5
|
+
import Scheduler from '../../scheduler/scheduler.ts'
|
|
6
|
+
import SchedulerRunner from '../../scheduler/runner.ts'
|
|
7
|
+
|
|
8
|
+
export function registerSchedulerWork(program: Command): void {
|
|
9
|
+
program
|
|
10
|
+
.command('scheduler:work')
|
|
11
|
+
.description('Start the task scheduler')
|
|
12
|
+
.action(async () => {
|
|
13
|
+
let db
|
|
14
|
+
try {
|
|
15
|
+
const { db: database } = await bootstrap()
|
|
16
|
+
db = database
|
|
17
|
+
|
|
18
|
+
// Load user's scheduled tasks
|
|
19
|
+
const schedulesPath = path.resolve('app/schedules.ts')
|
|
20
|
+
await import(schedulesPath)
|
|
21
|
+
|
|
22
|
+
const taskCount = Scheduler.tasks.length
|
|
23
|
+
if (taskCount === 0) {
|
|
24
|
+
console.log(chalk.yellow('No tasks registered. Add tasks in app/schedules.ts'))
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log(chalk.cyan(`Scheduler starting with ${taskCount} task(s)...`))
|
|
29
|
+
for (const task of Scheduler.tasks) {
|
|
30
|
+
console.log(chalk.dim(` ${task.name}`))
|
|
31
|
+
}
|
|
32
|
+
console.log(chalk.dim(' Press Ctrl+C to stop.\n'))
|
|
33
|
+
|
|
34
|
+
const runner = new SchedulerRunner()
|
|
35
|
+
await runner.start()
|
|
36
|
+
|
|
37
|
+
console.log(chalk.dim('\nScheduler stopped.'))
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
40
|
+
process.exit(1)
|
|
41
|
+
} finally {
|
|
42
|
+
if (db) await shutdown(db)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
}
|
package/src/cli/strav.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import 'reflect-metadata'
|
|
2
|
+
import { Command } from 'commander'
|
|
3
|
+
import { registerMigrationGenerate } from './commands/migration_generate.ts'
|
|
4
|
+
import { registerMigrationRun } from './commands/migration_run.ts'
|
|
5
|
+
import { registerMigrationRollback } from './commands/migration_rollback.ts'
|
|
6
|
+
import { registerMigrationFresh } from './commands/migration_fresh.ts'
|
|
7
|
+
import { registerMigrationCompare } from './commands/migration_compare.ts'
|
|
8
|
+
import { registerGenerateModels } from './commands/generate_models.ts'
|
|
9
|
+
import { registerQueueWork } from './commands/queue_work.ts'
|
|
10
|
+
import { registerQueueRetry } from './commands/queue_retry.ts'
|
|
11
|
+
import { registerQueueFlush } from './commands/queue_flush.ts'
|
|
12
|
+
import { registerGenerateApi } from './commands/generate_api.ts'
|
|
13
|
+
import { registerSchedulerWork } from './commands/scheduler_work.ts'
|
|
14
|
+
import { registerGenerateKey } from './commands/generate_key.ts'
|
|
15
|
+
|
|
16
|
+
const program = new Command()
|
|
17
|
+
|
|
18
|
+
program.name('strav').description('Strav CLI').version('0.1.0')
|
|
19
|
+
|
|
20
|
+
registerMigrationGenerate(program)
|
|
21
|
+
registerMigrationRun(program)
|
|
22
|
+
registerMigrationRollback(program)
|
|
23
|
+
registerMigrationFresh(program)
|
|
24
|
+
registerMigrationCompare(program)
|
|
25
|
+
registerGenerateModels(program)
|
|
26
|
+
registerQueueWork(program)
|
|
27
|
+
registerQueueRetry(program)
|
|
28
|
+
registerQueueFlush(program)
|
|
29
|
+
registerGenerateApi(program)
|
|
30
|
+
registerSchedulerWork(program)
|
|
31
|
+
registerGenerateKey(program)
|
|
32
|
+
|
|
33
|
+
program.parse()
|