@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,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
|
+
}
|