@drzl/analyzer 1.3.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 +266 -57
- package/dist/index.d.cts +96 -1
- package/dist/index.d.ts +96 -1
- package/dist/index.js +266 -57
- 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) {
|
|
@@ -177,12 +368,13 @@ var SchemaAnalyzer = class {
|
|
|
177
368
|
return { tsType: "unknown", dbType: "UNKNOWN" };
|
|
178
369
|
}
|
|
179
370
|
}
|
|
180
|
-
analyzeTable(tsName, tbl) {
|
|
371
|
+
analyzeTable(tsName, tbl, issues = []) {
|
|
181
372
|
const columnsObj = this.getSymbol(tbl, "drizzle:Columns") ?? {};
|
|
182
373
|
const columns = [];
|
|
183
374
|
const unique = [];
|
|
184
375
|
const indexes = [];
|
|
185
376
|
const checks = [];
|
|
377
|
+
const foreignKeys = [];
|
|
186
378
|
const pkCols = [];
|
|
187
379
|
const uniqueGroups = /* @__PURE__ */ new Map();
|
|
188
380
|
for (const [colName, col] of Object.entries(columnsObj)) {
|
|
@@ -195,13 +387,7 @@ var SchemaAnalyzer = class {
|
|
|
195
387
|
const nullable = !col?.notNull && !col?.config?.notNull;
|
|
196
388
|
const isGenerated = !!(col?.autoIncrement || col?.isGenerated);
|
|
197
389
|
const hasDefault = col?.default !== void 0 || col?.config?.default !== void 0 || isGenerated;
|
|
198
|
-
const
|
|
199
|
-
const references = ref ? {
|
|
200
|
-
table: ref.table ?? "unknown",
|
|
201
|
-
column: ref.column ?? "id",
|
|
202
|
-
onDelete: ref.onDelete,
|
|
203
|
-
onUpdate: ref.onUpdate
|
|
204
|
-
} : void 0;
|
|
390
|
+
const references = void 0;
|
|
205
391
|
const isUnique = !!(col?.isUnique || col?.config?.isUnique);
|
|
206
392
|
const isPk = !!(col?.primary || col?.config?.primaryKey);
|
|
207
393
|
if (isPk) pkCols.push(colName);
|
|
@@ -226,10 +412,11 @@ var SchemaAnalyzer = class {
|
|
|
226
412
|
}
|
|
227
413
|
const name = this.getSymbol(tbl, "drizzle:Name") || tsName;
|
|
228
414
|
const schema = this.getSymbol(tbl, "drizzle:Schema");
|
|
415
|
+
const toTs = this.dbToTsNames(columnsObj);
|
|
229
416
|
try {
|
|
230
|
-
const pkDef =
|
|
417
|
+
const pkDef = this.getSymbol(tbl, "drizzle:PrimaryKey");
|
|
231
418
|
if (pkDef && Array.isArray(pkDef.columns)) {
|
|
232
|
-
const cols = pkDef.columns.map((c) => c?.name
|
|
419
|
+
const cols = pkDef.columns.map((c) => toTs(c?.name)).filter(Boolean);
|
|
233
420
|
if (cols.length) {
|
|
234
421
|
pkCols.splice(0, pkCols.length, ...cols);
|
|
235
422
|
}
|
|
@@ -237,36 +424,50 @@ var SchemaAnalyzer = class {
|
|
|
237
424
|
} catch {
|
|
238
425
|
}
|
|
239
426
|
try {
|
|
240
|
-
const idxDef =
|
|
427
|
+
const idxDef = this.getSymbol(tbl, "drizzle:Indexes");
|
|
241
428
|
if (Array.isArray(idxDef)) {
|
|
242
429
|
for (const i of idxDef) {
|
|
243
|
-
const cols = (i?.columns ?? []).map((c) => c?.name
|
|
430
|
+
const cols = (i?.columns ?? []).map((c) => toTs(c?.name)).filter(Boolean);
|
|
244
431
|
if (cols.length) indexes.push({ columns: cols, name: i?.name });
|
|
245
432
|
if (i?.unique && cols.length) unique.push({ columns: cols });
|
|
246
433
|
}
|
|
247
434
|
}
|
|
248
435
|
} catch {
|
|
249
436
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
for (const [key, val] of entries) {
|
|
256
|
-
const cfg = val?.config ?? val;
|
|
257
|
-
const cols = (cfg?.columns ?? []).map((c) => c?.name ?? String(c)).filter(Boolean);
|
|
258
|
-
const uniqueFlag = !!(cfg?.unique || /unique/i.test(val?.constructor?.name ?? "") || /unique/i.test(key));
|
|
259
|
-
const idxName = cfg?.name ?? val?.name ?? key;
|
|
260
|
-
const expr = cfg?.where || cfg?.expression;
|
|
261
|
-
if (expr && !cols.length) {
|
|
262
|
-
checks.push({ name: idxName, expression: String(expr) });
|
|
263
|
-
} else if (cols.length) {
|
|
264
|
-
indexes.push({ columns: cols, name: idxName });
|
|
265
|
-
if (uniqueFlag) unique.push({ columns: cols });
|
|
266
|
-
}
|
|
267
|
-
}
|
|
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;
|
|
268
442
|
}
|
|
269
|
-
|
|
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
|
+
};
|
|
270
471
|
}
|
|
271
472
|
return {
|
|
272
473
|
name,
|
|
@@ -280,6 +481,7 @@ var SchemaAnalyzer = class {
|
|
|
280
481
|
],
|
|
281
482
|
indexes: [...pkCols.length ? [{ columns: pkCols }] : [], ...indexes],
|
|
282
483
|
checks,
|
|
484
|
+
foreignKeys,
|
|
283
485
|
meta: {}
|
|
284
486
|
};
|
|
285
487
|
}
|
|
@@ -315,39 +517,29 @@ var SchemaAnalyzer = class {
|
|
|
315
517
|
const tables = [];
|
|
316
518
|
const relations = [];
|
|
317
519
|
const enums = [];
|
|
520
|
+
const columnEnums = [];
|
|
318
521
|
for (const [name, val] of Object.entries(exportsObj)) {
|
|
319
522
|
try {
|
|
320
523
|
const cols = this.getSymbol(val, "drizzle:Columns");
|
|
321
524
|
if (cols && typeof cols === "object") {
|
|
322
|
-
const table = this.analyzeTable(name, val);
|
|
525
|
+
const table = this.analyzeTable(name, val, issues);
|
|
323
526
|
tables.push(table);
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
kind: "many",
|
|
329
|
-
from: table.name,
|
|
330
|
-
to: col.references.table
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
const enumVals = cols[col.name]?.enumValues;
|
|
334
|
-
if (enumVals && enumVals.length) {
|
|
335
|
-
const enumName = `${table.name}_${col.name}_enum`;
|
|
336
|
-
if (!enums.find((e) => e.name === enumName))
|
|
337
|
-
enums.push({ name: enumName, values: enumVals });
|
|
338
|
-
}
|
|
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 });
|
|
339
531
|
}
|
|
340
532
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const target = entry?.referencedTable;
|
|
346
|
-
const targetName = target ? this.getSymbol(target, "drizzle:Name") : void 0;
|
|
347
|
-
if (targetName) {
|
|
348
|
-
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 });
|
|
349
537
|
}
|
|
350
538
|
}
|
|
539
|
+
} else if (this.isRelationsObject(val)) {
|
|
540
|
+
if (opts.includeRelations) {
|
|
541
|
+
relations.push(...this.readRelationsObject(val, name, issues));
|
|
542
|
+
}
|
|
351
543
|
} else {
|
|
352
544
|
const ev = val?.enumValues;
|
|
353
545
|
if (Array.isArray(ev) && ev.every((x) => typeof x === "string")) {
|
|
@@ -369,6 +561,12 @@ var SchemaAnalyzer = class {
|
|
|
369
561
|
});
|
|
370
562
|
}
|
|
371
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
|
+
}
|
|
372
570
|
let dialect = "unknown";
|
|
373
571
|
const ctorNames = /* @__PURE__ */ new Set();
|
|
374
572
|
for (const [_, val] of Object.entries(exportsObj)) {
|
|
@@ -392,6 +590,9 @@ var SchemaAnalyzer = class {
|
|
|
392
590
|
);
|
|
393
591
|
if (looksSqlite) dialect = "sqlite";
|
|
394
592
|
}
|
|
593
|
+
if (opts.includeRelations) {
|
|
594
|
+
relations.push(...this.inferManyToMany(tables));
|
|
595
|
+
}
|
|
395
596
|
if (opts.includeRelations && opts.includeHeuristicRelations) {
|
|
396
597
|
const tableNames = new Set(tables.map((t) => t.name));
|
|
397
598
|
const findTarget = (base) => {
|
|
@@ -402,19 +603,27 @@ var SchemaAnalyzer = class {
|
|
|
402
603
|
};
|
|
403
604
|
for (const t of tables) {
|
|
404
605
|
for (const c of t.columns) {
|
|
606
|
+
if (c.references) continue;
|
|
405
607
|
if (c.name.endsWith("Id")) {
|
|
406
608
|
const base = c.name.slice(0, -2);
|
|
407
609
|
const target = findTarget(base);
|
|
408
|
-
if (target) relations.push({ kind: "
|
|
610
|
+
if (target) relations.push({ kind: "one", from: t.name, to: target });
|
|
409
611
|
}
|
|
410
612
|
}
|
|
411
613
|
}
|
|
412
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
|
+
});
|
|
413
622
|
return {
|
|
414
623
|
dialect,
|
|
415
624
|
tables,
|
|
416
625
|
enums,
|
|
417
|
-
relations,
|
|
626
|
+
relations: deduped,
|
|
418
627
|
issues
|
|
419
628
|
};
|
|
420
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) {
|
|
@@ -141,12 +332,13 @@ var SchemaAnalyzer = class {
|
|
|
141
332
|
return { tsType: "unknown", dbType: "UNKNOWN" };
|
|
142
333
|
}
|
|
143
334
|
}
|
|
144
|
-
analyzeTable(tsName, tbl) {
|
|
335
|
+
analyzeTable(tsName, tbl, issues = []) {
|
|
145
336
|
const columnsObj = this.getSymbol(tbl, "drizzle:Columns") ?? {};
|
|
146
337
|
const columns = [];
|
|
147
338
|
const unique = [];
|
|
148
339
|
const indexes = [];
|
|
149
340
|
const checks = [];
|
|
341
|
+
const foreignKeys = [];
|
|
150
342
|
const pkCols = [];
|
|
151
343
|
const uniqueGroups = /* @__PURE__ */ new Map();
|
|
152
344
|
for (const [colName, col] of Object.entries(columnsObj)) {
|
|
@@ -159,13 +351,7 @@ var SchemaAnalyzer = class {
|
|
|
159
351
|
const nullable = !col?.notNull && !col?.config?.notNull;
|
|
160
352
|
const isGenerated = !!(col?.autoIncrement || col?.isGenerated);
|
|
161
353
|
const hasDefault = col?.default !== void 0 || col?.config?.default !== void 0 || isGenerated;
|
|
162
|
-
const
|
|
163
|
-
const references = ref ? {
|
|
164
|
-
table: ref.table ?? "unknown",
|
|
165
|
-
column: ref.column ?? "id",
|
|
166
|
-
onDelete: ref.onDelete,
|
|
167
|
-
onUpdate: ref.onUpdate
|
|
168
|
-
} : void 0;
|
|
354
|
+
const references = void 0;
|
|
169
355
|
const isUnique = !!(col?.isUnique || col?.config?.isUnique);
|
|
170
356
|
const isPk = !!(col?.primary || col?.config?.primaryKey);
|
|
171
357
|
if (isPk) pkCols.push(colName);
|
|
@@ -190,10 +376,11 @@ var SchemaAnalyzer = class {
|
|
|
190
376
|
}
|
|
191
377
|
const name = this.getSymbol(tbl, "drizzle:Name") || tsName;
|
|
192
378
|
const schema = this.getSymbol(tbl, "drizzle:Schema");
|
|
379
|
+
const toTs = this.dbToTsNames(columnsObj);
|
|
193
380
|
try {
|
|
194
|
-
const pkDef =
|
|
381
|
+
const pkDef = this.getSymbol(tbl, "drizzle:PrimaryKey");
|
|
195
382
|
if (pkDef && Array.isArray(pkDef.columns)) {
|
|
196
|
-
const cols = pkDef.columns.map((c) => c?.name
|
|
383
|
+
const cols = pkDef.columns.map((c) => toTs(c?.name)).filter(Boolean);
|
|
197
384
|
if (cols.length) {
|
|
198
385
|
pkCols.splice(0, pkCols.length, ...cols);
|
|
199
386
|
}
|
|
@@ -201,36 +388,50 @@ var SchemaAnalyzer = class {
|
|
|
201
388
|
} catch {
|
|
202
389
|
}
|
|
203
390
|
try {
|
|
204
|
-
const idxDef =
|
|
391
|
+
const idxDef = this.getSymbol(tbl, "drizzle:Indexes");
|
|
205
392
|
if (Array.isArray(idxDef)) {
|
|
206
393
|
for (const i of idxDef) {
|
|
207
|
-
const cols = (i?.columns ?? []).map((c) => c?.name
|
|
394
|
+
const cols = (i?.columns ?? []).map((c) => toTs(c?.name)).filter(Boolean);
|
|
208
395
|
if (cols.length) indexes.push({ columns: cols, name: i?.name });
|
|
209
396
|
if (i?.unique && cols.length) unique.push({ columns: cols });
|
|
210
397
|
}
|
|
211
398
|
}
|
|
212
399
|
} catch {
|
|
213
400
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
for (const [key, val] of entries) {
|
|
220
|
-
const cfg = val?.config ?? val;
|
|
221
|
-
const cols = (cfg?.columns ?? []).map((c) => c?.name ?? String(c)).filter(Boolean);
|
|
222
|
-
const uniqueFlag = !!(cfg?.unique || /unique/i.test(val?.constructor?.name ?? "") || /unique/i.test(key));
|
|
223
|
-
const idxName = cfg?.name ?? val?.name ?? key;
|
|
224
|
-
const expr = cfg?.where || cfg?.expression;
|
|
225
|
-
if (expr && !cols.length) {
|
|
226
|
-
checks.push({ name: idxName, expression: String(expr) });
|
|
227
|
-
} else if (cols.length) {
|
|
228
|
-
indexes.push({ columns: cols, name: idxName });
|
|
229
|
-
if (uniqueFlag) unique.push({ columns: cols });
|
|
230
|
-
}
|
|
231
|
-
}
|
|
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;
|
|
232
406
|
}
|
|
233
|
-
|
|
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
|
+
};
|
|
234
435
|
}
|
|
235
436
|
return {
|
|
236
437
|
name,
|
|
@@ -244,6 +445,7 @@ var SchemaAnalyzer = class {
|
|
|
244
445
|
],
|
|
245
446
|
indexes: [...pkCols.length ? [{ columns: pkCols }] : [], ...indexes],
|
|
246
447
|
checks,
|
|
448
|
+
foreignKeys,
|
|
247
449
|
meta: {}
|
|
248
450
|
};
|
|
249
451
|
}
|
|
@@ -279,39 +481,29 @@ var SchemaAnalyzer = class {
|
|
|
279
481
|
const tables = [];
|
|
280
482
|
const relations = [];
|
|
281
483
|
const enums = [];
|
|
484
|
+
const columnEnums = [];
|
|
282
485
|
for (const [name, val] of Object.entries(exportsObj)) {
|
|
283
486
|
try {
|
|
284
487
|
const cols = this.getSymbol(val, "drizzle:Columns");
|
|
285
488
|
if (cols && typeof cols === "object") {
|
|
286
|
-
const table = this.analyzeTable(name, val);
|
|
489
|
+
const table = this.analyzeTable(name, val, issues);
|
|
287
490
|
tables.push(table);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
kind: "many",
|
|
293
|
-
from: table.name,
|
|
294
|
-
to: col.references.table
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
const enumVals = cols[col.name]?.enumValues;
|
|
298
|
-
if (enumVals && enumVals.length) {
|
|
299
|
-
const enumName = `${table.name}_${col.name}_enum`;
|
|
300
|
-
if (!enums.find((e) => e.name === enumName))
|
|
301
|
-
enums.push({ name: enumName, values: enumVals });
|
|
302
|
-
}
|
|
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 });
|
|
303
495
|
}
|
|
304
496
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const target = entry?.referencedTable;
|
|
310
|
-
const targetName = target ? this.getSymbol(target, "drizzle:Name") : void 0;
|
|
311
|
-
if (targetName) {
|
|
312
|
-
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 });
|
|
313
501
|
}
|
|
314
502
|
}
|
|
503
|
+
} else if (this.isRelationsObject(val)) {
|
|
504
|
+
if (opts.includeRelations) {
|
|
505
|
+
relations.push(...this.readRelationsObject(val, name, issues));
|
|
506
|
+
}
|
|
315
507
|
} else {
|
|
316
508
|
const ev = val?.enumValues;
|
|
317
509
|
if (Array.isArray(ev) && ev.every((x) => typeof x === "string")) {
|
|
@@ -333,6 +525,12 @@ var SchemaAnalyzer = class {
|
|
|
333
525
|
});
|
|
334
526
|
}
|
|
335
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
|
+
}
|
|
336
534
|
let dialect = "unknown";
|
|
337
535
|
const ctorNames = /* @__PURE__ */ new Set();
|
|
338
536
|
for (const [_, val] of Object.entries(exportsObj)) {
|
|
@@ -356,6 +554,9 @@ var SchemaAnalyzer = class {
|
|
|
356
554
|
);
|
|
357
555
|
if (looksSqlite) dialect = "sqlite";
|
|
358
556
|
}
|
|
557
|
+
if (opts.includeRelations) {
|
|
558
|
+
relations.push(...this.inferManyToMany(tables));
|
|
559
|
+
}
|
|
359
560
|
if (opts.includeRelations && opts.includeHeuristicRelations) {
|
|
360
561
|
const tableNames = new Set(tables.map((t) => t.name));
|
|
361
562
|
const findTarget = (base) => {
|
|
@@ -366,19 +567,27 @@ var SchemaAnalyzer = class {
|
|
|
366
567
|
};
|
|
367
568
|
for (const t of tables) {
|
|
368
569
|
for (const c of t.columns) {
|
|
570
|
+
if (c.references) continue;
|
|
369
571
|
if (c.name.endsWith("Id")) {
|
|
370
572
|
const base = c.name.slice(0, -2);
|
|
371
573
|
const target = findTarget(base);
|
|
372
|
-
if (target) relations.push({ kind: "
|
|
574
|
+
if (target) relations.push({ kind: "one", from: t.name, to: target });
|
|
373
575
|
}
|
|
374
576
|
}
|
|
375
577
|
}
|
|
376
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
|
+
});
|
|
377
586
|
return {
|
|
378
587
|
dialect,
|
|
379
588
|
tables,
|
|
380
589
|
enums,
|
|
381
|
-
relations,
|
|
590
|
+
relations: deduped,
|
|
382
591
|
issues
|
|
383
592
|
};
|
|
384
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
|
},
|