@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.
Files changed (165) hide show
  1. package/README.md +45 -0
  2. package/package.json +83 -0
  3. package/src/auth/access_token.ts +122 -0
  4. package/src/auth/auth.ts +86 -0
  5. package/src/auth/index.ts +7 -0
  6. package/src/auth/middleware/authenticate.ts +64 -0
  7. package/src/auth/middleware/csrf.ts +62 -0
  8. package/src/auth/middleware/guest.ts +46 -0
  9. package/src/broadcast/broadcast_manager.ts +411 -0
  10. package/src/broadcast/client.ts +302 -0
  11. package/src/broadcast/index.ts +58 -0
  12. package/src/cache/cache_manager.ts +56 -0
  13. package/src/cache/cache_store.ts +31 -0
  14. package/src/cache/helpers.ts +74 -0
  15. package/src/cache/http_cache.ts +109 -0
  16. package/src/cache/index.ts +6 -0
  17. package/src/cache/memory_store.ts +63 -0
  18. package/src/cli/bootstrap.ts +37 -0
  19. package/src/cli/commands/generate_api.ts +74 -0
  20. package/src/cli/commands/generate_key.ts +46 -0
  21. package/src/cli/commands/generate_models.ts +48 -0
  22. package/src/cli/commands/migration_compare.ts +152 -0
  23. package/src/cli/commands/migration_fresh.ts +123 -0
  24. package/src/cli/commands/migration_generate.ts +79 -0
  25. package/src/cli/commands/migration_rollback.ts +53 -0
  26. package/src/cli/commands/migration_run.ts +44 -0
  27. package/src/cli/commands/queue_flush.ts +35 -0
  28. package/src/cli/commands/queue_retry.ts +34 -0
  29. package/src/cli/commands/queue_work.ts +40 -0
  30. package/src/cli/commands/scheduler_work.ts +45 -0
  31. package/src/cli/strav.ts +33 -0
  32. package/src/config/configuration.ts +105 -0
  33. package/src/config/loaders/base_loader.ts +69 -0
  34. package/src/config/loaders/env_loader.ts +112 -0
  35. package/src/config/loaders/typescript_loader.ts +56 -0
  36. package/src/config/types.ts +8 -0
  37. package/src/core/application.ts +4 -0
  38. package/src/core/container.ts +117 -0
  39. package/src/core/index.ts +3 -0
  40. package/src/core/inject.ts +39 -0
  41. package/src/database/database.ts +54 -0
  42. package/src/database/index.ts +30 -0
  43. package/src/database/introspector.ts +446 -0
  44. package/src/database/migration/differ.ts +308 -0
  45. package/src/database/migration/file_generator.ts +125 -0
  46. package/src/database/migration/index.ts +18 -0
  47. package/src/database/migration/runner.ts +133 -0
  48. package/src/database/migration/sql_generator.ts +378 -0
  49. package/src/database/migration/tracker.ts +76 -0
  50. package/src/database/migration/types.ts +189 -0
  51. package/src/database/query_builder.ts +474 -0
  52. package/src/encryption/encryption_manager.ts +209 -0
  53. package/src/encryption/helpers.ts +158 -0
  54. package/src/encryption/index.ts +3 -0
  55. package/src/encryption/types.ts +6 -0
  56. package/src/events/emitter.ts +101 -0
  57. package/src/events/index.ts +2 -0
  58. package/src/exceptions/errors.ts +75 -0
  59. package/src/exceptions/exception_handler.ts +126 -0
  60. package/src/exceptions/helpers.ts +25 -0
  61. package/src/exceptions/http_exception.ts +129 -0
  62. package/src/exceptions/index.ts +23 -0
  63. package/src/exceptions/strav_error.ts +11 -0
  64. package/src/generators/api_generator.ts +972 -0
  65. package/src/generators/config.ts +87 -0
  66. package/src/generators/doc_generator.ts +974 -0
  67. package/src/generators/index.ts +11 -0
  68. package/src/generators/model_generator.ts +586 -0
  69. package/src/generators/route_generator.ts +188 -0
  70. package/src/generators/test_generator.ts +1666 -0
  71. package/src/helpers/crypto.ts +4 -0
  72. package/src/helpers/env.ts +50 -0
  73. package/src/helpers/identity.ts +12 -0
  74. package/src/helpers/index.ts +4 -0
  75. package/src/helpers/strings.ts +67 -0
  76. package/src/http/context.ts +215 -0
  77. package/src/http/cookie.ts +59 -0
  78. package/src/http/cors.ts +163 -0
  79. package/src/http/index.ts +16 -0
  80. package/src/http/middleware.ts +39 -0
  81. package/src/http/rate_limit.ts +173 -0
  82. package/src/http/router.ts +556 -0
  83. package/src/http/server.ts +79 -0
  84. package/src/i18n/defaults/en/validation.json +20 -0
  85. package/src/i18n/helpers.ts +72 -0
  86. package/src/i18n/i18n_manager.ts +155 -0
  87. package/src/i18n/index.ts +4 -0
  88. package/src/i18n/middleware.ts +90 -0
  89. package/src/i18n/translator.ts +96 -0
  90. package/src/i18n/types.ts +17 -0
  91. package/src/logger/index.ts +6 -0
  92. package/src/logger/logger.ts +100 -0
  93. package/src/logger/request_logger.ts +19 -0
  94. package/src/logger/sinks/console_sink.ts +24 -0
  95. package/src/logger/sinks/file_sink.ts +24 -0
  96. package/src/logger/sinks/sink.ts +36 -0
  97. package/src/mail/css_inliner.ts +79 -0
  98. package/src/mail/helpers.ts +212 -0
  99. package/src/mail/index.ts +19 -0
  100. package/src/mail/mail_manager.ts +92 -0
  101. package/src/mail/transports/log_transport.ts +69 -0
  102. package/src/mail/transports/resend_transport.ts +59 -0
  103. package/src/mail/transports/sendgrid_transport.ts +77 -0
  104. package/src/mail/transports/smtp_transport.ts +48 -0
  105. package/src/mail/types.ts +80 -0
  106. package/src/notification/base_notification.ts +67 -0
  107. package/src/notification/channels/database_channel.ts +30 -0
  108. package/src/notification/channels/discord_channel.ts +43 -0
  109. package/src/notification/channels/email_channel.ts +37 -0
  110. package/src/notification/channels/webhook_channel.ts +45 -0
  111. package/src/notification/helpers.ts +214 -0
  112. package/src/notification/index.ts +20 -0
  113. package/src/notification/notification_manager.ts +126 -0
  114. package/src/notification/types.ts +122 -0
  115. package/src/orm/base_model.ts +351 -0
  116. package/src/orm/decorators.ts +127 -0
  117. package/src/orm/index.ts +4 -0
  118. package/src/policy/authorize.ts +44 -0
  119. package/src/policy/index.ts +3 -0
  120. package/src/policy/policy_result.ts +13 -0
  121. package/src/queue/index.ts +11 -0
  122. package/src/queue/queue.ts +338 -0
  123. package/src/queue/worker.ts +197 -0
  124. package/src/scheduler/cron.ts +140 -0
  125. package/src/scheduler/index.ts +7 -0
  126. package/src/scheduler/runner.ts +116 -0
  127. package/src/scheduler/schedule.ts +183 -0
  128. package/src/scheduler/scheduler.ts +47 -0
  129. package/src/schema/database_representation.ts +122 -0
  130. package/src/schema/define_association.ts +60 -0
  131. package/src/schema/define_schema.ts +46 -0
  132. package/src/schema/field_builder.ts +155 -0
  133. package/src/schema/field_definition.ts +66 -0
  134. package/src/schema/index.ts +21 -0
  135. package/src/schema/naming.ts +19 -0
  136. package/src/schema/postgres.ts +109 -0
  137. package/src/schema/registry.ts +157 -0
  138. package/src/schema/representation_builder.ts +479 -0
  139. package/src/schema/type_builder.ts +107 -0
  140. package/src/schema/types.ts +35 -0
  141. package/src/session/index.ts +4 -0
  142. package/src/session/middleware.ts +46 -0
  143. package/src/session/session.ts +308 -0
  144. package/src/session/session_manager.ts +81 -0
  145. package/src/storage/index.ts +13 -0
  146. package/src/storage/local_driver.ts +46 -0
  147. package/src/storage/s3_driver.ts +51 -0
  148. package/src/storage/storage.ts +43 -0
  149. package/src/storage/storage_manager.ts +59 -0
  150. package/src/storage/types.ts +42 -0
  151. package/src/storage/upload.ts +91 -0
  152. package/src/validation/index.ts +18 -0
  153. package/src/validation/rules.ts +170 -0
  154. package/src/validation/validate.ts +41 -0
  155. package/src/view/cache.ts +47 -0
  156. package/src/view/client/islands.ts +50 -0
  157. package/src/view/compiler.ts +185 -0
  158. package/src/view/engine.ts +139 -0
  159. package/src/view/escape.ts +14 -0
  160. package/src/view/index.ts +13 -0
  161. package/src/view/islands/island_builder.ts +161 -0
  162. package/src/view/islands/vue_plugin.ts +140 -0
  163. package/src/view/middleware/static.ts +35 -0
  164. package/src/view/tokenizer.ts +172 -0
  165. 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
+ }
@@ -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()