@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,446 @@
1
+ import type Database from './database.ts'
2
+ import type {
3
+ DatabaseRepresentation,
4
+ TableDefinition,
5
+ ColumnDefinition,
6
+ EnumDefinition,
7
+ ForeignKeyConstraint,
8
+ PrimaryKeyConstraint,
9
+ UniqueConstraint,
10
+ IndexDefinition,
11
+ DefaultValue,
12
+ } from '../schema/database_representation.ts'
13
+ import type { PostgreSQLType, PostgreSQLCustomType } from '../schema/postgres.ts'
14
+
15
+ /** Maps PostgreSQL internal type names to our PostgreSQLType union. */
16
+ const PG_TYPE_MAP: Record<string, PostgreSQLType> = {
17
+ int2: 'smallint',
18
+ int4: 'integer',
19
+ int8: 'bigint',
20
+ float4: 'real',
21
+ float8: 'double_precision',
22
+ bool: 'boolean',
23
+ varchar: 'varchar',
24
+ bpchar: 'char',
25
+ text: 'text',
26
+ uuid: 'uuid',
27
+ json: 'json',
28
+ jsonb: 'jsonb',
29
+ bytea: 'bytea',
30
+ xml: 'xml',
31
+ inet: 'inet',
32
+ cidr: 'cidr',
33
+ macaddr: 'macaddr',
34
+ macaddr8: 'macaddr8',
35
+ money: 'money',
36
+ numeric: 'numeric',
37
+ timestamp: 'timestamp',
38
+ timestamptz: 'timestamptz',
39
+ date: 'date',
40
+ time: 'time',
41
+ timetz: 'timetz',
42
+ interval: 'interval',
43
+ point: 'point',
44
+ line: 'line',
45
+ lseg: 'lseg',
46
+ box: 'box',
47
+ path: 'path',
48
+ polygon: 'polygon',
49
+ circle: 'circle',
50
+ tsvector: 'tsvector',
51
+ tsquery: 'tsquery',
52
+ int4range: 'int4range',
53
+ int8range: 'int8range',
54
+ numrange: 'numrange',
55
+ tsrange: 'tsrange',
56
+ tstzrange: 'tstzrange',
57
+ daterange: 'daterange',
58
+ bit: 'bit',
59
+ varbit: 'bit_varying',
60
+ }
61
+
62
+ /** Maps integer types to their serial counterparts (for serial detection). */
63
+ const INTEGER_TO_SERIAL: Record<string, PostgreSQLType> = {
64
+ smallint: 'smallserial',
65
+ integer: 'serial',
66
+ bigint: 'bigserial',
67
+ }
68
+
69
+ /**
70
+ * Introspects a live PostgreSQL database and produces a {@link DatabaseRepresentation}
71
+ * that matches the same structure built by the schema's {@link RepresentationBuilder}.
72
+ *
73
+ * Only inspects the `public` schema.
74
+ *
75
+ * @example
76
+ * const introspector = new DatabaseIntrospector(db)
77
+ * const rep = await introspector.introspect()
78
+ */
79
+ export default class DatabaseIntrospector {
80
+ constructor(private db: Database) {}
81
+
82
+ async introspect(): Promise<DatabaseRepresentation> {
83
+ const enums = await this.loadEnums()
84
+ const enumNames = new Set(enums.map(e => e.name))
85
+ const tableNames = await this.loadTables()
86
+
87
+ const tables: TableDefinition[] = []
88
+ for (const name of tableNames) {
89
+ tables.push(await this.loadTable(name, enumNames))
90
+ }
91
+
92
+ return { enums, tables }
93
+ }
94
+
95
+ private async loadTable(name: string, enumNames: Set<string>): Promise<TableDefinition> {
96
+ const [columns, primaryKey, foreignKeys, uniqueConstraints, indexes] = await Promise.all([
97
+ this.loadColumns(name, enumNames),
98
+ this.loadPrimaryKey(name),
99
+ this.loadForeignKeys(name),
100
+ this.loadUniqueConstraints(name),
101
+ this.loadIndexes(name),
102
+ ])
103
+
104
+ // Mark PK columns
105
+ if (primaryKey) {
106
+ const pkSet = new Set(primaryKey.columns)
107
+ for (const col of columns) {
108
+ if (pkSet.has(col.name)) col.primaryKey = true
109
+ }
110
+ }
111
+
112
+ // Mark single-column unique constraints on the column
113
+ for (const uc of uniqueConstraints) {
114
+ if (uc.columns.length === 1) {
115
+ const col = columns.find(c => c.name === uc.columns[0])
116
+ if (col) col.unique = true
117
+ }
118
+ }
119
+
120
+ // Mark indexed columns
121
+ for (const idx of indexes) {
122
+ if (idx.columns.length === 1) {
123
+ const col = columns.find(c => c.name === idx.columns[0])
124
+ if (col) col.index = true
125
+ }
126
+ }
127
+
128
+ return { name, columns, primaryKey, foreignKeys, uniqueConstraints, indexes }
129
+ }
130
+
131
+ // --- Enums ---
132
+
133
+ private async loadEnums(): Promise<EnumDefinition[]> {
134
+ const rows = await this.db.sql`
135
+ SELECT t.typname AS name, e.enumlabel AS value
136
+ FROM pg_type t
137
+ JOIN pg_enum e ON e.enumtypid = t.oid
138
+ WHERE t.typnamespace = (
139
+ SELECT oid FROM pg_namespace WHERE nspname = 'public'
140
+ )
141
+ ORDER BY t.typname, e.enumsortorder
142
+ `
143
+
144
+ const map = new Map<string, string[]>()
145
+ for (const row of rows) {
146
+ const values = map.get(row.name) ?? []
147
+ values.push(row.value)
148
+ map.set(row.name, values)
149
+ }
150
+
151
+ return Array.from(map.entries()).map(([name, values]) => ({ name, values }))
152
+ }
153
+
154
+ // --- Tables ---
155
+
156
+ private async loadTables(): Promise<string[]> {
157
+ const rows = await this.db.sql`
158
+ SELECT table_name
159
+ FROM information_schema.tables
160
+ WHERE table_schema = 'public'
161
+ AND table_type = 'BASE TABLE'
162
+ AND table_name != '_strav_migrations'
163
+ ORDER BY table_name
164
+ `
165
+ return rows.map((r: any) => r.table_name)
166
+ }
167
+
168
+ // --- Columns ---
169
+
170
+ private async loadColumns(table: string, enumNames: Set<string>): Promise<ColumnDefinition[]> {
171
+ const rows = await this.db.sql`
172
+ SELECT
173
+ column_name,
174
+ udt_name,
175
+ data_type,
176
+ is_nullable,
177
+ column_default,
178
+ character_maximum_length,
179
+ numeric_precision,
180
+ numeric_scale
181
+ FROM information_schema.columns
182
+ WHERE table_schema = 'public'
183
+ AND table_name = ${table}
184
+ ORDER BY ordinal_position
185
+ `
186
+
187
+ return rows.map((row: any) => this.rowToColumn(row, enumNames))
188
+ }
189
+
190
+ private rowToColumn(row: any, enumNames: Set<string>): ColumnDefinition {
191
+ const udtName: string = row.udt_name
192
+ const dataType: string = row.data_type
193
+ const columnDefault: string | null = row.column_default
194
+ const notNull = row.is_nullable === 'NO'
195
+
196
+ let pgType = this.mapPgType(udtName, dataType, enumNames)
197
+ let autoIncrement = false
198
+ let isArray = false
199
+ let arrayDimensions = 1
200
+
201
+ // Serial detection: integer + nextval() default
202
+ if (columnDefault?.startsWith('nextval(')) {
203
+ const pgTypeStr = typeof pgType === 'string' ? pgType : null
204
+ if (pgTypeStr && pgTypeStr in INTEGER_TO_SERIAL) {
205
+ pgType = INTEGER_TO_SERIAL[pgTypeStr]!
206
+ autoIncrement = true
207
+ }
208
+ }
209
+
210
+ // Array detection: data_type = 'ARRAY' and udt_name starts with '_'
211
+ if (dataType === 'ARRAY' && udtName.startsWith('_')) {
212
+ isArray = true
213
+ const elementType = udtName.slice(1) // remove leading '_'
214
+ pgType = {
215
+ type: 'array',
216
+ element: this.mapPgType(elementType, elementType, enumNames) as any,
217
+ }
218
+ }
219
+
220
+ const defaultValue = autoIncrement ? undefined : this.parseDefault(columnDefault, pgType)
221
+
222
+ return {
223
+ name: row.column_name,
224
+ pgType,
225
+ notNull,
226
+ defaultValue,
227
+ unique: false, // filled later from constraints
228
+ primaryKey: false, // filled later from PK constraint
229
+ autoIncrement,
230
+ index: false, // filled later from indexes
231
+ isArray,
232
+ arrayDimensions,
233
+ length: row.character_maximum_length ?? undefined,
234
+ precision: row.numeric_precision ?? undefined,
235
+ scale: row.numeric_scale ?? undefined,
236
+ }
237
+ }
238
+
239
+ // --- Primary Key ---
240
+
241
+ private async loadPrimaryKey(table: string): Promise<PrimaryKeyConstraint | null> {
242
+ const rows = await this.db.sql`
243
+ SELECT kcu.column_name
244
+ FROM information_schema.table_constraints tc
245
+ JOIN information_schema.key_column_usage kcu
246
+ ON kcu.constraint_name = tc.constraint_name
247
+ AND kcu.table_schema = tc.table_schema
248
+ WHERE tc.constraint_type = 'PRIMARY KEY'
249
+ AND tc.table_schema = 'public'
250
+ AND tc.table_name = ${table}
251
+ ORDER BY kcu.ordinal_position
252
+ `
253
+
254
+ if (rows.length === 0) return null
255
+
256
+ const columns = rows.map((r: any) => r.column_name)
257
+
258
+ return { columns }
259
+ }
260
+
261
+ // --- Foreign Keys ---
262
+
263
+ private async loadForeignKeys(table: string): Promise<ForeignKeyConstraint[]> {
264
+ const rows = await this.db.sql`
265
+ SELECT
266
+ tc.constraint_name,
267
+ kcu.column_name,
268
+ ccu.table_name AS referenced_table,
269
+ ccu.column_name AS referenced_column,
270
+ rc.delete_rule,
271
+ rc.update_rule
272
+ FROM information_schema.table_constraints tc
273
+ JOIN information_schema.key_column_usage kcu
274
+ ON kcu.constraint_name = tc.constraint_name
275
+ AND kcu.table_schema = tc.table_schema
276
+ JOIN information_schema.constraint_column_usage ccu
277
+ ON ccu.constraint_name = tc.constraint_name
278
+ AND ccu.table_schema = tc.table_schema
279
+ JOIN information_schema.referential_constraints rc
280
+ ON rc.constraint_name = tc.constraint_name
281
+ AND rc.constraint_schema = tc.table_schema
282
+ WHERE tc.constraint_type = 'FOREIGN KEY'
283
+ AND tc.table_schema = 'public'
284
+ AND tc.table_name = ${table}
285
+ ORDER BY tc.constraint_name, kcu.ordinal_position
286
+ `
287
+
288
+ // Group by constraint name (for composite FKs)
289
+ const map = new Map<string, ForeignKeyConstraint>()
290
+ for (const row of rows) {
291
+ const key = row.constraint_name
292
+ if (!map.has(key)) {
293
+ map.set(key, {
294
+ columns: [],
295
+ referencedTable: row.referenced_table,
296
+ referencedColumns: [],
297
+ onDelete: this.normalizeRule(row.delete_rule),
298
+ onUpdate: this.normalizeRule(row.update_rule),
299
+ })
300
+ }
301
+ const fk = map.get(key)!
302
+ fk.columns.push(row.column_name)
303
+ fk.referencedColumns.push(row.referenced_column)
304
+ }
305
+
306
+ return Array.from(map.values())
307
+ }
308
+
309
+ // --- Unique Constraints ---
310
+
311
+ private async loadUniqueConstraints(table: string): Promise<UniqueConstraint[]> {
312
+ const rows = await this.db.sql`
313
+ SELECT tc.constraint_name, kcu.column_name
314
+ FROM information_schema.table_constraints tc
315
+ JOIN information_schema.key_column_usage kcu
316
+ ON kcu.constraint_name = tc.constraint_name
317
+ AND kcu.table_schema = tc.table_schema
318
+ WHERE tc.constraint_type = 'UNIQUE'
319
+ AND tc.table_schema = 'public'
320
+ AND tc.table_name = ${table}
321
+ ORDER BY tc.constraint_name, kcu.ordinal_position
322
+ `
323
+
324
+ const map = new Map<string, string[]>()
325
+ for (const row of rows) {
326
+ const cols = map.get(row.constraint_name) ?? []
327
+ cols.push(row.column_name)
328
+ map.set(row.constraint_name, cols)
329
+ }
330
+
331
+ return Array.from(map.values()).map(columns => ({ columns }))
332
+ }
333
+
334
+ // --- Indexes ---
335
+
336
+ private async loadIndexes(table: string): Promise<IndexDefinition[]> {
337
+ const rows = await this.db.sql`
338
+ SELECT
339
+ i.relname AS index_name,
340
+ a.attname AS column_name,
341
+ ix.indisunique AS is_unique,
342
+ ix.indisprimary AS is_primary
343
+ FROM pg_index ix
344
+ JOIN pg_class t ON t.oid = ix.indrelid
345
+ JOIN pg_class i ON i.oid = ix.indexrelid
346
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
347
+ JOIN pg_namespace n ON n.oid = t.relnamespace
348
+ WHERE n.nspname = 'public'
349
+ AND t.relname = ${table}
350
+ AND NOT ix.indisprimary
351
+ ORDER BY i.relname, a.attnum
352
+ `
353
+
354
+ const map = new Map<string, { columns: string[]; unique: boolean }>()
355
+ for (const row of rows) {
356
+ const key = row.index_name
357
+ if (!map.has(key)) {
358
+ map.set(key, { columns: [], unique: row.is_unique })
359
+ }
360
+ map.get(key)!.columns.push(row.column_name)
361
+ }
362
+
363
+ return Array.from(map.values())
364
+ }
365
+
366
+ // --- Helpers ---
367
+
368
+ private mapPgType(udtName: string, dataType: string, enumNames: Set<string>): PostgreSQLType {
369
+ // Check if it's a known enum
370
+ if (enumNames.has(udtName)) {
371
+ return { type: 'custom', name: udtName } satisfies PostgreSQLCustomType
372
+ }
373
+
374
+ return PG_TYPE_MAP[udtName] ?? (udtName as PostgreSQLType)
375
+ }
376
+
377
+ private parseDefault(
378
+ columnDefault: string | null,
379
+ pgType: PostgreSQLType
380
+ ): DefaultValue | undefined {
381
+ if (columnDefault === null || columnDefault === undefined) return undefined
382
+
383
+ const raw = columnDefault
384
+
385
+ // SQL expressions: gen_random_uuid(), CURRENT_TIMESTAMP, now(), nextval(...)
386
+ if (
387
+ raw.startsWith('gen_random_uuid()') ||
388
+ raw === 'CURRENT_TIMESTAMP' ||
389
+ raw.startsWith('now()') ||
390
+ raw.startsWith('nextval(')
391
+ ) {
392
+ return { kind: 'expression', sql: raw }
393
+ }
394
+
395
+ // Boolean literals
396
+ if (raw === 'true') return { kind: 'literal', value: true }
397
+ if (raw === 'false') return { kind: 'literal', value: false }
398
+
399
+ // NULL
400
+ if (raw === 'NULL' || raw === 'NULL::' + (typeof pgType === 'string' ? pgType : '')) {
401
+ return { kind: 'literal', value: null }
402
+ }
403
+
404
+ // Numeric literals
405
+ const pgTypeStr = typeof pgType === 'string' ? pgType : null
406
+ if (pgTypeStr && isNumericType(pgTypeStr)) {
407
+ const num = Number(raw)
408
+ if (!isNaN(num)) return { kind: 'literal', value: num }
409
+ }
410
+
411
+ // String literals: 'value'::type or 'value'
412
+ const stringMatch = raw.match(/^'(.*?)'(?:::.*)?$/)
413
+ if (stringMatch) {
414
+ return { kind: 'literal', value: stringMatch[1]! }
415
+ }
416
+
417
+ // Fallback: treat as expression
418
+ return { kind: 'expression', sql: raw }
419
+ }
420
+
421
+ private normalizeRule(rule: string): 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION' {
422
+ switch (rule) {
423
+ case 'CASCADE':
424
+ return 'CASCADE'
425
+ case 'SET NULL':
426
+ return 'SET NULL'
427
+ case 'RESTRICT':
428
+ return 'RESTRICT'
429
+ default:
430
+ return 'NO ACTION'
431
+ }
432
+ }
433
+ }
434
+
435
+ function isNumericType(pgType: string): boolean {
436
+ return [
437
+ 'smallint',
438
+ 'integer',
439
+ 'bigint',
440
+ 'real',
441
+ 'double_precision',
442
+ 'decimal',
443
+ 'numeric',
444
+ 'money',
445
+ ].includes(pgType)
446
+ }