@drzl/analyzer 1.2.0 → 1.4.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/dist/index.cjs +273 -60
- package/dist/index.d.cts +96 -1
- package/dist/index.d.ts +96 -1
- package/dist/index.js +273 -60
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -52,6 +52,197 @@ var SchemaAnalyzer = class {
|
|
|
52
52
|
}
|
|
53
53
|
return table[Symbol.for(key)];
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Drizzle keys the Columns object by TypeScript property name, but every other piece of
|
|
57
|
+
* metadata (foreign keys, indexes, composite primary keys) reports the *database* column
|
|
58
|
+
* name. We report the TS name, since that is what a generated schema has to spell, so
|
|
59
|
+
* anything coming from that other metadata has to be translated back.
|
|
60
|
+
*
|
|
61
|
+
* Falls through to the input when a name is unknown, which keeps raw SQL expressions and
|
|
62
|
+
* columns belonging to another table readable rather than dropping them.
|
|
63
|
+
*/
|
|
64
|
+
dbToTsNames(columnsObj) {
|
|
65
|
+
const map = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const [tsName, col] of Object.entries(columnsObj ?? {})) {
|
|
67
|
+
const dbName = col?.name;
|
|
68
|
+
if (typeof dbName === "string") map.set(dbName, tsName);
|
|
69
|
+
}
|
|
70
|
+
return (dbName) => {
|
|
71
|
+
const raw = typeof dbName === "string" ? dbName : dbName?.name ?? String(dbName);
|
|
72
|
+
return map.get(raw) ?? raw;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Entries from a table's third argument, the extra-config callback.
|
|
77
|
+
*
|
|
78
|
+
* Drizzle invokes this callback with the table's ExtraConfigColumns, NOT with the table.
|
|
79
|
+
* Passing the table throws, and because the whole block used to sit under a bare `catch {}`
|
|
80
|
+
* the throw was swallowed and every index, unique index, composite primary key, check
|
|
81
|
+
* constraint and table-level foreign key silently vanished from the analysis.
|
|
82
|
+
*
|
|
83
|
+
* Both shapes are accepted: modern Drizzle returns an array, older versions an object.
|
|
84
|
+
*/
|
|
85
|
+
extraConfigEntries(tbl, issues, tableName) {
|
|
86
|
+
const builder = this.getSymbol(tbl, "drizzle:ExtraConfigBuilder");
|
|
87
|
+
if (typeof builder !== "function") return [];
|
|
88
|
+
const cols = this.getSymbol(tbl, "drizzle:ExtraConfigColumns") ?? tbl;
|
|
89
|
+
try {
|
|
90
|
+
const built = builder(cols);
|
|
91
|
+
if (!built) return [];
|
|
92
|
+
return Array.isArray(built) ? built : Object.values(built);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
issues.push({
|
|
95
|
+
code: "DRZL_ANL_EXTRACONFIG",
|
|
96
|
+
level: "warn",
|
|
97
|
+
message: `Could not evaluate the extra-config callback for table "${tableName}": ${e.message}`,
|
|
98
|
+
hint: "Indexes, composite keys, checks and table-level foreign keys will be missing for this table."
|
|
99
|
+
});
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Foreign keys declared inline with `.references()`.
|
|
105
|
+
*
|
|
106
|
+
* Drizzle stores these per dialect under `drizzle:PgInlineForeignKeys`,
|
|
107
|
+
* `drizzle:MySqlInlineForeignKeys` and `drizzle:SQLiteInlineForeignKeys`. Matching the
|
|
108
|
+
* suffix rather than listing the three keeps new dialects working without a change here.
|
|
109
|
+
* SingleStore has no entry because it does not support foreign keys at all, which is why
|
|
110
|
+
* `.references()` is not even a function there.
|
|
111
|
+
*/
|
|
112
|
+
inlineForeignKeys(tbl) {
|
|
113
|
+
try {
|
|
114
|
+
for (const s of Object.getOwnPropertySymbols(tbl)) {
|
|
115
|
+
if (/InlineForeignKeys$/.test(s.description ?? "")) {
|
|
116
|
+
const v = tbl[s];
|
|
117
|
+
if (Array.isArray(v)) return v;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Normalise one foreign key, inline or table-level, into a common shape.
|
|
126
|
+
*
|
|
127
|
+
* `.reference()` yields the resolved columns on both. The referential actions do not live
|
|
128
|
+
* in the same place: a built ForeignKey exposes `onDelete`/`onUpdate` as strings, while an
|
|
129
|
+
* unbuilt ForeignKeyBuilder exposes them as the chainable setter functions and keeps the
|
|
130
|
+
* values in `_onDelete`/`_onUpdate`. Reading the wrong one yields a function where a string
|
|
131
|
+
* is expected, so check the type rather than the property's presence.
|
|
132
|
+
*
|
|
133
|
+
* Postgres reports 'no action' where MySQL and SQLite report nothing, for identical schemas.
|
|
134
|
+
* Since 'no action' *is* the default, it is normalised away so the same schema analyses the
|
|
135
|
+
* same across dialects.
|
|
136
|
+
*/
|
|
137
|
+
readForeignKey(fk, toTs) {
|
|
138
|
+
let ref;
|
|
139
|
+
try {
|
|
140
|
+
ref = fk?.reference?.();
|
|
141
|
+
} catch {
|
|
142
|
+
return void 0;
|
|
143
|
+
}
|
|
144
|
+
if (!ref?.foreignTable) return void 0;
|
|
145
|
+
const action = (v, fallback) => {
|
|
146
|
+
const raw = typeof v === "string" ? v : typeof fallback === "string" ? fallback : void 0;
|
|
147
|
+
return raw && raw.toLowerCase() !== "no action" ? raw : void 0;
|
|
148
|
+
};
|
|
149
|
+
const foreignColumnsObj = this.getSymbol(ref.foreignTable, "drizzle:Columns") ?? {};
|
|
150
|
+
const toForeignTs = this.dbToTsNames(foreignColumnsObj);
|
|
151
|
+
return {
|
|
152
|
+
columns: (ref.columns ?? []).map((c) => toTs(c?.name)),
|
|
153
|
+
foreignTable: this.getSymbol(ref.foreignTable, "drizzle:Name") ?? "unknown",
|
|
154
|
+
foreignColumns: (ref.foreignColumns ?? []).map((c) => toForeignTs(c?.name)),
|
|
155
|
+
onDelete: action(fk?.onDelete, fk?._onDelete),
|
|
156
|
+
onUpdate: action(fk?.onUpdate, fk?._onUpdate),
|
|
157
|
+
name: typeof fk?.getName === "function" ? void 0 : fk?.name
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Render a Drizzle SQL template back into readable text.
|
|
162
|
+
*
|
|
163
|
+
* A `sql` tagged template is stored as alternating chunks: literal fragments holding a
|
|
164
|
+
* string array, and column references. `String()` on that array yields "[object Object]",
|
|
165
|
+
* so a check constraint's expression has to be assembled rather than stringified.
|
|
166
|
+
* Anything unrecognised becomes `?`, which is honest about the gap without inventing SQL.
|
|
167
|
+
*/
|
|
168
|
+
renderSql(value, toTs) {
|
|
169
|
+
const chunks = value?.queryChunks;
|
|
170
|
+
if (!Array.isArray(chunks)) return String(value ?? "");
|
|
171
|
+
return chunks.map((c) => {
|
|
172
|
+
if (Array.isArray(c?.value)) return c.value.join("");
|
|
173
|
+
if (typeof c?.name === "string") return toTs(c.name);
|
|
174
|
+
if (c?.queryChunks) return this.renderSql(c, toTs);
|
|
175
|
+
return "?";
|
|
176
|
+
}).join("").trim();
|
|
177
|
+
}
|
|
178
|
+
/** A value produced by Drizzle's `relations()` helper: a source table plus a callback. */
|
|
179
|
+
isRelationsObject(val) {
|
|
180
|
+
return !!val && typeof val === "object" && typeof val.config === "function" && !!this.getSymbol(val.table, "drizzle:Columns");
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Read the relations declared by `relations(table, ({ one, many }) => ...)`.
|
|
184
|
+
*
|
|
185
|
+
* The previous implementation looked for `val.config.relations`. That property does not
|
|
186
|
+
* exist: `config` is a *function*, so the expression was always undefined and the branch
|
|
187
|
+
* never ran. Nothing failed loudly, relations simply came back empty forever.
|
|
188
|
+
*
|
|
189
|
+
* `config` cannot just be read, it has to be invoked with the builder Drizzle would pass.
|
|
190
|
+
* Rather than depend on drizzle-orm to obtain the real builder, which this package
|
|
191
|
+
* deliberately does not do anywhere else, a stand-in supplies the only two functions a
|
|
192
|
+
* user's callback can call. `relations()` wraps that callback and calls `.withFieldName()`
|
|
193
|
+
* on each returned value, so the stand-in results must carry that method or the call throws.
|
|
194
|
+
*/
|
|
195
|
+
readRelationsObject(val, exportName, issues) {
|
|
196
|
+
const from = this.getSymbol(val.table, "drizzle:Name") ?? exportName;
|
|
197
|
+
const make = (kind) => (table, cfg) => ({
|
|
198
|
+
kind,
|
|
199
|
+
referencedTable: table,
|
|
200
|
+
cfg,
|
|
201
|
+
withFieldName(n) {
|
|
202
|
+
this.fieldName = n;
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
try {
|
|
207
|
+
const built = val.config({ one: make("one"), many: make("many") });
|
|
208
|
+
const out = [];
|
|
209
|
+
for (const rel of Object.values(built ?? {})) {
|
|
210
|
+
const to = this.getSymbol(rel?.referencedTable, "drizzle:Name");
|
|
211
|
+
if (to) out.push({ kind: rel.kind, from, to });
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
} catch (e) {
|
|
215
|
+
issues.push({
|
|
216
|
+
code: "DRZL_ANL_RELATIONS",
|
|
217
|
+
level: "warn",
|
|
218
|
+
message: `Could not read the relations declared in "${exportName}": ${e.message}`,
|
|
219
|
+
hint: "Relations for this table will be missing from the analysis."
|
|
220
|
+
});
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Infer many-to-many links through a join table.
|
|
226
|
+
*
|
|
227
|
+
* A join table is taken to be one whose every column participates in a foreign key, and
|
|
228
|
+
* which points at exactly two distinct tables. Requiring *all* columns to be foreign keys
|
|
229
|
+
* is deliberate: a table carrying its own data is a real entity, not plumbing, and calling
|
|
230
|
+
* it a join table would invent a relation the author never declared.
|
|
231
|
+
*/
|
|
232
|
+
inferManyToMany(tables) {
|
|
233
|
+
const out = [];
|
|
234
|
+
for (const t of tables) {
|
|
235
|
+
const fks = t.foreignKeys ?? [];
|
|
236
|
+
if (fks.length < 2) continue;
|
|
237
|
+
const fkCols = new Set(fks.flatMap((f) => f.columns));
|
|
238
|
+
if (!t.columns.every((c) => fkCols.has(c.name))) continue;
|
|
239
|
+
const targets = [...new Set(fks.map((f) => f.foreignTable))];
|
|
240
|
+
if (targets.length !== 2) continue;
|
|
241
|
+
out.push({ kind: "manyToMany", from: targets[0], to: targets[1], via: t.name });
|
|
242
|
+
out.push({ kind: "manyToMany", from: targets[1], to: targets[0], via: t.name });
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
55
246
|
mapColumnType(column) {
|
|
56
247
|
const ctor = column?.constructor?.name ?? "";
|
|
57
248
|
switch (ctor) {
|
|
@@ -67,14 +258,17 @@ var SchemaAnalyzer = class {
|
|
|
67
258
|
case "SQLiteBlob":
|
|
68
259
|
return { tsType: "Uint8Array", dbType: "BLOB" };
|
|
69
260
|
case "SQLiteNumeric":
|
|
70
|
-
return { tsType: "
|
|
261
|
+
return { tsType: "string", dbType: "NUMERIC" };
|
|
71
262
|
case "SQLiteBoolean":
|
|
72
263
|
return { tsType: "boolean", dbType: "INTEGER" };
|
|
73
264
|
case "PgInteger":
|
|
74
265
|
case "PgSmallInt":
|
|
75
266
|
return { tsType: "number", dbType: "INTEGER" };
|
|
76
267
|
case "PgBigInt":
|
|
77
|
-
return {
|
|
268
|
+
return {
|
|
269
|
+
tsType: column?.config?.mode === "number" ? "number" : "bigint",
|
|
270
|
+
dbType: "BIGINT"
|
|
271
|
+
};
|
|
78
272
|
case "PgSerial":
|
|
79
273
|
case "PgSmallSerial":
|
|
80
274
|
case "PgBigSerial":
|
|
@@ -92,9 +286,10 @@ var SchemaAnalyzer = class {
|
|
|
92
286
|
case "PgDate":
|
|
93
287
|
return { tsType: "Date", dbType: "TIMESTAMP" };
|
|
94
288
|
case "PgNumeric":
|
|
289
|
+
return { tsType: "string", dbType: "NUMERIC" };
|
|
95
290
|
case "PgFloat":
|
|
96
291
|
case "PgDoublePrecision":
|
|
97
|
-
return { tsType: "number", dbType: "
|
|
292
|
+
return { tsType: "number", dbType: "DOUBLE" };
|
|
98
293
|
case "PgJson":
|
|
99
294
|
case "PgJsonb":
|
|
100
295
|
return { tsType: "any", dbType: ctor === "PgJsonb" ? "JSONB" : "JSON" };
|
|
@@ -173,12 +368,13 @@ var SchemaAnalyzer = class {
|
|
|
173
368
|
return { tsType: "unknown", dbType: "UNKNOWN" };
|
|
174
369
|
}
|
|
175
370
|
}
|
|
176
|
-
analyzeTable(tsName, tbl) {
|
|
371
|
+
analyzeTable(tsName, tbl, issues = []) {
|
|
177
372
|
const columnsObj = this.getSymbol(tbl, "drizzle:Columns") ?? {};
|
|
178
373
|
const columns = [];
|
|
179
374
|
const unique = [];
|
|
180
375
|
const indexes = [];
|
|
181
376
|
const checks = [];
|
|
377
|
+
const foreignKeys = [];
|
|
182
378
|
const pkCols = [];
|
|
183
379
|
const uniqueGroups = /* @__PURE__ */ new Map();
|
|
184
380
|
for (const [colName, col] of Object.entries(columnsObj)) {
|
|
@@ -191,13 +387,7 @@ var SchemaAnalyzer = class {
|
|
|
191
387
|
const nullable = !col?.notNull && !col?.config?.notNull;
|
|
192
388
|
const isGenerated = !!(col?.autoIncrement || col?.isGenerated);
|
|
193
389
|
const hasDefault = col?.default !== void 0 || col?.config?.default !== void 0 || isGenerated;
|
|
194
|
-
const
|
|
195
|
-
const references = ref ? {
|
|
196
|
-
table: ref.table ?? "unknown",
|
|
197
|
-
column: ref.column ?? "id",
|
|
198
|
-
onDelete: ref.onDelete,
|
|
199
|
-
onUpdate: ref.onUpdate
|
|
200
|
-
} : void 0;
|
|
390
|
+
const references = void 0;
|
|
201
391
|
const isUnique = !!(col?.isUnique || col?.config?.isUnique);
|
|
202
392
|
const isPk = !!(col?.primary || col?.config?.primaryKey);
|
|
203
393
|
if (isPk) pkCols.push(colName);
|
|
@@ -222,10 +412,11 @@ var SchemaAnalyzer = class {
|
|
|
222
412
|
}
|
|
223
413
|
const name = this.getSymbol(tbl, "drizzle:Name") || tsName;
|
|
224
414
|
const schema = this.getSymbol(tbl, "drizzle:Schema");
|
|
415
|
+
const toTs = this.dbToTsNames(columnsObj);
|
|
225
416
|
try {
|
|
226
|
-
const pkDef =
|
|
417
|
+
const pkDef = this.getSymbol(tbl, "drizzle:PrimaryKey");
|
|
227
418
|
if (pkDef && Array.isArray(pkDef.columns)) {
|
|
228
|
-
const cols = pkDef.columns.map((c) => c?.name
|
|
419
|
+
const cols = pkDef.columns.map((c) => toTs(c?.name)).filter(Boolean);
|
|
229
420
|
if (cols.length) {
|
|
230
421
|
pkCols.splice(0, pkCols.length, ...cols);
|
|
231
422
|
}
|
|
@@ -233,36 +424,50 @@ var SchemaAnalyzer = class {
|
|
|
233
424
|
} catch {
|
|
234
425
|
}
|
|
235
426
|
try {
|
|
236
|
-
const idxDef =
|
|
427
|
+
const idxDef = this.getSymbol(tbl, "drizzle:Indexes");
|
|
237
428
|
if (Array.isArray(idxDef)) {
|
|
238
429
|
for (const i of idxDef) {
|
|
239
|
-
const cols = (i?.columns ?? []).map((c) => c?.name
|
|
430
|
+
const cols = (i?.columns ?? []).map((c) => toTs(c?.name)).filter(Boolean);
|
|
240
431
|
if (cols.length) indexes.push({ columns: cols, name: i?.name });
|
|
241
432
|
if (i?.unique && cols.length) unique.push({ columns: cols });
|
|
242
433
|
}
|
|
243
434
|
}
|
|
244
435
|
} catch {
|
|
245
436
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
for (const [key, val] of entries) {
|
|
252
|
-
const cfg = val?.config ?? val;
|
|
253
|
-
const cols = (cfg?.columns ?? []).map((c) => c?.name ?? String(c)).filter(Boolean);
|
|
254
|
-
const uniqueFlag = !!(cfg?.unique || /unique/i.test(val?.constructor?.name ?? "") || /unique/i.test(key));
|
|
255
|
-
const idxName = cfg?.name ?? val?.name ?? key;
|
|
256
|
-
const expr = cfg?.where || cfg?.expression;
|
|
257
|
-
if (expr && !cols.length) {
|
|
258
|
-
checks.push({ name: idxName, expression: String(expr) });
|
|
259
|
-
} else if (cols.length) {
|
|
260
|
-
indexes.push({ columns: cols, name: idxName });
|
|
261
|
-
if (uniqueFlag) unique.push({ columns: cols });
|
|
262
|
-
}
|
|
263
|
-
}
|
|
437
|
+
for (const entry of this.extraConfigEntries(tbl, issues, name)) {
|
|
438
|
+
if (typeof entry?.reference === "function") {
|
|
439
|
+
const fk = this.readForeignKey(entry, toTs);
|
|
440
|
+
if (fk) foreignKeys.push(fk);
|
|
441
|
+
continue;
|
|
264
442
|
}
|
|
265
|
-
|
|
443
|
+
if (entry?.value?.queryChunks && entry?.name !== void 0) {
|
|
444
|
+
checks.push({ name: entry.name, expression: this.renderSql(entry.value, toTs) });
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const cfg = entry?.config ?? entry ?? {};
|
|
448
|
+
const cols = (cfg.columns ?? []).map((c) => toTs(c?.name)).filter(Boolean);
|
|
449
|
+
if (!cols.length) continue;
|
|
450
|
+
if (cfg.unique === void 0) {
|
|
451
|
+
pkCols.splice(0, pkCols.length, ...cols);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
indexes.push({ columns: cols, name: cfg.name });
|
|
455
|
+
if (cfg.unique) unique.push({ columns: cols, name: cfg.name });
|
|
456
|
+
}
|
|
457
|
+
for (const fk of this.inlineForeignKeys(tbl)) {
|
|
458
|
+
const read = this.readForeignKey(fk, toTs);
|
|
459
|
+
if (read) foreignKeys.push(read);
|
|
460
|
+
}
|
|
461
|
+
for (const fk of foreignKeys) {
|
|
462
|
+
if (fk.columns.length !== 1 || fk.foreignColumns.length !== 1) continue;
|
|
463
|
+
const col = columns.find((c) => c.name === fk.columns[0]);
|
|
464
|
+
if (!col) continue;
|
|
465
|
+
col.references = {
|
|
466
|
+
table: fk.foreignTable,
|
|
467
|
+
column: fk.foreignColumns[0],
|
|
468
|
+
onDelete: fk.onDelete,
|
|
469
|
+
onUpdate: fk.onUpdate
|
|
470
|
+
};
|
|
266
471
|
}
|
|
267
472
|
return {
|
|
268
473
|
name,
|
|
@@ -276,6 +481,7 @@ var SchemaAnalyzer = class {
|
|
|
276
481
|
],
|
|
277
482
|
indexes: [...pkCols.length ? [{ columns: pkCols }] : [], ...indexes],
|
|
278
483
|
checks,
|
|
484
|
+
foreignKeys,
|
|
279
485
|
meta: {}
|
|
280
486
|
};
|
|
281
487
|
}
|
|
@@ -311,39 +517,29 @@ var SchemaAnalyzer = class {
|
|
|
311
517
|
const tables = [];
|
|
312
518
|
const relations = [];
|
|
313
519
|
const enums = [];
|
|
520
|
+
const columnEnums = [];
|
|
314
521
|
for (const [name, val] of Object.entries(exportsObj)) {
|
|
315
522
|
try {
|
|
316
523
|
const cols = this.getSymbol(val, "drizzle:Columns");
|
|
317
524
|
if (cols && typeof cols === "object") {
|
|
318
|
-
const table = this.analyzeTable(name, val);
|
|
525
|
+
const table = this.analyzeTable(name, val, issues);
|
|
319
526
|
tables.push(table);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
kind: "many",
|
|
325
|
-
from: table.name,
|
|
326
|
-
to: col.references.table
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
const enumVals = cols[col.name]?.enumValues;
|
|
330
|
-
if (enumVals && enumVals.length) {
|
|
331
|
-
const enumName = `${table.name}_${col.name}_enum`;
|
|
332
|
-
if (!enums.find((e) => e.name === enumName))
|
|
333
|
-
enums.push({ name: enumName, values: enumVals });
|
|
334
|
-
}
|
|
527
|
+
for (const col of table.columns) {
|
|
528
|
+
const enumVals = cols[col.name]?.enumValues;
|
|
529
|
+
if (enumVals && enumVals.length) {
|
|
530
|
+
columnEnums.push({ name: `${table.name}_${col.name}_enum`, values: enumVals });
|
|
335
531
|
}
|
|
336
532
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const target = entry?.referencedTable;
|
|
342
|
-
const targetName = target ? this.getSymbol(target, "drizzle:Name") : void 0;
|
|
343
|
-
if (targetName) {
|
|
344
|
-
relations.push({ kind: "many", from: base, to: targetName });
|
|
533
|
+
if (opts.includeRelations) {
|
|
534
|
+
for (const fk of table.foreignKeys ?? []) {
|
|
535
|
+
relations.push({ kind: "one", from: table.name, to: fk.foreignTable });
|
|
536
|
+
relations.push({ kind: "many", from: fk.foreignTable, to: table.name });
|
|
345
537
|
}
|
|
346
538
|
}
|
|
539
|
+
} else if (this.isRelationsObject(val)) {
|
|
540
|
+
if (opts.includeRelations) {
|
|
541
|
+
relations.push(...this.readRelationsObject(val, name, issues));
|
|
542
|
+
}
|
|
347
543
|
} else {
|
|
348
544
|
const ev = val?.enumValues;
|
|
349
545
|
if (Array.isArray(ev) && ev.every((x) => typeof x === "string")) {
|
|
@@ -365,6 +561,12 @@ var SchemaAnalyzer = class {
|
|
|
365
561
|
});
|
|
366
562
|
}
|
|
367
563
|
}
|
|
564
|
+
for (const candidate of columnEnums) {
|
|
565
|
+
const key = JSON.stringify(candidate.values);
|
|
566
|
+
if (enums.some((e) => JSON.stringify(e.values) === key)) continue;
|
|
567
|
+
if (enums.some((e) => e.name === candidate.name)) continue;
|
|
568
|
+
enums.push(candidate);
|
|
569
|
+
}
|
|
368
570
|
let dialect = "unknown";
|
|
369
571
|
const ctorNames = /* @__PURE__ */ new Set();
|
|
370
572
|
for (const [_, val] of Object.entries(exportsObj)) {
|
|
@@ -388,6 +590,9 @@ var SchemaAnalyzer = class {
|
|
|
388
590
|
);
|
|
389
591
|
if (looksSqlite) dialect = "sqlite";
|
|
390
592
|
}
|
|
593
|
+
if (opts.includeRelations) {
|
|
594
|
+
relations.push(...this.inferManyToMany(tables));
|
|
595
|
+
}
|
|
391
596
|
if (opts.includeRelations && opts.includeHeuristicRelations) {
|
|
392
597
|
const tableNames = new Set(tables.map((t) => t.name));
|
|
393
598
|
const findTarget = (base) => {
|
|
@@ -398,19 +603,27 @@ var SchemaAnalyzer = class {
|
|
|
398
603
|
};
|
|
399
604
|
for (const t of tables) {
|
|
400
605
|
for (const c of t.columns) {
|
|
606
|
+
if (c.references) continue;
|
|
401
607
|
if (c.name.endsWith("Id")) {
|
|
402
608
|
const base = c.name.slice(0, -2);
|
|
403
609
|
const target = findTarget(base);
|
|
404
|
-
if (target) relations.push({ kind: "
|
|
610
|
+
if (target) relations.push({ kind: "one", from: t.name, to: target });
|
|
405
611
|
}
|
|
406
612
|
}
|
|
407
613
|
}
|
|
408
614
|
}
|
|
615
|
+
const seen = /* @__PURE__ */ new Set();
|
|
616
|
+
const deduped = relations.filter((r) => {
|
|
617
|
+
const key = `${r.kind}|${r.from}|${r.to}|${r.via ?? ""}`;
|
|
618
|
+
if (seen.has(key)) return false;
|
|
619
|
+
seen.add(key);
|
|
620
|
+
return true;
|
|
621
|
+
});
|
|
409
622
|
return {
|
|
410
623
|
dialect,
|
|
411
624
|
tables,
|
|
412
625
|
enums,
|
|
413
|
-
relations,
|
|
626
|
+
relations: deduped,
|
|
414
627
|
issues
|
|
415
628
|
};
|
|
416
629
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -44,6 +44,21 @@ interface Check {
|
|
|
44
44
|
name?: string;
|
|
45
45
|
expression?: string;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* A foreign key as declared, which may span several columns. Single-column keys are also
|
|
49
|
+
* mirrored onto `Column.references` for convenience; composite ones exist only here, because
|
|
50
|
+
* they cannot be attributed to any one column.
|
|
51
|
+
*
|
|
52
|
+
* Column names are TypeScript property names, matching `Column.name`.
|
|
53
|
+
*/
|
|
54
|
+
interface ForeignKey {
|
|
55
|
+
name?: string;
|
|
56
|
+
columns: string[];
|
|
57
|
+
foreignTable: string;
|
|
58
|
+
foreignColumns: string[];
|
|
59
|
+
onDelete?: string;
|
|
60
|
+
onUpdate?: string;
|
|
61
|
+
}
|
|
47
62
|
interface Table {
|
|
48
63
|
name: string;
|
|
49
64
|
tsName: string;
|
|
@@ -53,6 +68,7 @@ interface Table {
|
|
|
53
68
|
unique: Key[];
|
|
54
69
|
indexes: Index[];
|
|
55
70
|
checks?: Check[];
|
|
71
|
+
foreignKeys?: ForeignKey[];
|
|
56
72
|
meta?: Record<string, unknown>;
|
|
57
73
|
}
|
|
58
74
|
interface Enum {
|
|
@@ -76,9 +92,88 @@ declare class SchemaAnalyzer {
|
|
|
76
92
|
private readonly schemaPath;
|
|
77
93
|
constructor(schemaPath: string);
|
|
78
94
|
private getSymbol;
|
|
95
|
+
/**
|
|
96
|
+
* Drizzle keys the Columns object by TypeScript property name, but every other piece of
|
|
97
|
+
* metadata (foreign keys, indexes, composite primary keys) reports the *database* column
|
|
98
|
+
* name. We report the TS name, since that is what a generated schema has to spell, so
|
|
99
|
+
* anything coming from that other metadata has to be translated back.
|
|
100
|
+
*
|
|
101
|
+
* Falls through to the input when a name is unknown, which keeps raw SQL expressions and
|
|
102
|
+
* columns belonging to another table readable rather than dropping them.
|
|
103
|
+
*/
|
|
104
|
+
private dbToTsNames;
|
|
105
|
+
/**
|
|
106
|
+
* Entries from a table's third argument, the extra-config callback.
|
|
107
|
+
*
|
|
108
|
+
* Drizzle invokes this callback with the table's ExtraConfigColumns, NOT with the table.
|
|
109
|
+
* Passing the table throws, and because the whole block used to sit under a bare `catch {}`
|
|
110
|
+
* the throw was swallowed and every index, unique index, composite primary key, check
|
|
111
|
+
* constraint and table-level foreign key silently vanished from the analysis.
|
|
112
|
+
*
|
|
113
|
+
* Both shapes are accepted: modern Drizzle returns an array, older versions an object.
|
|
114
|
+
*/
|
|
115
|
+
private extraConfigEntries;
|
|
116
|
+
/**
|
|
117
|
+
* Foreign keys declared inline with `.references()`.
|
|
118
|
+
*
|
|
119
|
+
* Drizzle stores these per dialect under `drizzle:PgInlineForeignKeys`,
|
|
120
|
+
* `drizzle:MySqlInlineForeignKeys` and `drizzle:SQLiteInlineForeignKeys`. Matching the
|
|
121
|
+
* suffix rather than listing the three keeps new dialects working without a change here.
|
|
122
|
+
* SingleStore has no entry because it does not support foreign keys at all, which is why
|
|
123
|
+
* `.references()` is not even a function there.
|
|
124
|
+
*/
|
|
125
|
+
private inlineForeignKeys;
|
|
126
|
+
/**
|
|
127
|
+
* Normalise one foreign key, inline or table-level, into a common shape.
|
|
128
|
+
*
|
|
129
|
+
* `.reference()` yields the resolved columns on both. The referential actions do not live
|
|
130
|
+
* in the same place: a built ForeignKey exposes `onDelete`/`onUpdate` as strings, while an
|
|
131
|
+
* unbuilt ForeignKeyBuilder exposes them as the chainable setter functions and keeps the
|
|
132
|
+
* values in `_onDelete`/`_onUpdate`. Reading the wrong one yields a function where a string
|
|
133
|
+
* is expected, so check the type rather than the property's presence.
|
|
134
|
+
*
|
|
135
|
+
* Postgres reports 'no action' where MySQL and SQLite report nothing, for identical schemas.
|
|
136
|
+
* Since 'no action' *is* the default, it is normalised away so the same schema analyses the
|
|
137
|
+
* same across dialects.
|
|
138
|
+
*/
|
|
139
|
+
private readForeignKey;
|
|
140
|
+
/**
|
|
141
|
+
* Render a Drizzle SQL template back into readable text.
|
|
142
|
+
*
|
|
143
|
+
* A `sql` tagged template is stored as alternating chunks: literal fragments holding a
|
|
144
|
+
* string array, and column references. `String()` on that array yields "[object Object]",
|
|
145
|
+
* so a check constraint's expression has to be assembled rather than stringified.
|
|
146
|
+
* Anything unrecognised becomes `?`, which is honest about the gap without inventing SQL.
|
|
147
|
+
*/
|
|
148
|
+
private renderSql;
|
|
149
|
+
/** A value produced by Drizzle's `relations()` helper: a source table plus a callback. */
|
|
150
|
+
private isRelationsObject;
|
|
151
|
+
/**
|
|
152
|
+
* Read the relations declared by `relations(table, ({ one, many }) => ...)`.
|
|
153
|
+
*
|
|
154
|
+
* The previous implementation looked for `val.config.relations`. That property does not
|
|
155
|
+
* exist: `config` is a *function*, so the expression was always undefined and the branch
|
|
156
|
+
* never ran. Nothing failed loudly, relations simply came back empty forever.
|
|
157
|
+
*
|
|
158
|
+
* `config` cannot just be read, it has to be invoked with the builder Drizzle would pass.
|
|
159
|
+
* Rather than depend on drizzle-orm to obtain the real builder, which this package
|
|
160
|
+
* deliberately does not do anywhere else, a stand-in supplies the only two functions a
|
|
161
|
+
* user's callback can call. `relations()` wraps that callback and calls `.withFieldName()`
|
|
162
|
+
* on each returned value, so the stand-in results must carry that method or the call throws.
|
|
163
|
+
*/
|
|
164
|
+
private readRelationsObject;
|
|
165
|
+
/**
|
|
166
|
+
* Infer many-to-many links through a join table.
|
|
167
|
+
*
|
|
168
|
+
* A join table is taken to be one whose every column participates in a foreign key, and
|
|
169
|
+
* which points at exactly two distinct tables. Requiring *all* columns to be foreign keys
|
|
170
|
+
* is deliberate: a table carrying its own data is a real entity, not plumbing, and calling
|
|
171
|
+
* it a join table would invent a relation the author never declared.
|
|
172
|
+
*/
|
|
173
|
+
private inferManyToMany;
|
|
79
174
|
private mapColumnType;
|
|
80
175
|
private analyzeTable;
|
|
81
176
|
analyze(opts?: AnalyzeOptions): Promise<Analysis>;
|
|
82
177
|
}
|
|
83
178
|
|
|
84
|
-
export { type Analysis, type AnalyzeOptions, type Check, type Column, type ColumnRef, type Dialect, type Enum, type Index, type Issue, type Key, type Relation, SchemaAnalyzer, type Table, SchemaAnalyzer as default };
|
|
179
|
+
export { type Analysis, type AnalyzeOptions, type Check, type Column, type ColumnRef, type Dialect, type Enum, type ForeignKey, type Index, type Issue, type Key, type Relation, SchemaAnalyzer, type Table, SchemaAnalyzer as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -44,6 +44,21 @@ interface Check {
|
|
|
44
44
|
name?: string;
|
|
45
45
|
expression?: string;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* A foreign key as declared, which may span several columns. Single-column keys are also
|
|
49
|
+
* mirrored onto `Column.references` for convenience; composite ones exist only here, because
|
|
50
|
+
* they cannot be attributed to any one column.
|
|
51
|
+
*
|
|
52
|
+
* Column names are TypeScript property names, matching `Column.name`.
|
|
53
|
+
*/
|
|
54
|
+
interface ForeignKey {
|
|
55
|
+
name?: string;
|
|
56
|
+
columns: string[];
|
|
57
|
+
foreignTable: string;
|
|
58
|
+
foreignColumns: string[];
|
|
59
|
+
onDelete?: string;
|
|
60
|
+
onUpdate?: string;
|
|
61
|
+
}
|
|
47
62
|
interface Table {
|
|
48
63
|
name: string;
|
|
49
64
|
tsName: string;
|
|
@@ -53,6 +68,7 @@ interface Table {
|
|
|
53
68
|
unique: Key[];
|
|
54
69
|
indexes: Index[];
|
|
55
70
|
checks?: Check[];
|
|
71
|
+
foreignKeys?: ForeignKey[];
|
|
56
72
|
meta?: Record<string, unknown>;
|
|
57
73
|
}
|
|
58
74
|
interface Enum {
|
|
@@ -76,9 +92,88 @@ declare class SchemaAnalyzer {
|
|
|
76
92
|
private readonly schemaPath;
|
|
77
93
|
constructor(schemaPath: string);
|
|
78
94
|
private getSymbol;
|
|
95
|
+
/**
|
|
96
|
+
* Drizzle keys the Columns object by TypeScript property name, but every other piece of
|
|
97
|
+
* metadata (foreign keys, indexes, composite primary keys) reports the *database* column
|
|
98
|
+
* name. We report the TS name, since that is what a generated schema has to spell, so
|
|
99
|
+
* anything coming from that other metadata has to be translated back.
|
|
100
|
+
*
|
|
101
|
+
* Falls through to the input when a name is unknown, which keeps raw SQL expressions and
|
|
102
|
+
* columns belonging to another table readable rather than dropping them.
|
|
103
|
+
*/
|
|
104
|
+
private dbToTsNames;
|
|
105
|
+
/**
|
|
106
|
+
* Entries from a table's third argument, the extra-config callback.
|
|
107
|
+
*
|
|
108
|
+
* Drizzle invokes this callback with the table's ExtraConfigColumns, NOT with the table.
|
|
109
|
+
* Passing the table throws, and because the whole block used to sit under a bare `catch {}`
|
|
110
|
+
* the throw was swallowed and every index, unique index, composite primary key, check
|
|
111
|
+
* constraint and table-level foreign key silently vanished from the analysis.
|
|
112
|
+
*
|
|
113
|
+
* Both shapes are accepted: modern Drizzle returns an array, older versions an object.
|
|
114
|
+
*/
|
|
115
|
+
private extraConfigEntries;
|
|
116
|
+
/**
|
|
117
|
+
* Foreign keys declared inline with `.references()`.
|
|
118
|
+
*
|
|
119
|
+
* Drizzle stores these per dialect under `drizzle:PgInlineForeignKeys`,
|
|
120
|
+
* `drizzle:MySqlInlineForeignKeys` and `drizzle:SQLiteInlineForeignKeys`. Matching the
|
|
121
|
+
* suffix rather than listing the three keeps new dialects working without a change here.
|
|
122
|
+
* SingleStore has no entry because it does not support foreign keys at all, which is why
|
|
123
|
+
* `.references()` is not even a function there.
|
|
124
|
+
*/
|
|
125
|
+
private inlineForeignKeys;
|
|
126
|
+
/**
|
|
127
|
+
* Normalise one foreign key, inline or table-level, into a common shape.
|
|
128
|
+
*
|
|
129
|
+
* `.reference()` yields the resolved columns on both. The referential actions do not live
|
|
130
|
+
* in the same place: a built ForeignKey exposes `onDelete`/`onUpdate` as strings, while an
|
|
131
|
+
* unbuilt ForeignKeyBuilder exposes them as the chainable setter functions and keeps the
|
|
132
|
+
* values in `_onDelete`/`_onUpdate`. Reading the wrong one yields a function where a string
|
|
133
|
+
* is expected, so check the type rather than the property's presence.
|
|
134
|
+
*
|
|
135
|
+
* Postgres reports 'no action' where MySQL and SQLite report nothing, for identical schemas.
|
|
136
|
+
* Since 'no action' *is* the default, it is normalised away so the same schema analyses the
|
|
137
|
+
* same across dialects.
|
|
138
|
+
*/
|
|
139
|
+
private readForeignKey;
|
|
140
|
+
/**
|
|
141
|
+
* Render a Drizzle SQL template back into readable text.
|
|
142
|
+
*
|
|
143
|
+
* A `sql` tagged template is stored as alternating chunks: literal fragments holding a
|
|
144
|
+
* string array, and column references. `String()` on that array yields "[object Object]",
|
|
145
|
+
* so a check constraint's expression has to be assembled rather than stringified.
|
|
146
|
+
* Anything unrecognised becomes `?`, which is honest about the gap without inventing SQL.
|
|
147
|
+
*/
|
|
148
|
+
private renderSql;
|
|
149
|
+
/** A value produced by Drizzle's `relations()` helper: a source table plus a callback. */
|
|
150
|
+
private isRelationsObject;
|
|
151
|
+
/**
|
|
152
|
+
* Read the relations declared by `relations(table, ({ one, many }) => ...)`.
|
|
153
|
+
*
|
|
154
|
+
* The previous implementation looked for `val.config.relations`. That property does not
|
|
155
|
+
* exist: `config` is a *function*, so the expression was always undefined and the branch
|
|
156
|
+
* never ran. Nothing failed loudly, relations simply came back empty forever.
|
|
157
|
+
*
|
|
158
|
+
* `config` cannot just be read, it has to be invoked with the builder Drizzle would pass.
|
|
159
|
+
* Rather than depend on drizzle-orm to obtain the real builder, which this package
|
|
160
|
+
* deliberately does not do anywhere else, a stand-in supplies the only two functions a
|
|
161
|
+
* user's callback can call. `relations()` wraps that callback and calls `.withFieldName()`
|
|
162
|
+
* on each returned value, so the stand-in results must carry that method or the call throws.
|
|
163
|
+
*/
|
|
164
|
+
private readRelationsObject;
|
|
165
|
+
/**
|
|
166
|
+
* Infer many-to-many links through a join table.
|
|
167
|
+
*
|
|
168
|
+
* A join table is taken to be one whose every column participates in a foreign key, and
|
|
169
|
+
* which points at exactly two distinct tables. Requiring *all* columns to be foreign keys
|
|
170
|
+
* is deliberate: a table carrying its own data is a real entity, not plumbing, and calling
|
|
171
|
+
* it a join table would invent a relation the author never declared.
|
|
172
|
+
*/
|
|
173
|
+
private inferManyToMany;
|
|
79
174
|
private mapColumnType;
|
|
80
175
|
private analyzeTable;
|
|
81
176
|
analyze(opts?: AnalyzeOptions): Promise<Analysis>;
|
|
82
177
|
}
|
|
83
178
|
|
|
84
|
-
export { type Analysis, type AnalyzeOptions, type Check, type Column, type ColumnRef, type Dialect, type Enum, type Index, type Issue, type Key, type Relation, SchemaAnalyzer, type Table, SchemaAnalyzer as default };
|
|
179
|
+
export { type Analysis, type AnalyzeOptions, type Check, type Column, type ColumnRef, type Dialect, type Enum, type ForeignKey, type Index, type Issue, type Key, type Relation, SchemaAnalyzer, type Table, SchemaAnalyzer as default };
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,197 @@ var SchemaAnalyzer = class {
|
|
|
16
16
|
}
|
|
17
17
|
return table[Symbol.for(key)];
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Drizzle keys the Columns object by TypeScript property name, but every other piece of
|
|
21
|
+
* metadata (foreign keys, indexes, composite primary keys) reports the *database* column
|
|
22
|
+
* name. We report the TS name, since that is what a generated schema has to spell, so
|
|
23
|
+
* anything coming from that other metadata has to be translated back.
|
|
24
|
+
*
|
|
25
|
+
* Falls through to the input when a name is unknown, which keeps raw SQL expressions and
|
|
26
|
+
* columns belonging to another table readable rather than dropping them.
|
|
27
|
+
*/
|
|
28
|
+
dbToTsNames(columnsObj) {
|
|
29
|
+
const map = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const [tsName, col] of Object.entries(columnsObj ?? {})) {
|
|
31
|
+
const dbName = col?.name;
|
|
32
|
+
if (typeof dbName === "string") map.set(dbName, tsName);
|
|
33
|
+
}
|
|
34
|
+
return (dbName) => {
|
|
35
|
+
const raw = typeof dbName === "string" ? dbName : dbName?.name ?? String(dbName);
|
|
36
|
+
return map.get(raw) ?? raw;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Entries from a table's third argument, the extra-config callback.
|
|
41
|
+
*
|
|
42
|
+
* Drizzle invokes this callback with the table's ExtraConfigColumns, NOT with the table.
|
|
43
|
+
* Passing the table throws, and because the whole block used to sit under a bare `catch {}`
|
|
44
|
+
* the throw was swallowed and every index, unique index, composite primary key, check
|
|
45
|
+
* constraint and table-level foreign key silently vanished from the analysis.
|
|
46
|
+
*
|
|
47
|
+
* Both shapes are accepted: modern Drizzle returns an array, older versions an object.
|
|
48
|
+
*/
|
|
49
|
+
extraConfigEntries(tbl, issues, tableName) {
|
|
50
|
+
const builder = this.getSymbol(tbl, "drizzle:ExtraConfigBuilder");
|
|
51
|
+
if (typeof builder !== "function") return [];
|
|
52
|
+
const cols = this.getSymbol(tbl, "drizzle:ExtraConfigColumns") ?? tbl;
|
|
53
|
+
try {
|
|
54
|
+
const built = builder(cols);
|
|
55
|
+
if (!built) return [];
|
|
56
|
+
return Array.isArray(built) ? built : Object.values(built);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
issues.push({
|
|
59
|
+
code: "DRZL_ANL_EXTRACONFIG",
|
|
60
|
+
level: "warn",
|
|
61
|
+
message: `Could not evaluate the extra-config callback for table "${tableName}": ${e.message}`,
|
|
62
|
+
hint: "Indexes, composite keys, checks and table-level foreign keys will be missing for this table."
|
|
63
|
+
});
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Foreign keys declared inline with `.references()`.
|
|
69
|
+
*
|
|
70
|
+
* Drizzle stores these per dialect under `drizzle:PgInlineForeignKeys`,
|
|
71
|
+
* `drizzle:MySqlInlineForeignKeys` and `drizzle:SQLiteInlineForeignKeys`. Matching the
|
|
72
|
+
* suffix rather than listing the three keeps new dialects working without a change here.
|
|
73
|
+
* SingleStore has no entry because it does not support foreign keys at all, which is why
|
|
74
|
+
* `.references()` is not even a function there.
|
|
75
|
+
*/
|
|
76
|
+
inlineForeignKeys(tbl) {
|
|
77
|
+
try {
|
|
78
|
+
for (const s of Object.getOwnPropertySymbols(tbl)) {
|
|
79
|
+
if (/InlineForeignKeys$/.test(s.description ?? "")) {
|
|
80
|
+
const v = tbl[s];
|
|
81
|
+
if (Array.isArray(v)) return v;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Normalise one foreign key, inline or table-level, into a common shape.
|
|
90
|
+
*
|
|
91
|
+
* `.reference()` yields the resolved columns on both. The referential actions do not live
|
|
92
|
+
* in the same place: a built ForeignKey exposes `onDelete`/`onUpdate` as strings, while an
|
|
93
|
+
* unbuilt ForeignKeyBuilder exposes them as the chainable setter functions and keeps the
|
|
94
|
+
* values in `_onDelete`/`_onUpdate`. Reading the wrong one yields a function where a string
|
|
95
|
+
* is expected, so check the type rather than the property's presence.
|
|
96
|
+
*
|
|
97
|
+
* Postgres reports 'no action' where MySQL and SQLite report nothing, for identical schemas.
|
|
98
|
+
* Since 'no action' *is* the default, it is normalised away so the same schema analyses the
|
|
99
|
+
* same across dialects.
|
|
100
|
+
*/
|
|
101
|
+
readForeignKey(fk, toTs) {
|
|
102
|
+
let ref;
|
|
103
|
+
try {
|
|
104
|
+
ref = fk?.reference?.();
|
|
105
|
+
} catch {
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
108
|
+
if (!ref?.foreignTable) return void 0;
|
|
109
|
+
const action = (v, fallback) => {
|
|
110
|
+
const raw = typeof v === "string" ? v : typeof fallback === "string" ? fallback : void 0;
|
|
111
|
+
return raw && raw.toLowerCase() !== "no action" ? raw : void 0;
|
|
112
|
+
};
|
|
113
|
+
const foreignColumnsObj = this.getSymbol(ref.foreignTable, "drizzle:Columns") ?? {};
|
|
114
|
+
const toForeignTs = this.dbToTsNames(foreignColumnsObj);
|
|
115
|
+
return {
|
|
116
|
+
columns: (ref.columns ?? []).map((c) => toTs(c?.name)),
|
|
117
|
+
foreignTable: this.getSymbol(ref.foreignTable, "drizzle:Name") ?? "unknown",
|
|
118
|
+
foreignColumns: (ref.foreignColumns ?? []).map((c) => toForeignTs(c?.name)),
|
|
119
|
+
onDelete: action(fk?.onDelete, fk?._onDelete),
|
|
120
|
+
onUpdate: action(fk?.onUpdate, fk?._onUpdate),
|
|
121
|
+
name: typeof fk?.getName === "function" ? void 0 : fk?.name
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Render a Drizzle SQL template back into readable text.
|
|
126
|
+
*
|
|
127
|
+
* A `sql` tagged template is stored as alternating chunks: literal fragments holding a
|
|
128
|
+
* string array, and column references. `String()` on that array yields "[object Object]",
|
|
129
|
+
* so a check constraint's expression has to be assembled rather than stringified.
|
|
130
|
+
* Anything unrecognised becomes `?`, which is honest about the gap without inventing SQL.
|
|
131
|
+
*/
|
|
132
|
+
renderSql(value, toTs) {
|
|
133
|
+
const chunks = value?.queryChunks;
|
|
134
|
+
if (!Array.isArray(chunks)) return String(value ?? "");
|
|
135
|
+
return chunks.map((c) => {
|
|
136
|
+
if (Array.isArray(c?.value)) return c.value.join("");
|
|
137
|
+
if (typeof c?.name === "string") return toTs(c.name);
|
|
138
|
+
if (c?.queryChunks) return this.renderSql(c, toTs);
|
|
139
|
+
return "?";
|
|
140
|
+
}).join("").trim();
|
|
141
|
+
}
|
|
142
|
+
/** A value produced by Drizzle's `relations()` helper: a source table plus a callback. */
|
|
143
|
+
isRelationsObject(val) {
|
|
144
|
+
return !!val && typeof val === "object" && typeof val.config === "function" && !!this.getSymbol(val.table, "drizzle:Columns");
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Read the relations declared by `relations(table, ({ one, many }) => ...)`.
|
|
148
|
+
*
|
|
149
|
+
* The previous implementation looked for `val.config.relations`. That property does not
|
|
150
|
+
* exist: `config` is a *function*, so the expression was always undefined and the branch
|
|
151
|
+
* never ran. Nothing failed loudly, relations simply came back empty forever.
|
|
152
|
+
*
|
|
153
|
+
* `config` cannot just be read, it has to be invoked with the builder Drizzle would pass.
|
|
154
|
+
* Rather than depend on drizzle-orm to obtain the real builder, which this package
|
|
155
|
+
* deliberately does not do anywhere else, a stand-in supplies the only two functions a
|
|
156
|
+
* user's callback can call. `relations()` wraps that callback and calls `.withFieldName()`
|
|
157
|
+
* on each returned value, so the stand-in results must carry that method or the call throws.
|
|
158
|
+
*/
|
|
159
|
+
readRelationsObject(val, exportName, issues) {
|
|
160
|
+
const from = this.getSymbol(val.table, "drizzle:Name") ?? exportName;
|
|
161
|
+
const make = (kind) => (table, cfg) => ({
|
|
162
|
+
kind,
|
|
163
|
+
referencedTable: table,
|
|
164
|
+
cfg,
|
|
165
|
+
withFieldName(n) {
|
|
166
|
+
this.fieldName = n;
|
|
167
|
+
return this;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
try {
|
|
171
|
+
const built = val.config({ one: make("one"), many: make("many") });
|
|
172
|
+
const out = [];
|
|
173
|
+
for (const rel of Object.values(built ?? {})) {
|
|
174
|
+
const to = this.getSymbol(rel?.referencedTable, "drizzle:Name");
|
|
175
|
+
if (to) out.push({ kind: rel.kind, from, to });
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
} catch (e) {
|
|
179
|
+
issues.push({
|
|
180
|
+
code: "DRZL_ANL_RELATIONS",
|
|
181
|
+
level: "warn",
|
|
182
|
+
message: `Could not read the relations declared in "${exportName}": ${e.message}`,
|
|
183
|
+
hint: "Relations for this table will be missing from the analysis."
|
|
184
|
+
});
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Infer many-to-many links through a join table.
|
|
190
|
+
*
|
|
191
|
+
* A join table is taken to be one whose every column participates in a foreign key, and
|
|
192
|
+
* which points at exactly two distinct tables. Requiring *all* columns to be foreign keys
|
|
193
|
+
* is deliberate: a table carrying its own data is a real entity, not plumbing, and calling
|
|
194
|
+
* it a join table would invent a relation the author never declared.
|
|
195
|
+
*/
|
|
196
|
+
inferManyToMany(tables) {
|
|
197
|
+
const out = [];
|
|
198
|
+
for (const t of tables) {
|
|
199
|
+
const fks = t.foreignKeys ?? [];
|
|
200
|
+
if (fks.length < 2) continue;
|
|
201
|
+
const fkCols = new Set(fks.flatMap((f) => f.columns));
|
|
202
|
+
if (!t.columns.every((c) => fkCols.has(c.name))) continue;
|
|
203
|
+
const targets = [...new Set(fks.map((f) => f.foreignTable))];
|
|
204
|
+
if (targets.length !== 2) continue;
|
|
205
|
+
out.push({ kind: "manyToMany", from: targets[0], to: targets[1], via: t.name });
|
|
206
|
+
out.push({ kind: "manyToMany", from: targets[1], to: targets[0], via: t.name });
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
19
210
|
mapColumnType(column) {
|
|
20
211
|
const ctor = column?.constructor?.name ?? "";
|
|
21
212
|
switch (ctor) {
|
|
@@ -31,14 +222,17 @@ var SchemaAnalyzer = class {
|
|
|
31
222
|
case "SQLiteBlob":
|
|
32
223
|
return { tsType: "Uint8Array", dbType: "BLOB" };
|
|
33
224
|
case "SQLiteNumeric":
|
|
34
|
-
return { tsType: "
|
|
225
|
+
return { tsType: "string", dbType: "NUMERIC" };
|
|
35
226
|
case "SQLiteBoolean":
|
|
36
227
|
return { tsType: "boolean", dbType: "INTEGER" };
|
|
37
228
|
case "PgInteger":
|
|
38
229
|
case "PgSmallInt":
|
|
39
230
|
return { tsType: "number", dbType: "INTEGER" };
|
|
40
231
|
case "PgBigInt":
|
|
41
|
-
return {
|
|
232
|
+
return {
|
|
233
|
+
tsType: column?.config?.mode === "number" ? "number" : "bigint",
|
|
234
|
+
dbType: "BIGINT"
|
|
235
|
+
};
|
|
42
236
|
case "PgSerial":
|
|
43
237
|
case "PgSmallSerial":
|
|
44
238
|
case "PgBigSerial":
|
|
@@ -56,9 +250,10 @@ var SchemaAnalyzer = class {
|
|
|
56
250
|
case "PgDate":
|
|
57
251
|
return { tsType: "Date", dbType: "TIMESTAMP" };
|
|
58
252
|
case "PgNumeric":
|
|
253
|
+
return { tsType: "string", dbType: "NUMERIC" };
|
|
59
254
|
case "PgFloat":
|
|
60
255
|
case "PgDoublePrecision":
|
|
61
|
-
return { tsType: "number", dbType: "
|
|
256
|
+
return { tsType: "number", dbType: "DOUBLE" };
|
|
62
257
|
case "PgJson":
|
|
63
258
|
case "PgJsonb":
|
|
64
259
|
return { tsType: "any", dbType: ctor === "PgJsonb" ? "JSONB" : "JSON" };
|
|
@@ -137,12 +332,13 @@ var SchemaAnalyzer = class {
|
|
|
137
332
|
return { tsType: "unknown", dbType: "UNKNOWN" };
|
|
138
333
|
}
|
|
139
334
|
}
|
|
140
|
-
analyzeTable(tsName, tbl) {
|
|
335
|
+
analyzeTable(tsName, tbl, issues = []) {
|
|
141
336
|
const columnsObj = this.getSymbol(tbl, "drizzle:Columns") ?? {};
|
|
142
337
|
const columns = [];
|
|
143
338
|
const unique = [];
|
|
144
339
|
const indexes = [];
|
|
145
340
|
const checks = [];
|
|
341
|
+
const foreignKeys = [];
|
|
146
342
|
const pkCols = [];
|
|
147
343
|
const uniqueGroups = /* @__PURE__ */ new Map();
|
|
148
344
|
for (const [colName, col] of Object.entries(columnsObj)) {
|
|
@@ -155,13 +351,7 @@ var SchemaAnalyzer = class {
|
|
|
155
351
|
const nullable = !col?.notNull && !col?.config?.notNull;
|
|
156
352
|
const isGenerated = !!(col?.autoIncrement || col?.isGenerated);
|
|
157
353
|
const hasDefault = col?.default !== void 0 || col?.config?.default !== void 0 || isGenerated;
|
|
158
|
-
const
|
|
159
|
-
const references = ref ? {
|
|
160
|
-
table: ref.table ?? "unknown",
|
|
161
|
-
column: ref.column ?? "id",
|
|
162
|
-
onDelete: ref.onDelete,
|
|
163
|
-
onUpdate: ref.onUpdate
|
|
164
|
-
} : void 0;
|
|
354
|
+
const references = void 0;
|
|
165
355
|
const isUnique = !!(col?.isUnique || col?.config?.isUnique);
|
|
166
356
|
const isPk = !!(col?.primary || col?.config?.primaryKey);
|
|
167
357
|
if (isPk) pkCols.push(colName);
|
|
@@ -186,10 +376,11 @@ var SchemaAnalyzer = class {
|
|
|
186
376
|
}
|
|
187
377
|
const name = this.getSymbol(tbl, "drizzle:Name") || tsName;
|
|
188
378
|
const schema = this.getSymbol(tbl, "drizzle:Schema");
|
|
379
|
+
const toTs = this.dbToTsNames(columnsObj);
|
|
189
380
|
try {
|
|
190
|
-
const pkDef =
|
|
381
|
+
const pkDef = this.getSymbol(tbl, "drizzle:PrimaryKey");
|
|
191
382
|
if (pkDef && Array.isArray(pkDef.columns)) {
|
|
192
|
-
const cols = pkDef.columns.map((c) => c?.name
|
|
383
|
+
const cols = pkDef.columns.map((c) => toTs(c?.name)).filter(Boolean);
|
|
193
384
|
if (cols.length) {
|
|
194
385
|
pkCols.splice(0, pkCols.length, ...cols);
|
|
195
386
|
}
|
|
@@ -197,36 +388,50 @@ var SchemaAnalyzer = class {
|
|
|
197
388
|
} catch {
|
|
198
389
|
}
|
|
199
390
|
try {
|
|
200
|
-
const idxDef =
|
|
391
|
+
const idxDef = this.getSymbol(tbl, "drizzle:Indexes");
|
|
201
392
|
if (Array.isArray(idxDef)) {
|
|
202
393
|
for (const i of idxDef) {
|
|
203
|
-
const cols = (i?.columns ?? []).map((c) => c?.name
|
|
394
|
+
const cols = (i?.columns ?? []).map((c) => toTs(c?.name)).filter(Boolean);
|
|
204
395
|
if (cols.length) indexes.push({ columns: cols, name: i?.name });
|
|
205
396
|
if (i?.unique && cols.length) unique.push({ columns: cols });
|
|
206
397
|
}
|
|
207
398
|
}
|
|
208
399
|
} catch {
|
|
209
400
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
for (const [key, val] of entries) {
|
|
216
|
-
const cfg = val?.config ?? val;
|
|
217
|
-
const cols = (cfg?.columns ?? []).map((c) => c?.name ?? String(c)).filter(Boolean);
|
|
218
|
-
const uniqueFlag = !!(cfg?.unique || /unique/i.test(val?.constructor?.name ?? "") || /unique/i.test(key));
|
|
219
|
-
const idxName = cfg?.name ?? val?.name ?? key;
|
|
220
|
-
const expr = cfg?.where || cfg?.expression;
|
|
221
|
-
if (expr && !cols.length) {
|
|
222
|
-
checks.push({ name: idxName, expression: String(expr) });
|
|
223
|
-
} else if (cols.length) {
|
|
224
|
-
indexes.push({ columns: cols, name: idxName });
|
|
225
|
-
if (uniqueFlag) unique.push({ columns: cols });
|
|
226
|
-
}
|
|
227
|
-
}
|
|
401
|
+
for (const entry of this.extraConfigEntries(tbl, issues, name)) {
|
|
402
|
+
if (typeof entry?.reference === "function") {
|
|
403
|
+
const fk = this.readForeignKey(entry, toTs);
|
|
404
|
+
if (fk) foreignKeys.push(fk);
|
|
405
|
+
continue;
|
|
228
406
|
}
|
|
229
|
-
|
|
407
|
+
if (entry?.value?.queryChunks && entry?.name !== void 0) {
|
|
408
|
+
checks.push({ name: entry.name, expression: this.renderSql(entry.value, toTs) });
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const cfg = entry?.config ?? entry ?? {};
|
|
412
|
+
const cols = (cfg.columns ?? []).map((c) => toTs(c?.name)).filter(Boolean);
|
|
413
|
+
if (!cols.length) continue;
|
|
414
|
+
if (cfg.unique === void 0) {
|
|
415
|
+
pkCols.splice(0, pkCols.length, ...cols);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
indexes.push({ columns: cols, name: cfg.name });
|
|
419
|
+
if (cfg.unique) unique.push({ columns: cols, name: cfg.name });
|
|
420
|
+
}
|
|
421
|
+
for (const fk of this.inlineForeignKeys(tbl)) {
|
|
422
|
+
const read = this.readForeignKey(fk, toTs);
|
|
423
|
+
if (read) foreignKeys.push(read);
|
|
424
|
+
}
|
|
425
|
+
for (const fk of foreignKeys) {
|
|
426
|
+
if (fk.columns.length !== 1 || fk.foreignColumns.length !== 1) continue;
|
|
427
|
+
const col = columns.find((c) => c.name === fk.columns[0]);
|
|
428
|
+
if (!col) continue;
|
|
429
|
+
col.references = {
|
|
430
|
+
table: fk.foreignTable,
|
|
431
|
+
column: fk.foreignColumns[0],
|
|
432
|
+
onDelete: fk.onDelete,
|
|
433
|
+
onUpdate: fk.onUpdate
|
|
434
|
+
};
|
|
230
435
|
}
|
|
231
436
|
return {
|
|
232
437
|
name,
|
|
@@ -240,6 +445,7 @@ var SchemaAnalyzer = class {
|
|
|
240
445
|
],
|
|
241
446
|
indexes: [...pkCols.length ? [{ columns: pkCols }] : [], ...indexes],
|
|
242
447
|
checks,
|
|
448
|
+
foreignKeys,
|
|
243
449
|
meta: {}
|
|
244
450
|
};
|
|
245
451
|
}
|
|
@@ -275,39 +481,29 @@ var SchemaAnalyzer = class {
|
|
|
275
481
|
const tables = [];
|
|
276
482
|
const relations = [];
|
|
277
483
|
const enums = [];
|
|
484
|
+
const columnEnums = [];
|
|
278
485
|
for (const [name, val] of Object.entries(exportsObj)) {
|
|
279
486
|
try {
|
|
280
487
|
const cols = this.getSymbol(val, "drizzle:Columns");
|
|
281
488
|
if (cols && typeof cols === "object") {
|
|
282
|
-
const table = this.analyzeTable(name, val);
|
|
489
|
+
const table = this.analyzeTable(name, val, issues);
|
|
283
490
|
tables.push(table);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
kind: "many",
|
|
289
|
-
from: table.name,
|
|
290
|
-
to: col.references.table
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
const enumVals = cols[col.name]?.enumValues;
|
|
294
|
-
if (enumVals && enumVals.length) {
|
|
295
|
-
const enumName = `${table.name}_${col.name}_enum`;
|
|
296
|
-
if (!enums.find((e) => e.name === enumName))
|
|
297
|
-
enums.push({ name: enumName, values: enumVals });
|
|
298
|
-
}
|
|
491
|
+
for (const col of table.columns) {
|
|
492
|
+
const enumVals = cols[col.name]?.enumValues;
|
|
493
|
+
if (enumVals && enumVals.length) {
|
|
494
|
+
columnEnums.push({ name: `${table.name}_${col.name}_enum`, values: enumVals });
|
|
299
495
|
}
|
|
300
496
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const target = entry?.referencedTable;
|
|
306
|
-
const targetName = target ? this.getSymbol(target, "drizzle:Name") : void 0;
|
|
307
|
-
if (targetName) {
|
|
308
|
-
relations.push({ kind: "many", from: base, to: targetName });
|
|
497
|
+
if (opts.includeRelations) {
|
|
498
|
+
for (const fk of table.foreignKeys ?? []) {
|
|
499
|
+
relations.push({ kind: "one", from: table.name, to: fk.foreignTable });
|
|
500
|
+
relations.push({ kind: "many", from: fk.foreignTable, to: table.name });
|
|
309
501
|
}
|
|
310
502
|
}
|
|
503
|
+
} else if (this.isRelationsObject(val)) {
|
|
504
|
+
if (opts.includeRelations) {
|
|
505
|
+
relations.push(...this.readRelationsObject(val, name, issues));
|
|
506
|
+
}
|
|
311
507
|
} else {
|
|
312
508
|
const ev = val?.enumValues;
|
|
313
509
|
if (Array.isArray(ev) && ev.every((x) => typeof x === "string")) {
|
|
@@ -329,6 +525,12 @@ var SchemaAnalyzer = class {
|
|
|
329
525
|
});
|
|
330
526
|
}
|
|
331
527
|
}
|
|
528
|
+
for (const candidate of columnEnums) {
|
|
529
|
+
const key = JSON.stringify(candidate.values);
|
|
530
|
+
if (enums.some((e) => JSON.stringify(e.values) === key)) continue;
|
|
531
|
+
if (enums.some((e) => e.name === candidate.name)) continue;
|
|
532
|
+
enums.push(candidate);
|
|
533
|
+
}
|
|
332
534
|
let dialect = "unknown";
|
|
333
535
|
const ctorNames = /* @__PURE__ */ new Set();
|
|
334
536
|
for (const [_, val] of Object.entries(exportsObj)) {
|
|
@@ -352,6 +554,9 @@ var SchemaAnalyzer = class {
|
|
|
352
554
|
);
|
|
353
555
|
if (looksSqlite) dialect = "sqlite";
|
|
354
556
|
}
|
|
557
|
+
if (opts.includeRelations) {
|
|
558
|
+
relations.push(...this.inferManyToMany(tables));
|
|
559
|
+
}
|
|
355
560
|
if (opts.includeRelations && opts.includeHeuristicRelations) {
|
|
356
561
|
const tableNames = new Set(tables.map((t) => t.name));
|
|
357
562
|
const findTarget = (base) => {
|
|
@@ -362,19 +567,27 @@ var SchemaAnalyzer = class {
|
|
|
362
567
|
};
|
|
363
568
|
for (const t of tables) {
|
|
364
569
|
for (const c of t.columns) {
|
|
570
|
+
if (c.references) continue;
|
|
365
571
|
if (c.name.endsWith("Id")) {
|
|
366
572
|
const base = c.name.slice(0, -2);
|
|
367
573
|
const target = findTarget(base);
|
|
368
|
-
if (target) relations.push({ kind: "
|
|
574
|
+
if (target) relations.push({ kind: "one", from: t.name, to: target });
|
|
369
575
|
}
|
|
370
576
|
}
|
|
371
577
|
}
|
|
372
578
|
}
|
|
579
|
+
const seen = /* @__PURE__ */ new Set();
|
|
580
|
+
const deduped = relations.filter((r) => {
|
|
581
|
+
const key = `${r.kind}|${r.from}|${r.to}|${r.via ?? ""}`;
|
|
582
|
+
if (seen.has(key)) return false;
|
|
583
|
+
seen.add(key);
|
|
584
|
+
return true;
|
|
585
|
+
});
|
|
373
586
|
return {
|
|
374
587
|
dialect,
|
|
375
588
|
tables,
|
|
376
589
|
enums,
|
|
377
|
-
relations,
|
|
590
|
+
relations: deduped,
|
|
378
591
|
issues
|
|
379
592
|
};
|
|
380
593
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drzl/analyzer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"jiti": "^2.5.1"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
+
"drizzle-orm": "^0.45.2",
|
|
24
25
|
"tsup": "^8.5.0",
|
|
25
26
|
"typescript": "^5.9.2"
|
|
26
27
|
},
|