@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,378 @@
1
+ import type { ColumnDefinition, DefaultValue } from '../../schema/database_representation.ts'
2
+ import type { PostgreSQLType } from '../../schema/postgres.ts'
3
+ import type {
4
+ SchemaDiff,
5
+ GeneratedSql,
6
+ EnumDiff,
7
+ TableDiff,
8
+ ColumnDiff,
9
+ ConstraintDiff,
10
+ IndexDiff,
11
+ } from './types.ts'
12
+
13
+ /**
14
+ * Generates SQL migration statements from a {@link SchemaDiff}.
15
+ *
16
+ * Output is split into four categories matching the migration file structure:
17
+ * enums, tables, constraints, indexes — each with up and down SQL.
18
+ */
19
+ export default class SqlGenerator {
20
+ generate(diff: SchemaDiff): GeneratedSql {
21
+ return {
22
+ enumsUp: this.generateEnumsUp(diff.enums),
23
+ enumsDown: this.generateEnumsDown(diff.enums),
24
+ tables: this.generateTables(diff.tables),
25
+ constraintsUp: this.generateConstraintsUp(diff.constraints),
26
+ constraintsDown: this.generateConstraintsDown(diff.constraints),
27
+ indexesUp: this.generateIndexesUp(diff.indexes),
28
+ indexesDown: this.generateIndexesDown(diff.indexes),
29
+ }
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Enums
34
+ // ---------------------------------------------------------------------------
35
+
36
+ private generateEnumsUp(diffs: EnumDiff[]): string {
37
+ const lines: string[] = []
38
+ for (const d of diffs) {
39
+ if (d.kind === 'create') {
40
+ const vals = d.values.map(v => `'${v}'`).join(', ')
41
+ lines.push(`-- Create enum: ${d.name}`)
42
+ lines.push(`CREATE TYPE "${d.name}" AS ENUM (${vals});\n`)
43
+ } else if (d.kind === 'modify') {
44
+ lines.push(`-- Add values to enum: ${d.name}`)
45
+ for (const v of d.addedValues) {
46
+ lines.push(`ALTER TYPE "${d.name}" ADD VALUE '${v}';`)
47
+ }
48
+ lines.push('')
49
+ } else if (d.kind === 'drop') {
50
+ lines.push(`-- Drop enum: ${d.name}`)
51
+ lines.push(`DROP TYPE IF EXISTS "${d.name}";\n`)
52
+ }
53
+ }
54
+ return lines.join('\n').trim()
55
+ }
56
+
57
+ private generateEnumsDown(diffs: EnumDiff[]): string {
58
+ const lines: string[] = []
59
+ for (const d of diffs) {
60
+ if (d.kind === 'create') {
61
+ lines.push(`-- Reverse: drop enum ${d.name}`)
62
+ lines.push(`DROP TYPE IF EXISTS "${d.name}";\n`)
63
+ } else if (d.kind === 'modify') {
64
+ lines.push(`-- Reverse: cannot remove enum values for ${d.name}`)
65
+ lines.push(
66
+ `-- Manual intervention required to remove values: ${d.addedValues.join(', ')}\n`
67
+ )
68
+ } else if (d.kind === 'drop') {
69
+ const vals = d.values.map(v => `'${v}'`).join(', ')
70
+ lines.push(`-- Reverse: recreate enum ${d.name}`)
71
+ lines.push(`CREATE TYPE "${d.name}" AS ENUM (${vals});\n`)
72
+ }
73
+ }
74
+ return lines.join('\n').trim()
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Tables
79
+ // ---------------------------------------------------------------------------
80
+
81
+ private generateTables(diffs: TableDiff[]): Map<string, { up: string; down: string }> {
82
+ const result = new Map<string, { up: string; down: string }>()
83
+
84
+ for (const d of diffs) {
85
+ if (d.kind === 'create') {
86
+ result.set(d.table.name, {
87
+ up: this.generateCreateTable(
88
+ d.table.name,
89
+ d.table.columns,
90
+ d.table.primaryKey?.columns ?? []
91
+ ),
92
+ down: `-- Drop table: ${d.table.name}\nDROP TABLE IF EXISTS "${d.table.name}" CASCADE;`,
93
+ })
94
+ } else if (d.kind === 'drop') {
95
+ result.set(d.table.name, {
96
+ up: `-- Drop table: ${d.table.name}\nDROP TABLE IF EXISTS "${d.table.name}" CASCADE;`,
97
+ down: this.generateCreateTable(
98
+ d.table.name,
99
+ d.table.columns,
100
+ d.table.primaryKey?.columns ?? []
101
+ ),
102
+ })
103
+ } else if (d.kind === 'modify') {
104
+ result.set(d.tableName, {
105
+ up: this.generateAlterUp(d.tableName, d.columns),
106
+ down: this.generateAlterDown(d.tableName, d.columns),
107
+ })
108
+ }
109
+ }
110
+
111
+ return result
112
+ }
113
+
114
+ private generateCreateTable(
115
+ name: string,
116
+ columns: ColumnDefinition[],
117
+ pkColumns: string[]
118
+ ): string {
119
+ const lines: string[] = []
120
+ lines.push(`-- Create table: ${name}`)
121
+ lines.push(`CREATE TABLE IF NOT EXISTS "${name}" (`)
122
+
123
+ const colDefs: string[] = []
124
+ for (const c of columns) {
125
+ colDefs.push(` ${columnToSql(c)}`)
126
+ }
127
+ if (pkColumns.length > 0) {
128
+ const pkCols = pkColumns.map(c => `"${c}"`).join(', ')
129
+ colDefs.push(` CONSTRAINT "pk_${name}" PRIMARY KEY (${pkCols})`)
130
+ }
131
+ lines.push(colDefs.join(',\n'))
132
+ lines.push(');')
133
+ return lines.join('\n')
134
+ }
135
+
136
+ private generateAlterUp(tableName: string, columns: ColumnDiff[]): string {
137
+ const lines: string[] = []
138
+ lines.push(`-- Modify table: ${tableName}`)
139
+
140
+ for (const c of columns) {
141
+ if (c.kind === 'add') {
142
+ lines.push(`ALTER TABLE "${tableName}" ADD COLUMN ${columnToSql(c.column)};`)
143
+ } else if (c.kind === 'drop') {
144
+ lines.push(`ALTER TABLE "${tableName}" DROP COLUMN IF EXISTS "${c.column.name}";`)
145
+ } else if (c.kind === 'alter') {
146
+ if (c.typeChange) {
147
+ const newType = pgTypeToSql(c.typeChange.to)
148
+ lines.push(
149
+ `ALTER TABLE "${tableName}" ALTER COLUMN "${c.columnName}" TYPE ${newType} USING "${c.columnName}"::${newType};`
150
+ )
151
+ }
152
+ if (c.nullableChange) {
153
+ if (c.nullableChange.to) {
154
+ lines.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${c.columnName}" SET NOT NULL;`)
155
+ } else {
156
+ lines.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${c.columnName}" DROP NOT NULL;`)
157
+ }
158
+ }
159
+ if (c.defaultChange) {
160
+ if (c.defaultChange.to !== undefined) {
161
+ lines.push(
162
+ `ALTER TABLE "${tableName}" ALTER COLUMN "${c.columnName}" SET DEFAULT ${defaultValueToSql(c.defaultChange.to)};`
163
+ )
164
+ } else {
165
+ lines.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${c.columnName}" DROP DEFAULT;`)
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ return lines.join('\n')
172
+ }
173
+
174
+ private generateAlterDown(tableName: string, columns: ColumnDiff[]): string {
175
+ const lines: string[] = []
176
+ lines.push(`-- Reverse modify table: ${tableName}`)
177
+
178
+ // Reverse in opposite order
179
+ for (const c of [...columns].reverse()) {
180
+ if (c.kind === 'add') {
181
+ // Reverse of add is drop
182
+ lines.push(`ALTER TABLE "${tableName}" DROP COLUMN IF EXISTS "${c.column.name}";`)
183
+ } else if (c.kind === 'drop') {
184
+ // Reverse of drop is add
185
+ lines.push(`ALTER TABLE "${tableName}" ADD COLUMN ${columnToSql(c.column)};`)
186
+ } else if (c.kind === 'alter') {
187
+ // Reverse each change
188
+ if (c.defaultChange) {
189
+ if (c.defaultChange.from !== undefined) {
190
+ lines.push(
191
+ `ALTER TABLE "${tableName}" ALTER COLUMN "${c.columnName}" SET DEFAULT ${defaultValueToSql(c.defaultChange.from)};`
192
+ )
193
+ } else {
194
+ lines.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${c.columnName}" DROP DEFAULT;`)
195
+ }
196
+ }
197
+ if (c.nullableChange) {
198
+ if (c.nullableChange.from) {
199
+ lines.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${c.columnName}" SET NOT NULL;`)
200
+ } else {
201
+ lines.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${c.columnName}" DROP NOT NULL;`)
202
+ }
203
+ }
204
+ if (c.typeChange) {
205
+ const oldType = pgTypeToSql(c.typeChange.from)
206
+ lines.push(
207
+ `ALTER TABLE "${tableName}" ALTER COLUMN "${c.columnName}" TYPE ${oldType} USING "${c.columnName}"::${oldType};`
208
+ )
209
+ }
210
+ }
211
+ }
212
+
213
+ return lines.join('\n')
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Constraints
218
+ // ---------------------------------------------------------------------------
219
+
220
+ private generateConstraintsUp(diffs: ConstraintDiff[]): string {
221
+ const lines: string[] = []
222
+ for (const d of diffs) {
223
+ if (d.kind === 'add_fk') {
224
+ const name = `fk_${d.tableName}_${d.constraint.columns.join('_')}`
225
+ const cols = d.constraint.columns.map(c => `"${c}"`).join(', ')
226
+ const refCols = d.constraint.referencedColumns.map(c => `"${c}"`).join(', ')
227
+ lines.push(
228
+ `ALTER TABLE "${d.tableName}" ADD CONSTRAINT "${name}" FOREIGN KEY (${cols}) REFERENCES "${d.constraint.referencedTable}" (${refCols}) ON DELETE ${d.constraint.onDelete} ON UPDATE ${d.constraint.onUpdate};`
229
+ )
230
+ } else if (d.kind === 'drop_fk') {
231
+ const name = `fk_${d.tableName}_${d.constraint.columns.join('_')}`
232
+ lines.push(`ALTER TABLE "${d.tableName}" DROP CONSTRAINT IF EXISTS "${name}";`)
233
+ } else if (d.kind === 'add_unique') {
234
+ const name = `uq_${d.tableName}_${d.constraint.columns.join('_')}`
235
+ const cols = d.constraint.columns.map(c => `"${c}"`).join(', ')
236
+ lines.push(`ALTER TABLE "${d.tableName}" ADD CONSTRAINT "${name}" UNIQUE (${cols});`)
237
+ } else if (d.kind === 'drop_unique') {
238
+ const name = `uq_${d.tableName}_${d.constraint.columns.join('_')}`
239
+ lines.push(`ALTER TABLE "${d.tableName}" DROP CONSTRAINT IF EXISTS "${name}";`)
240
+ }
241
+ }
242
+ return lines.join('\n').trim()
243
+ }
244
+
245
+ private generateConstraintsDown(diffs: ConstraintDiff[]): string {
246
+ const lines: string[] = []
247
+ // Reverse: adds become drops, drops become adds
248
+ for (const d of [...diffs].reverse()) {
249
+ if (d.kind === 'add_fk') {
250
+ const name = `fk_${d.tableName}_${d.constraint.columns.join('_')}`
251
+ lines.push(`ALTER TABLE "${d.tableName}" DROP CONSTRAINT IF EXISTS "${name}";`)
252
+ } else if (d.kind === 'drop_fk') {
253
+ const name = `fk_${d.tableName}_${d.constraint.columns.join('_')}`
254
+ const cols = d.constraint.columns.map(c => `"${c}"`).join(', ')
255
+ const refCols = d.constraint.referencedColumns.map(c => `"${c}"`).join(', ')
256
+ lines.push(
257
+ `ALTER TABLE "${d.tableName}" ADD CONSTRAINT "${name}" FOREIGN KEY (${cols}) REFERENCES "${d.constraint.referencedTable}" (${refCols}) ON DELETE ${d.constraint.onDelete} ON UPDATE ${d.constraint.onUpdate};`
258
+ )
259
+ } else if (d.kind === 'add_unique') {
260
+ const name = `uq_${d.tableName}_${d.constraint.columns.join('_')}`
261
+ lines.push(`ALTER TABLE "${d.tableName}" DROP CONSTRAINT IF EXISTS "${name}";`)
262
+ } else if (d.kind === 'drop_unique') {
263
+ const name = `uq_${d.tableName}_${d.constraint.columns.join('_')}`
264
+ const cols = d.constraint.columns.map(c => `"${c}"`).join(', ')
265
+ lines.push(`ALTER TABLE "${d.tableName}" ADD CONSTRAINT "${name}" UNIQUE (${cols});`)
266
+ }
267
+ }
268
+ return lines.join('\n').trim()
269
+ }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // Indexes
273
+ // ---------------------------------------------------------------------------
274
+
275
+ private generateIndexesUp(diffs: IndexDiff[]): string {
276
+ const lines: string[] = []
277
+ for (const d of diffs) {
278
+ if (d.kind === 'add') {
279
+ const name = indexName(d.tableName, d.index)
280
+ const cols = d.index.columns.map(c => `"${c}"`).join(', ')
281
+ const unique = d.index.unique ? 'UNIQUE ' : ''
282
+ lines.push(`CREATE ${unique}INDEX IF NOT EXISTS "${name}" ON "${d.tableName}" (${cols});`)
283
+ } else if (d.kind === 'drop') {
284
+ const name = indexName(d.tableName, d.index)
285
+ lines.push(`DROP INDEX IF EXISTS "${name}";`)
286
+ }
287
+ }
288
+ return lines.join('\n').trim()
289
+ }
290
+
291
+ private generateIndexesDown(diffs: IndexDiff[]): string {
292
+ const lines: string[] = []
293
+ for (const d of [...diffs].reverse()) {
294
+ if (d.kind === 'add') {
295
+ const name = indexName(d.tableName, d.index)
296
+ lines.push(`DROP INDEX IF EXISTS "${name}";`)
297
+ } else if (d.kind === 'drop') {
298
+ const name = indexName(d.tableName, d.index)
299
+ const cols = d.index.columns.map(c => `"${c}"`).join(', ')
300
+ const unique = d.index.unique ? 'UNIQUE ' : ''
301
+ lines.push(`CREATE ${unique}INDEX IF NOT EXISTS "${name}" ON "${d.tableName}" (${cols});`)
302
+ }
303
+ }
304
+ return lines.join('\n').trim()
305
+ }
306
+ }
307
+
308
+ // ---------------------------------------------------------------------------
309
+ // Helpers (exported for testing)
310
+ // ---------------------------------------------------------------------------
311
+
312
+ function indexName(tableName: string, idx: { columns: string[]; unique: boolean }): string {
313
+ const suffix = idx.unique ? '_unique' : ''
314
+ return `idx_${tableName}_${idx.columns.join('_')}${suffix}`
315
+ }
316
+
317
+ /** Render a column definition as a SQL fragment (no trailing comma). */
318
+ export function columnToSql(c: ColumnDefinition): string {
319
+ const typeSql = pgTypeToSql(c.pgType, c)
320
+ const parts = [`"${c.name}" ${typeSql}`]
321
+
322
+ // Serial types handle NOT NULL and default implicitly
323
+ if (!isSerial(c.pgType)) {
324
+ if (c.notNull) parts.push('NOT NULL')
325
+ if (c.defaultValue !== undefined) parts.push(`DEFAULT ${defaultValueToSql(c.defaultValue)}`)
326
+ }
327
+
328
+ return parts.join(' ')
329
+ }
330
+
331
+ /** Convert a PostgreSQLType (+ optional column metadata) to a SQL type string. */
332
+ export function pgTypeToSql(pgType: PostgreSQLType, col?: ColumnDefinition): string {
333
+ if (typeof pgType === 'object') {
334
+ if (pgType.type === 'custom') return `"${pgType.name}"`
335
+ if (pgType.type === 'array') return `${pgTypeToSql(pgType.element as PostgreSQLType)}[]`
336
+ }
337
+
338
+ // Multi-word types
339
+ const multiWord: Record<string, string> = {
340
+ double_precision: 'DOUBLE PRECISION',
341
+ character_varying: 'CHARACTER VARYING',
342
+ bit_varying: 'BIT VARYING',
343
+ }
344
+ if (typeof pgType === 'string' && pgType in multiWord) {
345
+ return multiWord[pgType]!
346
+ }
347
+
348
+ const upper = (pgType as string).toUpperCase()
349
+
350
+ // Types with parameters
351
+ if (col) {
352
+ if ((pgType === 'varchar' || pgType === 'character_varying') && col.length) {
353
+ return `VARCHAR(${col.length})`
354
+ }
355
+ if ((pgType === 'char' || pgType === 'character') && col.length) {
356
+ return `CHAR(${col.length})`
357
+ }
358
+ if ((pgType === 'numeric' || pgType === 'decimal') && col.precision !== undefined) {
359
+ if (col.scale !== undefined) return `${upper}(${col.precision},${col.scale})`
360
+ return `${upper}(${col.precision})`
361
+ }
362
+ }
363
+
364
+ return upper
365
+ }
366
+
367
+ /** Convert a DefaultValue to its SQL representation. */
368
+ export function defaultValueToSql(dv: DefaultValue): string {
369
+ if (dv.kind === 'expression') return dv.sql
370
+ if (dv.value === null) return 'NULL'
371
+ if (typeof dv.value === 'boolean') return dv.value ? 'true' : 'false'
372
+ if (typeof dv.value === 'number') return String(dv.value)
373
+ return `'${dv.value}'`
374
+ }
375
+
376
+ function isSerial(pgType: PostgreSQLType): boolean {
377
+ return pgType === 'serial' || pgType === 'bigserial' || pgType === 'smallserial'
378
+ }
@@ -0,0 +1,76 @@
1
+ import type Database from '../database.ts'
2
+ import type { MigrationRecord } from './types.ts'
3
+
4
+ const TABLE_NAME = '_strav_migrations'
5
+
6
+ /**
7
+ * Manages the `_strav_migrations` tracking table.
8
+ *
9
+ * Keeps track of which migration versions have been applied and
10
+ * groups them into batches so rollbacks can undo an entire batch at once.
11
+ */
12
+ export default class MigrationTracker {
13
+ constructor(private db: Database) {}
14
+
15
+ /** Create the tracking table if it does not exist. */
16
+ async ensureTable(): Promise<void> {
17
+ await this.db.sql.unsafe(`
18
+ CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (
19
+ id SERIAL PRIMARY KEY,
20
+ version VARCHAR(255) NOT NULL,
21
+ batch INTEGER NOT NULL,
22
+ executed_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
23
+ )
24
+ `)
25
+ }
26
+
27
+ /** Return all applied migration records ordered by version. */
28
+ async getAppliedMigrations(): Promise<MigrationRecord[]> {
29
+ return (await this.db.sql.unsafe(
30
+ `SELECT id, version, batch, executed_at FROM ${TABLE_NAME} ORDER BY version`
31
+ )) as MigrationRecord[]
32
+ }
33
+
34
+ /** Given a list of all known versions, return those not yet applied. */
35
+ async getPendingVersions(allVersions: string[]): Promise<string[]> {
36
+ const applied = await this.getAppliedMigrations()
37
+ const appliedSet = new Set(applied.map(r => r.version))
38
+ return allVersions.filter(v => !appliedSet.has(v))
39
+ }
40
+
41
+ /** Return the highest batch number, or 0 if no migrations exist. */
42
+ async getLastBatch(): Promise<number> {
43
+ const rows = await this.db.sql.unsafe(
44
+ `SELECT COALESCE(MAX(batch), 0) AS max_batch FROM ${TABLE_NAME}`
45
+ )
46
+ return (rows[0] as any).max_batch
47
+ }
48
+
49
+ /** Record a migration as applied. */
50
+ async recordMigration(version: string, batch: number): Promise<void> {
51
+ await this.db.sql.unsafe(`INSERT INTO ${TABLE_NAME} (version, batch) VALUES ($1, $2)`, [
52
+ version,
53
+ batch,
54
+ ])
55
+ }
56
+
57
+ /** Remove a migration record by version. */
58
+ async removeMigration(version: string): Promise<void> {
59
+ await this.db.sql.unsafe(`DELETE FROM ${TABLE_NAME} WHERE version = $1`, [version])
60
+ }
61
+
62
+ /** Return all migrations for a specific batch. */
63
+ async getMigrationsByBatch(batch: number): Promise<MigrationRecord[]> {
64
+ return (await this.db.sql.unsafe(
65
+ `SELECT id, version, batch, executed_at FROM ${TABLE_NAME} WHERE batch = $1 ORDER BY version DESC`,
66
+ [batch]
67
+ )) as MigrationRecord[]
68
+ }
69
+
70
+ /** Return migrations from the latest batch (most recent run). */
71
+ async getLatestBatchMigrations(): Promise<MigrationRecord[]> {
72
+ const lastBatch = await this.getLastBatch()
73
+ if (lastBatch === 0) return []
74
+ return this.getMigrationsByBatch(lastBatch)
75
+ }
76
+ }
@@ -0,0 +1,189 @@
1
+ import type {
2
+ ColumnDefinition,
3
+ ForeignKeyConstraint,
4
+ UniqueConstraint,
5
+ IndexDefinition,
6
+ TableDefinition,
7
+ } from '../../schema/database_representation.ts'
8
+ import type { PostgreSQLType } from '../../schema/postgres.ts'
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Enum diffs
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export interface EnumCreate {
15
+ kind: 'create'
16
+ name: string
17
+ values: string[]
18
+ }
19
+
20
+ export interface EnumDrop {
21
+ kind: 'drop'
22
+ name: string
23
+ values: string[]
24
+ }
25
+
26
+ export interface EnumModify {
27
+ kind: 'modify'
28
+ name: string
29
+ addedValues: string[]
30
+ }
31
+
32
+ export type EnumDiff = EnumCreate | EnumDrop | EnumModify
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Column diffs
36
+ // ---------------------------------------------------------------------------
37
+
38
+ export interface ColumnAdd {
39
+ kind: 'add'
40
+ column: ColumnDefinition
41
+ }
42
+
43
+ export interface ColumnDrop {
44
+ kind: 'drop'
45
+ column: ColumnDefinition
46
+ }
47
+
48
+ export interface ColumnAlter {
49
+ kind: 'alter'
50
+ columnName: string
51
+ typeChange?: { from: PostgreSQLType; to: PostgreSQLType }
52
+ nullableChange?: { from: boolean; to: boolean }
53
+ defaultChange?: {
54
+ from: ColumnDefinition['defaultValue']
55
+ to: ColumnDefinition['defaultValue']
56
+ }
57
+ }
58
+
59
+ export type ColumnDiff = ColumnAdd | ColumnDrop | ColumnAlter
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Table diffs
63
+ // ---------------------------------------------------------------------------
64
+
65
+ export interface TableCreate {
66
+ kind: 'create'
67
+ table: TableDefinition
68
+ }
69
+
70
+ export interface TableDrop {
71
+ kind: 'drop'
72
+ table: TableDefinition
73
+ }
74
+
75
+ export interface TableModify {
76
+ kind: 'modify'
77
+ tableName: string
78
+ columns: ColumnDiff[]
79
+ }
80
+
81
+ export type TableDiff = TableCreate | TableDrop | TableModify
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Constraint diffs
85
+ // ---------------------------------------------------------------------------
86
+
87
+ export interface ForeignKeyAdd {
88
+ kind: 'add_fk'
89
+ tableName: string
90
+ constraint: ForeignKeyConstraint
91
+ }
92
+
93
+ export interface ForeignKeyDrop {
94
+ kind: 'drop_fk'
95
+ tableName: string
96
+ constraint: ForeignKeyConstraint
97
+ }
98
+
99
+ export interface UniqueAdd {
100
+ kind: 'add_unique'
101
+ tableName: string
102
+ constraint: UniqueConstraint
103
+ }
104
+
105
+ export interface UniqueDrop {
106
+ kind: 'drop_unique'
107
+ tableName: string
108
+ constraint: UniqueConstraint
109
+ }
110
+
111
+ export type ConstraintDiff = ForeignKeyAdd | ForeignKeyDrop | UniqueAdd | UniqueDrop
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Index diffs
115
+ // ---------------------------------------------------------------------------
116
+
117
+ export interface IndexAdd {
118
+ kind: 'add'
119
+ tableName: string
120
+ index: IndexDefinition
121
+ }
122
+
123
+ export interface IndexDrop {
124
+ kind: 'drop'
125
+ tableName: string
126
+ index: IndexDefinition
127
+ }
128
+
129
+ export type IndexDiff = IndexAdd | IndexDrop
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Top-level schema diff
133
+ // ---------------------------------------------------------------------------
134
+
135
+ export interface SchemaDiff {
136
+ enums: EnumDiff[]
137
+ tables: TableDiff[]
138
+ constraints: ConstraintDiff[]
139
+ indexes: IndexDiff[]
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Generated SQL output
144
+ // ---------------------------------------------------------------------------
145
+
146
+ export interface GeneratedSql {
147
+ enumsUp: string
148
+ enumsDown: string
149
+ tables: Map<string, { up: string; down: string }>
150
+ constraintsUp: string
151
+ constraintsDown: string
152
+ indexesUp: string
153
+ indexesDown: string
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Migration manifest (stored as manifest.json)
158
+ // ---------------------------------------------------------------------------
159
+
160
+ export interface MigrationSummary {
161
+ tablesToCreate: number
162
+ tablesToDrop: number
163
+ tablesToModify: number
164
+ enumsToCreate: number
165
+ enumsToModify: number
166
+ enumsToDrop: number
167
+ }
168
+
169
+ export interface MigrationManifest {
170
+ version: string
171
+ message: string
172
+ generatedAt: string
173
+ summary: MigrationSummary
174
+ executionOrder: {
175
+ up: string[]
176
+ down: string[]
177
+ }
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Migration tracking record (from _strav_migrations table)
182
+ // ---------------------------------------------------------------------------
183
+
184
+ export interface MigrationRecord {
185
+ id: number
186
+ version: string
187
+ batch: number
188
+ executed_at: Date
189
+ }