@everystack/cli 0.2.39 → 0.3.3
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 +28 -5
- package/package.json +12 -2
- package/src/cli/authz-compile.ts +364 -0
- package/src/cli/authz-contract-io.ts +92 -0
- package/src/cli/authz-contract.ts +589 -0
- package/src/cli/authz-owner-probe.ts +218 -0
- package/src/cli/authz-reconcile.ts +152 -0
- package/src/cli/authz-redteam.ts +251 -0
- package/src/cli/authz-render.ts +248 -0
- package/src/cli/aws.ts +100 -0
- package/src/cli/backup.ts +99 -0
- package/src/cli/commands/db-authz.ts +300 -0
- package/src/cli/commands/db-backup.ts +218 -0
- package/src/cli/commands/db-generate.ts +211 -0
- package/src/cli/commands/db-pull.ts +83 -0
- package/src/cli/commands/db-snapshot.ts +113 -0
- package/src/cli/commands/deploy.ts +46 -0
- package/src/cli/config.ts +8 -0
- package/src/cli/index.ts +68 -0
- package/src/cli/migration-compile.ts +133 -0
- package/src/cli/migration-generate.ts +178 -0
- package/src/cli/model-api.ts +36 -0
- package/src/cli/model-render.ts +262 -0
- package/src/cli/pg-ident.ts +59 -0
- package/src/cli/rds-snapshot.ts +47 -0
- package/src/cli/schema-compile.ts +496 -0
- package/src/cli/schema-diff.ts +608 -0
- package/src/cli/schema-introspect.ts +425 -0
- package/src/cli/schema-source.ts +416 -0
- package/src/cli/security-audit.ts +7 -0
- package/src/cli/security-catalog.ts +16 -1
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whole-DB introspection — the data layer (the counterpart to authz-contract.ts).
|
|
3
|
+
*
|
|
4
|
+
* authz-contract reads RLS, policies, grants, and functions. This reads the rest
|
|
5
|
+
* of the database state drizzle-kit's pull would — columns and their pg-precise
|
|
6
|
+
* types, primary keys, uniques, checks, and foreign keys — into a structured
|
|
7
|
+
* SchemaSnapshot. That snapshot is what `db:generate` diffs the compiled Model
|
|
8
|
+
* against, and what `db:pull` reverses into `field()` declarations.
|
|
9
|
+
*
|
|
10
|
+
* Same contract as the authz introspection: the SQL runs through the ops Lambda
|
|
11
|
+
* (`db:query`); the mappers here are pure and unit-tested without a database.
|
|
12
|
+
* Types come from `format_type` (the canonical 'timestamp with time zone',
|
|
13
|
+
* 'numeric(12,2)', 'uuid' the Model compiler also emits), so the two sides diff.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { IGNORED_SCHEMAS, coerceBool, type QueryRunner } from './authz-contract.js';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// The data-layer snapshot — the structured shape both producers target.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
export interface ColumnSchema {
|
|
23
|
+
/** SQL column name (snake_case). */
|
|
24
|
+
name: string;
|
|
25
|
+
/** `format_type` output — the pg-precise type, e.g. `timestamp with time zone`, `numeric(12,2)`. */
|
|
26
|
+
type: string;
|
|
27
|
+
notNull: boolean;
|
|
28
|
+
/** The DEFAULT expression (deparsed), or null. e.g. `now()`, `gen_random_uuid()`, `'draft'::text`. */
|
|
29
|
+
default: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ForeignKey {
|
|
33
|
+
name: string;
|
|
34
|
+
columns: string[];
|
|
35
|
+
refTable: string;
|
|
36
|
+
refColumns: string[];
|
|
37
|
+
/** Lowercased referential actions, omitted when the Postgres default (`no action`). */
|
|
38
|
+
onDelete?: string;
|
|
39
|
+
onUpdate?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UniqueConstraint {
|
|
43
|
+
name: string;
|
|
44
|
+
columns: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CheckConstraint {
|
|
48
|
+
name: string;
|
|
49
|
+
/** The predicate text, as `pg_get_constraintdef` deparses it. */
|
|
50
|
+
expr: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A standalone index (NOT one backing a PK or UNIQUE constraint — those are excluded). */
|
|
54
|
+
export interface IndexSchema {
|
|
55
|
+
name: string;
|
|
56
|
+
columns: string[];
|
|
57
|
+
unique: boolean;
|
|
58
|
+
/** Partial-index predicate (`WHERE …`), omitted when the index is total. */
|
|
59
|
+
where?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface TableSchema {
|
|
63
|
+
/** Schema-qualified: `public.posts`. */
|
|
64
|
+
table: string;
|
|
65
|
+
/** Columns in ordinal position. */
|
|
66
|
+
columns: ColumnSchema[];
|
|
67
|
+
/** Primary-key column names (empty when the table has no PK). */
|
|
68
|
+
primaryKey: string[];
|
|
69
|
+
uniques: UniqueConstraint[];
|
|
70
|
+
checks: CheckConstraint[];
|
|
71
|
+
foreignKeys: ForeignKey[];
|
|
72
|
+
indexes: IndexSchema[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A Postgres enum type — its name and its values in `enumsortorder` (the declared order). */
|
|
76
|
+
export interface EnumType {
|
|
77
|
+
name: string;
|
|
78
|
+
values: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SchemaSnapshot {
|
|
82
|
+
tables: TableSchema[];
|
|
83
|
+
/** Enum types in the schema (`CREATE TYPE … AS ENUM`). Absent/empty when none. */
|
|
84
|
+
enums?: EnumType[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Introspection SQL.
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Every column of every base table, with its precise type (`format_type`),
|
|
93
|
+
* nullability, and default. Extension-owned tables are excluded via pg_depend,
|
|
94
|
+
* matching the authz queries.
|
|
95
|
+
*/
|
|
96
|
+
export const COLUMNS_SQL = `
|
|
97
|
+
SELECT
|
|
98
|
+
n.nspname AS schema,
|
|
99
|
+
c.relname AS "table",
|
|
100
|
+
a.attname AS "column",
|
|
101
|
+
format_type(a.atttypid, a.atttypmod) AS type,
|
|
102
|
+
a.attnotnull AS not_null,
|
|
103
|
+
pg_get_expr(d.adbin, d.adrelid) AS "default",
|
|
104
|
+
a.attnum AS position
|
|
105
|
+
FROM pg_attribute a
|
|
106
|
+
JOIN pg_class c ON c.oid = a.attrelid
|
|
107
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
108
|
+
LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
|
|
109
|
+
WHERE c.relkind = 'r'
|
|
110
|
+
AND a.attnum > 0 AND NOT a.attisdropped
|
|
111
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
112
|
+
AND n.nspname NOT LIKE 'pg_%'
|
|
113
|
+
AND NOT EXISTS (
|
|
114
|
+
SELECT 1 FROM pg_depend dep WHERE dep.objid = c.oid AND dep.deptype = 'e'
|
|
115
|
+
)
|
|
116
|
+
ORDER BY n.nspname, c.relname, a.attnum;
|
|
117
|
+
`.trim();
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Every table constraint — primary key, unique, check, foreign key — as its
|
|
121
|
+
* canonical `pg_get_constraintdef` text. The mapper parses that text (stable
|
|
122
|
+
* deparser output) into structured columns/refs, the same way pg_dump-class tools
|
|
123
|
+
* read it.
|
|
124
|
+
*/
|
|
125
|
+
export const CONSTRAINTS_SQL = `
|
|
126
|
+
SELECT
|
|
127
|
+
n.nspname AS schema,
|
|
128
|
+
c.relname AS "table",
|
|
129
|
+
con.conname AS name,
|
|
130
|
+
con.contype AS type,
|
|
131
|
+
pg_get_constraintdef(con.oid) AS definition
|
|
132
|
+
FROM pg_constraint con
|
|
133
|
+
JOIN pg_class c ON c.oid = con.conrelid
|
|
134
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
135
|
+
WHERE con.contype IN ('p', 'u', 'c', 'f')
|
|
136
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
137
|
+
AND n.nspname NOT LIKE 'pg_%'
|
|
138
|
+
AND NOT EXISTS (
|
|
139
|
+
SELECT 1 FROM pg_depend dep WHERE dep.objid = c.oid AND dep.deptype = 'e'
|
|
140
|
+
)
|
|
141
|
+
ORDER BY n.nspname, c.relname, con.conname;
|
|
142
|
+
`.trim();
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Every enum type and its values, in `enumsortorder` (the order the values were declared,
|
|
146
|
+
* which `CREATE TYPE` must reproduce). Extension-owned types are excluded via pg_depend,
|
|
147
|
+
* matching the column/constraint queries.
|
|
148
|
+
*/
|
|
149
|
+
export const ENUMS_SQL = `
|
|
150
|
+
SELECT
|
|
151
|
+
n.nspname AS schema,
|
|
152
|
+
t.typname AS name,
|
|
153
|
+
array_agg(e.enumlabel ORDER BY e.enumsortorder) AS values
|
|
154
|
+
FROM pg_type t
|
|
155
|
+
JOIN pg_enum e ON e.enumtypid = t.oid
|
|
156
|
+
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
157
|
+
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
158
|
+
AND n.nspname NOT LIKE 'pg_%'
|
|
159
|
+
AND NOT EXISTS (
|
|
160
|
+
SELECT 1 FROM pg_depend dep WHERE dep.objid = t.oid AND dep.deptype = 'e'
|
|
161
|
+
)
|
|
162
|
+
GROUP BY n.nspname, t.typname
|
|
163
|
+
ORDER BY n.nspname, t.typname;
|
|
164
|
+
`.trim();
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Standalone indexes — `CREATE INDEX` the app declares, NOT the implicit indexes that back a
|
|
168
|
+
* primary key or unique constraint (those are excluded by the `conindid` check, since they're
|
|
169
|
+
* already represented as PK/unique). Columns come ordered by index position; a partial index
|
|
170
|
+
* carries its `WHERE` predicate. Expression indexes (no plain column list) fold to no columns
|
|
171
|
+
* and are skipped by the assembler.
|
|
172
|
+
*/
|
|
173
|
+
export const INDEXES_SQL = `
|
|
174
|
+
SELECT
|
|
175
|
+
n.nspname AS schema,
|
|
176
|
+
t.relname AS "table",
|
|
177
|
+
i.relname AS name,
|
|
178
|
+
idx.indisunique AS is_unique,
|
|
179
|
+
(SELECT array_agg(a.attname ORDER BY k.ord)
|
|
180
|
+
FROM unnest(idx.indkey) WITH ORDINALITY k(attnum, ord)
|
|
181
|
+
JOIN pg_attribute a ON a.attrelid = idx.indrelid AND a.attnum = k.attnum) AS columns,
|
|
182
|
+
pg_get_expr(idx.indpred, idx.indrelid) AS predicate
|
|
183
|
+
FROM pg_index idx
|
|
184
|
+
JOIN pg_class i ON i.oid = idx.indexrelid
|
|
185
|
+
JOIN pg_class t ON t.oid = idx.indrelid
|
|
186
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
187
|
+
WHERE t.relkind = 'r'
|
|
188
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
189
|
+
AND n.nspname NOT LIKE 'pg_%'
|
|
190
|
+
AND NOT EXISTS (SELECT 1 FROM pg_constraint con WHERE con.conindid = idx.indexrelid)
|
|
191
|
+
AND NOT EXISTS (SELECT 1 FROM pg_depend dep WHERE dep.objid = t.oid AND dep.deptype = 'e')
|
|
192
|
+
ORDER BY n.nspname, t.relname, i.relname;
|
|
193
|
+
`.trim();
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Pure mappers.
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
export interface ColumnRow {
|
|
200
|
+
schema: string;
|
|
201
|
+
table: string;
|
|
202
|
+
column: string;
|
|
203
|
+
type: unknown;
|
|
204
|
+
not_null: unknown;
|
|
205
|
+
default: unknown;
|
|
206
|
+
position: unknown;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function columnRowToDescriptor(row: ColumnRow): { table: string; column: ColumnSchema; position: number } {
|
|
210
|
+
const def = row.default == null ? null : String(row.default);
|
|
211
|
+
return {
|
|
212
|
+
table: `${row.schema}.${row.table}`,
|
|
213
|
+
position: Number(row.position),
|
|
214
|
+
column: {
|
|
215
|
+
name: row.column,
|
|
216
|
+
type: String(row.type),
|
|
217
|
+
notNull: coerceBool(row.not_null),
|
|
218
|
+
default: def && def.length > 0 ? def : null,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Column list inside `(...)`, unquoted and trimmed: `(a, "b")` -> `['a', 'b']`. */
|
|
224
|
+
function parseColumnList(parenGroup: string): string[] {
|
|
225
|
+
const inner = parenGroup.replace(/^\(/, '').replace(/\)$/, '');
|
|
226
|
+
return inner.split(',').map((c) => c.trim().replace(/^"|"$/g, '')).filter(Boolean);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export type ConstraintKind = 'primaryKey' | 'unique' | 'check' | 'foreignKey';
|
|
230
|
+
|
|
231
|
+
export interface ConstraintRow {
|
|
232
|
+
schema: string;
|
|
233
|
+
table: string;
|
|
234
|
+
name: string;
|
|
235
|
+
type: unknown;
|
|
236
|
+
definition: unknown;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface ConstraintDescriptor {
|
|
240
|
+
table: string;
|
|
241
|
+
name: string;
|
|
242
|
+
kind: ConstraintKind;
|
|
243
|
+
/** Local columns (PK / unique / FK). */
|
|
244
|
+
columns?: string[];
|
|
245
|
+
/** CHECK predicate text. */
|
|
246
|
+
expr?: string;
|
|
247
|
+
/** FK target. */
|
|
248
|
+
refTable?: string;
|
|
249
|
+
refColumns?: string[];
|
|
250
|
+
/** FK referential actions, lowercased; omitted when the Postgres default (`no action`). */
|
|
251
|
+
onDelete?: string;
|
|
252
|
+
onUpdate?: string;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Parse one `pg_get_constraintdef` row into a structured descriptor. */
|
|
256
|
+
export function constraintRowToDescriptor(row: ConstraintRow): ConstraintDescriptor {
|
|
257
|
+
const table = `${row.schema}.${row.table}`;
|
|
258
|
+
const def = String(row.definition);
|
|
259
|
+
const type = String(row.type);
|
|
260
|
+
|
|
261
|
+
if (type === 'p' || type === 'u') {
|
|
262
|
+
const cols = def.match(/\(([^)]*)\)/);
|
|
263
|
+
return {
|
|
264
|
+
table, name: row.name,
|
|
265
|
+
kind: type === 'p' ? 'primaryKey' : 'unique',
|
|
266
|
+
columns: cols ? parseColumnList(cols[0]) : [],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (type === 'f') {
|
|
270
|
+
// The ref table may be schema-qualified and/or double-quoted with embedded spaces
|
|
271
|
+
// ("My Schema"."Weird Table"), so match a quoted-or-bare token, not "up to whitespace".
|
|
272
|
+
const m = def.match(/FOREIGN KEY\s*(\([^)]*\))\s*REFERENCES\s+((?:"[^"]*"|[^\s("]+)(?:\.(?:"[^"]*"|[^\s("]+))?)\s*(\([^)]*\))/i);
|
|
273
|
+
const action = (clause: 'DELETE' | 'UPDATE'): string | undefined => {
|
|
274
|
+
const a = def.match(new RegExp(`ON ${clause}\\s+(CASCADE|RESTRICT|NO ACTION|SET NULL|SET DEFAULT)`, 'i'));
|
|
275
|
+
// `NO ACTION` is the default — Postgres omits it from constraintdef, so we omit it too.
|
|
276
|
+
return a && a[1].toUpperCase() !== 'NO ACTION' ? a[1].toLowerCase() : undefined;
|
|
277
|
+
};
|
|
278
|
+
return {
|
|
279
|
+
table, name: row.name, kind: 'foreignKey',
|
|
280
|
+
columns: m ? parseColumnList(m[1]) : [],
|
|
281
|
+
refTable: m ? m[2].replace(/"/g, '') : '',
|
|
282
|
+
refColumns: m ? parseColumnList(m[3]) : [],
|
|
283
|
+
onDelete: action('DELETE'),
|
|
284
|
+
onUpdate: action('UPDATE'),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
// check — keep the predicate text inside the outer CHECK ( ... )
|
|
288
|
+
const m = def.match(/^CHECK\s*\((.*)\)$/s);
|
|
289
|
+
return { table, name: row.name, kind: 'check', expr: m ? m[1].trim() : def };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Assembly.
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
export interface IndexRow {
|
|
297
|
+
schema: string;
|
|
298
|
+
table: string;
|
|
299
|
+
name: string;
|
|
300
|
+
is_unique: unknown;
|
|
301
|
+
columns: unknown;
|
|
302
|
+
predicate: unknown;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function indexRowToDescriptor(row: IndexRow): { table: string; index: IndexSchema } {
|
|
306
|
+
const columns = Array.isArray(row.columns) ? row.columns.map(String) : [];
|
|
307
|
+
const pred = row.predicate == null ? '' : String(row.predicate);
|
|
308
|
+
return {
|
|
309
|
+
table: `${row.schema}.${row.table}`,
|
|
310
|
+
index: { name: row.name, columns, unique: coerceBool(row.is_unique), ...(pred ? { where: pred } : {}) },
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export interface EnumRow {
|
|
315
|
+
schema: string;
|
|
316
|
+
name: string;
|
|
317
|
+
values: unknown;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function enumRowToDescriptor(row: EnumRow): EnumType {
|
|
321
|
+
const values = Array.isArray(row.values) ? row.values.map(String) : [];
|
|
322
|
+
return { name: row.name, values };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export interface SchemaRows {
|
|
326
|
+
columns: ColumnRow[];
|
|
327
|
+
constraints: ConstraintRow[];
|
|
328
|
+
enums?: EnumRow[];
|
|
329
|
+
indexes?: IndexRow[];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Fold introspected rows into the snapshot. Deterministic: tables sorted by name,
|
|
334
|
+
* columns by ordinal position, and each constraint list sorted by name — so a
|
|
335
|
+
* re-pull of an unchanged database is byte-stable.
|
|
336
|
+
*/
|
|
337
|
+
export function assembleSchema(rows: SchemaRows): SchemaSnapshot {
|
|
338
|
+
const tables = new Map<string, TableSchema>();
|
|
339
|
+
const positions = new Map<string, Map<string, number>>();
|
|
340
|
+
|
|
341
|
+
const ensure = (table: string): TableSchema => {
|
|
342
|
+
let t = tables.get(table);
|
|
343
|
+
if (!t) {
|
|
344
|
+
t = { table, columns: [], primaryKey: [], uniques: [], checks: [], foreignKeys: [], indexes: [] };
|
|
345
|
+
tables.set(table, t);
|
|
346
|
+
positions.set(table, new Map());
|
|
347
|
+
}
|
|
348
|
+
return t;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
for (const row of rows.columns) {
|
|
352
|
+
if (IGNORED_SCHEMAS.has(row.schema)) continue;
|
|
353
|
+
const { table, column, position } = columnRowToDescriptor(row);
|
|
354
|
+
const t = ensure(table);
|
|
355
|
+
t.columns.push(column);
|
|
356
|
+
positions.get(table)!.set(column.name, position);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
for (const t of tables.values()) {
|
|
360
|
+
const pos = positions.get(t.table)!;
|
|
361
|
+
t.columns.sort((a, b) => (pos.get(a.name) ?? 0) - (pos.get(b.name) ?? 0));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
for (const row of rows.constraints) {
|
|
365
|
+
if (IGNORED_SCHEMAS.has(row.schema)) continue;
|
|
366
|
+
const d = constraintRowToDescriptor(row);
|
|
367
|
+
const t = tables.get(d.table);
|
|
368
|
+
if (!t) continue; // a constraint on a table we didn't enumerate (e.g. a view)
|
|
369
|
+
switch (d.kind) {
|
|
370
|
+
case 'primaryKey': t.primaryKey = d.columns ?? []; break;
|
|
371
|
+
case 'unique': t.uniques.push({ name: d.name, columns: d.columns ?? [] }); break;
|
|
372
|
+
case 'check': t.checks.push({ name: d.name, expr: d.expr ?? '' }); break;
|
|
373
|
+
case 'foreignKey':
|
|
374
|
+
t.foreignKeys.push({
|
|
375
|
+
name: d.name, columns: d.columns ?? [], refTable: d.refTable ?? '', refColumns: d.refColumns ?? [],
|
|
376
|
+
...(d.onDelete ? { onDelete: d.onDelete } : {}),
|
|
377
|
+
...(d.onUpdate ? { onUpdate: d.onUpdate } : {}),
|
|
378
|
+
});
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
for (const row of rows.indexes ?? []) {
|
|
384
|
+
if (IGNORED_SCHEMAS.has(row.schema)) continue;
|
|
385
|
+
const { table, index } = indexRowToDescriptor(row);
|
|
386
|
+
const t = tables.get(table);
|
|
387
|
+
if (!t || index.columns.length === 0) continue; // unknown table, or an expression index (no plain columns)
|
|
388
|
+
t.indexes.push(index);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
for (const t of tables.values()) {
|
|
392
|
+
t.uniques.sort((a, b) => a.name.localeCompare(b.name));
|
|
393
|
+
t.checks.sort((a, b) => a.name.localeCompare(b.name));
|
|
394
|
+
t.foreignKeys.sort((a, b) => a.name.localeCompare(b.name));
|
|
395
|
+
t.indexes.sort((a, b) => a.name.localeCompare(b.name));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const enums = (rows.enums ?? [])
|
|
399
|
+
.filter((r) => !IGNORED_SCHEMAS.has(r.schema))
|
|
400
|
+
.map(enumRowToDescriptor)
|
|
401
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
tables: [...tables.values()].sort((a, b) => a.table.localeCompare(b.table)),
|
|
405
|
+
enums,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Introspect the live database into a SchemaSnapshot — the data-layer analog of
|
|
411
|
+
* `introspectContract`. Runs the two read-only SQL constants through the injected runner
|
|
412
|
+
* (the ops Lambda `db:query` in production, a fake in tests) and folds the rows with
|
|
413
|
+
* `assembleSchema`. This is the `current` side `db:generate` diffs the compiled Models against.
|
|
414
|
+
*/
|
|
415
|
+
export async function introspectSchema(run: QueryRunner): Promise<SchemaSnapshot> {
|
|
416
|
+
const [columns, constraints, enums, indexes] = await Promise.all([
|
|
417
|
+
run(COLUMNS_SQL), run(CONSTRAINTS_SQL), run(ENUMS_SQL), run(INDEXES_SQL),
|
|
418
|
+
]);
|
|
419
|
+
return assembleSchema({
|
|
420
|
+
columns: columns as ColumnRow[],
|
|
421
|
+
constraints: constraints as ConstraintRow[],
|
|
422
|
+
enums: enums as EnumRow[],
|
|
423
|
+
indexes: indexes as IndexRow[],
|
|
424
|
+
});
|
|
425
|
+
}
|