mikro-orm-markdown 0.1.0-alpha.2 → 0.1.0-alpha.4
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/CHANGELOG.md +66 -0
- package/CODE_OF_CONDUCT.md +25 -0
- package/LICENSE +1 -1
- package/README.ko.md +248 -41
- package/README.md +248 -41
- package/SECURITY.md +28 -0
- package/dist/cli.cjs +729 -136
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +51 -0
- package/dist/cli.d.ts +51 -0
- package/dist/cli.js +709 -138
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +559 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -17
- package/dist/index.d.ts +21 -17
- package/dist/index.js +548 -121
- package/dist/index.js.map +1 -1
- package/package.json +30 -6
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
+
import * as syncFs from "fs";
|
|
4
5
|
import * as fs from "fs/promises";
|
|
5
|
-
import * as
|
|
6
|
+
import * as path2 from "path";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
6
8
|
import { Command } from "commander";
|
|
7
9
|
|
|
8
10
|
// src/docs/jsdoc.ts
|
|
9
11
|
import { Project } from "ts-morph";
|
|
10
|
-
function loadJsDoc(
|
|
12
|
+
function loadJsDoc(filePaths) {
|
|
11
13
|
const entities = /* @__PURE__ */ new Map();
|
|
12
14
|
const props = /* @__PURE__ */ new Map();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
const classNames = /* @__PURE__ */ new Set();
|
|
16
|
+
if (filePaths.length === 0) {
|
|
17
|
+
return { entities, props, sourceFileCount: 0, classNames };
|
|
15
18
|
}
|
|
16
19
|
const project = new Project({
|
|
17
20
|
skipAddingFilesFromTsConfig: true,
|
|
@@ -21,34 +24,44 @@ function loadJsDoc(srcGlobs) {
|
|
|
21
24
|
skipLibCheck: true
|
|
22
25
|
}
|
|
23
26
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
for (const prop of cls.getProperties()) {
|
|
37
|
-
const propDocs = prop.getJsDocs();
|
|
38
|
-
if (propDocs.length === 0) {
|
|
27
|
+
for (const filePath of filePaths) {
|
|
28
|
+
try {
|
|
29
|
+
project.addSourceFilesAtPaths(filePath);
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const sourceFiles = project.getSourceFiles();
|
|
34
|
+
for (const sourceFile of sourceFiles) {
|
|
35
|
+
try {
|
|
36
|
+
for (const cls of sourceFile.getClasses()) {
|
|
37
|
+
const className = cls.getName();
|
|
38
|
+
if (!className) {
|
|
39
39
|
continue;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
classNames.add(className);
|
|
42
|
+
const classDocs = cls.getJsDocs();
|
|
43
|
+
if (classDocs.length > 0) {
|
|
44
|
+
entities.set(className, parseEntityJsDoc(classDocs));
|
|
45
|
+
}
|
|
46
|
+
const propMap = /* @__PURE__ */ new Map();
|
|
47
|
+
for (const prop of cls.getProperties()) {
|
|
48
|
+
const propDocs = prop.getJsDocs();
|
|
49
|
+
if (propDocs.length === 0) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const info = parsePropJsDoc(propDocs);
|
|
53
|
+
if (info.description !== void 0 || info.atLeastOne) {
|
|
54
|
+
propMap.set(prop.getName(), info);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (propMap.size > 0) {
|
|
58
|
+
props.set(className, propMap);
|
|
44
59
|
}
|
|
45
60
|
}
|
|
46
|
-
|
|
47
|
-
props.set(className, propMap);
|
|
48
|
-
}
|
|
61
|
+
} catch {
|
|
49
62
|
}
|
|
50
63
|
}
|
|
51
|
-
return { entities, props };
|
|
64
|
+
return { entities, props, sourceFileCount: sourceFiles.length, classNames };
|
|
52
65
|
}
|
|
53
66
|
function parseEntityJsDoc(jsDocs) {
|
|
54
67
|
const namespaces = [];
|
|
@@ -83,18 +96,26 @@ function parseEntityJsDoc(jsDocs) {
|
|
|
83
96
|
hidden
|
|
84
97
|
};
|
|
85
98
|
}
|
|
86
|
-
function
|
|
99
|
+
function parsePropJsDoc(jsDocs) {
|
|
100
|
+
let description;
|
|
101
|
+
let atLeastOne = false;
|
|
87
102
|
for (const doc of jsDocs) {
|
|
88
103
|
const desc = doc.getDescription().trim();
|
|
89
|
-
if (desc) {
|
|
90
|
-
|
|
104
|
+
if (desc && description === void 0) {
|
|
105
|
+
description = desc;
|
|
106
|
+
}
|
|
107
|
+
for (const tag of doc.getTags()) {
|
|
108
|
+
if (tag.getTagName() === "atLeastOne") {
|
|
109
|
+
atLeastOne = true;
|
|
110
|
+
}
|
|
91
111
|
}
|
|
92
112
|
}
|
|
93
|
-
return void 0;
|
|
113
|
+
return { ...description !== void 0 && { description }, atLeastOne };
|
|
94
114
|
}
|
|
95
115
|
|
|
96
116
|
// src/metadata/load.ts
|
|
97
|
-
import
|
|
117
|
+
import * as path from "path";
|
|
118
|
+
import { EntitySchema, MikroORM } from "@mikro-orm/core";
|
|
98
119
|
var MetadataLoadError = class extends Error {
|
|
99
120
|
constructor(message, cause) {
|
|
100
121
|
super(message);
|
|
@@ -102,13 +123,46 @@ var MetadataLoadError = class extends Error {
|
|
|
102
123
|
this.name = "MetadataLoadError";
|
|
103
124
|
}
|
|
104
125
|
};
|
|
126
|
+
async function closeDiscoveryResources(orm) {
|
|
127
|
+
await orm.config.getMetadataCacheAdapter()?.close?.();
|
|
128
|
+
await orm.config.getResultCacheAdapter()?.close?.();
|
|
129
|
+
}
|
|
130
|
+
function collectEntitySchemaNames(options) {
|
|
131
|
+
const configuredEntities = [...options.entities ?? [], ...options.entitiesTs ?? []];
|
|
132
|
+
const names = [];
|
|
133
|
+
for (const entity of configuredEntities) {
|
|
134
|
+
if (entity instanceof EntitySchema) {
|
|
135
|
+
names.push(entity.meta.className);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (entity !== null && typeof entity === "object" && "schema" in entity && entity.schema instanceof EntitySchema) {
|
|
139
|
+
names.push(entity.schema.meta.className);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return names;
|
|
143
|
+
}
|
|
144
|
+
function assertNoEntitySchemaEntities(options) {
|
|
145
|
+
const schemaNames = collectEntitySchemaNames(options);
|
|
146
|
+
if (schemaNames.length === 0) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
throw new MetadataLoadError(
|
|
150
|
+
`EntitySchema-defined entities are not currently supported: ${schemaNames.join(", ")}.
|
|
151
|
+
Use decorator-based @Entity() classes instead.`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
105
154
|
async function loadEntityMetadata(options) {
|
|
155
|
+
assertNoEntitySchemaEntities(options);
|
|
106
156
|
let orm;
|
|
107
157
|
try {
|
|
108
158
|
orm = await MikroORM.init({
|
|
109
159
|
...options,
|
|
110
160
|
debug: false,
|
|
111
|
-
connect: false
|
|
161
|
+
connect: false,
|
|
162
|
+
// Always disable the metadata cache for one-shot doc runs so the project
|
|
163
|
+
// is never littered with a temp/ folder, regardless of how metadataProvider
|
|
164
|
+
// was configured.
|
|
165
|
+
metadataCache: { ...options.metadataCache, enabled: false }
|
|
112
166
|
});
|
|
113
167
|
} catch (cause) {
|
|
114
168
|
throw new MetadataLoadError(
|
|
@@ -116,23 +170,80 @@ async function loadEntityMetadata(options) {
|
|
|
116
170
|
cause
|
|
117
171
|
);
|
|
118
172
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
173
|
+
try {
|
|
174
|
+
const all = Object.values(orm.getMetadata().getAll());
|
|
175
|
+
if (all.length === 0) {
|
|
176
|
+
throw new MetadataLoadError(
|
|
177
|
+
"No entities were discovered. Check that your config specifies at least one entity path or class."
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const baseDir = orm.config.get("baseDir");
|
|
181
|
+
const sourcePaths = [...new Set(all.filter((m) => m.path).map((m) => path.resolve(baseDir, m.path)))];
|
|
182
|
+
return { metas: all, sourcePaths };
|
|
183
|
+
} finally {
|
|
184
|
+
await closeDiscoveryResources(orm);
|
|
124
185
|
}
|
|
125
|
-
return all;
|
|
126
186
|
}
|
|
127
187
|
|
|
188
|
+
// src/model/build.ts
|
|
189
|
+
import { ReferenceKind as ReferenceKind2 } from "@mikro-orm/core";
|
|
190
|
+
|
|
128
191
|
// src/render/mermaid.ts
|
|
129
192
|
import { ReferenceKind } from "@mikro-orm/core";
|
|
193
|
+
|
|
194
|
+
// src/render/escape.ts
|
|
195
|
+
var MARKDOWN_INLINE_SPECIAL_CHARS = /[\\`|*#]/g;
|
|
196
|
+
var MERMAID_IDENTIFIER_INVALID_CHARS = /[^a-zA-Z0-9_]/g;
|
|
197
|
+
function normalizeInlineText(value) {
|
|
198
|
+
return value.replace(/\r\n?/g, "\n").replace(/[\n\t]+/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
199
|
+
}
|
|
200
|
+
function splitNormalizedLines(value) {
|
|
201
|
+
return value.replace(/\r\n?/g, "\n").split("\n");
|
|
202
|
+
}
|
|
203
|
+
function escapeHtmlText(value) {
|
|
204
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
205
|
+
}
|
|
206
|
+
function escapeMarkdownInline(value) {
|
|
207
|
+
return escapeHtmlText(normalizeInlineText(value)).replace(MARKDOWN_INLINE_SPECIAL_CHARS, "\\$&");
|
|
208
|
+
}
|
|
209
|
+
function escapeMarkdownTableCell(value) {
|
|
210
|
+
return splitNormalizedLines(value).map((line) => escapeMarkdownInline(line)).join("<br>");
|
|
211
|
+
}
|
|
212
|
+
function escapeMarkdownParagraph(value) {
|
|
213
|
+
return splitNormalizedLines(value).map((line) => escapeMarkdownInline(line)).join(" \n");
|
|
214
|
+
}
|
|
215
|
+
function renderMarkdownBlockQuote(value) {
|
|
216
|
+
return splitNormalizedLines(value).map((line) => `> ${escapeMarkdownInline(line)}`).join("\n");
|
|
217
|
+
}
|
|
218
|
+
function renderMarkdownInlineCode(value) {
|
|
219
|
+
const normalized = normalizeInlineText(value);
|
|
220
|
+
const backtickRuns = normalized.match(/`+/g) ?? [];
|
|
221
|
+
const longestRun = Math.max(0, ...backtickRuns.map((run2) => run2.length));
|
|
222
|
+
const fence = "`".repeat(longestRun + 1);
|
|
223
|
+
const needsPadding = normalized.startsWith("`") || normalized.endsWith("`");
|
|
224
|
+
const content = needsPadding ? ` ${normalized} ` : normalized;
|
|
225
|
+
return `${fence}${content}${fence}`;
|
|
226
|
+
}
|
|
227
|
+
function toMermaidIdentifier(value) {
|
|
228
|
+
const normalized = normalizeInlineText(value).replace(MERMAID_IDENTIFIER_INVALID_CHARS, "_").replace(/_+/g, "_");
|
|
229
|
+
const identifier = normalized === "" ? "_" : normalized;
|
|
230
|
+
return /^[a-zA-Z_]/.test(identifier) ? identifier : `_${identifier}`;
|
|
231
|
+
}
|
|
232
|
+
function escapeMermaidQuotedText(value) {
|
|
233
|
+
return normalizeInlineText(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
234
|
+
}
|
|
235
|
+
function toMarkdownAnchor(value) {
|
|
236
|
+
return normalizeInlineText(value).toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu, "").replace(/\s+/g, "-");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/render/mermaid.ts
|
|
130
240
|
var FORMULA_DUMMY_TABLE = {
|
|
131
241
|
alias: "e0",
|
|
132
242
|
name: "",
|
|
133
243
|
qualifiedName: "",
|
|
134
244
|
toString: () => "e0"
|
|
135
245
|
};
|
|
246
|
+
var UNRESOLVED_FORMULA = "<unresolved>";
|
|
136
247
|
function buildDiagramModel(metas) {
|
|
137
248
|
const metaByClass = new Map(metas.map((m) => [m.className, m]));
|
|
138
249
|
const entities = metas.filter((meta) => !meta.pivotTable && !meta.embeddable).map((meta) => buildEntityModel(meta, metaByClass));
|
|
@@ -140,17 +251,14 @@ function buildDiagramModel(metas) {
|
|
|
140
251
|
return { entities, relations };
|
|
141
252
|
}
|
|
142
253
|
function buildEntityModel(meta, metaByClass) {
|
|
143
|
-
const isStiRoot = meta.discriminatorColumn !== void 0 && !meta.
|
|
254
|
+
const isStiRoot = meta.discriminatorColumn !== void 0 && !meta.extends;
|
|
144
255
|
const isStiChild = Boolean(meta.extends) && meta.discriminatorValue !== void 0;
|
|
145
256
|
const columns = [];
|
|
146
257
|
for (const prop of Object.values(meta.properties)) {
|
|
147
258
|
if (isStiRoot && prop.inherited === true) {
|
|
148
259
|
continue;
|
|
149
260
|
}
|
|
150
|
-
|
|
151
|
-
if (col !== null) {
|
|
152
|
-
columns.push(col);
|
|
153
|
-
}
|
|
261
|
+
columns.push(...buildColumns(prop, metaByClass, meta));
|
|
154
262
|
}
|
|
155
263
|
return {
|
|
156
264
|
className: meta.className,
|
|
@@ -160,47 +268,73 @@ function buildEntityModel(meta, metaByClass) {
|
|
|
160
268
|
isEmbeddable: meta.embeddable === true,
|
|
161
269
|
...isStiRoot && { discriminatorColumn: meta.discriminatorColumn },
|
|
162
270
|
...isStiChild && { extendsEntity: meta.extends },
|
|
271
|
+
...isStiChild && meta.discriminatorValue !== void 0 && { discriminatorValue: String(meta.discriminatorValue) },
|
|
163
272
|
constraints: buildConstraints(meta)
|
|
164
273
|
};
|
|
165
274
|
}
|
|
166
|
-
function
|
|
275
|
+
function buildColumns(prop, metaByClass, owningMeta) {
|
|
167
276
|
if (prop.kind === ReferenceKind.EMBEDDED) {
|
|
168
|
-
|
|
277
|
+
if (prop.object === true || prop.array === true) {
|
|
278
|
+
return [
|
|
279
|
+
{
|
|
280
|
+
propName: prop.name,
|
|
281
|
+
fieldName: prop.fieldNames?.[0] ?? prop.name,
|
|
282
|
+
type: "json",
|
|
283
|
+
isPrimary: false,
|
|
284
|
+
isForeignKey: false,
|
|
285
|
+
isUnique: prop.unique === true,
|
|
286
|
+
isNullable: prop.nullable === true,
|
|
287
|
+
...prop.comment !== void 0 && { comment: prop.comment },
|
|
288
|
+
embeddedIn: prop.array === true ? `${prop.type}[]` : prop.type
|
|
289
|
+
}
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
return [];
|
|
169
293
|
}
|
|
170
294
|
if (prop.kind === ReferenceKind.SCALAR) {
|
|
295
|
+
if (prop.object === true && prop.embedded !== void 0) {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
171
298
|
const formulaExpr = prop.formula !== void 0 ? resolveFormulaExpr(prop.formula) : void 0;
|
|
172
299
|
let embeddedIn;
|
|
300
|
+
let embeddedPropName;
|
|
173
301
|
if (prop.embedded !== void 0) {
|
|
174
302
|
const parentPropName = prop.embedded[0];
|
|
175
303
|
embeddedIn = owningMeta.properties[parentPropName]?.type;
|
|
304
|
+
embeddedPropName = prop.embedded[1];
|
|
176
305
|
}
|
|
177
306
|
const isDiscriminator = owningMeta.discriminatorColumn !== void 0 && prop.name === owningMeta.discriminatorColumn;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
307
|
+
const enumItems = prop.enum === true && Array.isArray(prop.items) && prop.items.length > 0 ? prop.items.map((item) => String(item)) : void 0;
|
|
308
|
+
return [
|
|
309
|
+
{
|
|
310
|
+
propName: prop.name,
|
|
311
|
+
fieldName: prop.fieldNames?.[0] ?? prop.name,
|
|
312
|
+
// Store the original type (e.g. `varchar(255)`); the Mermaid renderer
|
|
313
|
+
// sanitizes it for diagram identifiers, while the markdown table shows
|
|
314
|
+
// it verbatim. Guard against a missing type so downstream string
|
|
315
|
+
// handling never sees undefined (matches the FK path's defaulting).
|
|
316
|
+
type: prop.type ?? "unknown",
|
|
317
|
+
isPrimary: prop.primary === true,
|
|
318
|
+
isForeignKey: false,
|
|
319
|
+
isUnique: prop.unique === true,
|
|
320
|
+
isNullable: prop.nullable === true,
|
|
321
|
+
...prop.comment !== void 0 && { comment: prop.comment },
|
|
322
|
+
...formulaExpr !== void 0 && { formula: formulaExpr },
|
|
323
|
+
...embeddedIn !== void 0 && { embeddedIn },
|
|
324
|
+
...embeddedPropName !== void 0 && { embeddedPropName },
|
|
325
|
+
...isDiscriminator && { isDiscriminator: true },
|
|
326
|
+
...enumItems !== void 0 && { enumItems }
|
|
327
|
+
}
|
|
328
|
+
];
|
|
190
329
|
}
|
|
191
330
|
if (prop.kind === ReferenceKind.MANY_TO_ONE || prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner === true) {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
isPrimary: false,
|
|
198
|
-
isForeignKey: true,
|
|
199
|
-
isUnique: prop.unique === true,
|
|
200
|
-
isNullable: prop.nullable === true
|
|
201
|
-
};
|
|
331
|
+
const cols = buildForeignKeyColumns(prop, metaByClass);
|
|
332
|
+
if (prop.type === owningMeta.className) {
|
|
333
|
+
return cols.map((col) => ({ ...col, isSelfReference: true }));
|
|
334
|
+
}
|
|
335
|
+
return cols;
|
|
202
336
|
}
|
|
203
|
-
return
|
|
337
|
+
return [];
|
|
204
338
|
}
|
|
205
339
|
function resolveFormulaExpr(cb) {
|
|
206
340
|
try {
|
|
@@ -210,18 +344,69 @@ function resolveFormulaExpr(cb) {
|
|
|
210
344
|
get: (_target, key) => typeof key === "string" ? key : ""
|
|
211
345
|
}
|
|
212
346
|
);
|
|
213
|
-
|
|
347
|
+
const result = cb(FORMULA_DUMMY_TABLE, cols);
|
|
348
|
+
return typeof result === "string" ? result : String(result);
|
|
214
349
|
} catch {
|
|
215
|
-
return
|
|
350
|
+
return UNRESOLVED_FORMULA;
|
|
216
351
|
}
|
|
217
352
|
}
|
|
218
|
-
function
|
|
219
|
-
const
|
|
353
|
+
function buildForeignKeyColumns(prop, metaByClass) {
|
|
354
|
+
const fieldNames = prop.fieldNames && prop.fieldNames.length > 0 ? prop.fieldNames : [`${prop.name}_id`];
|
|
355
|
+
const fkTypes = resolveFkTypes(prop, metaByClass, fieldNames.length);
|
|
356
|
+
return fieldNames.map((fieldName, index) => ({
|
|
357
|
+
propName: prop.name,
|
|
358
|
+
fieldName,
|
|
359
|
+
type: fkTypes[index] ?? fkTypes[0] ?? "integer",
|
|
360
|
+
isPrimary: prop.primary === true,
|
|
361
|
+
isForeignKey: true,
|
|
362
|
+
isUnique: prop.unique === true,
|
|
363
|
+
isNullable: prop.nullable === true,
|
|
364
|
+
...prop.comment !== void 0 && { comment: prop.comment },
|
|
365
|
+
referencedEntity: prop.type
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
function resolveFkTypes(prop, metaByClass, fieldNameCount) {
|
|
369
|
+
const refMeta = metaByClass.get(prop.type);
|
|
220
370
|
if (!refMeta) {
|
|
371
|
+
return Array.from({ length: fieldNameCount }, () => "integer");
|
|
372
|
+
}
|
|
373
|
+
const primaryProps = getPrimaryProps(refMeta);
|
|
374
|
+
const referencedColumnNames = prop.referencedColumnNames ?? [];
|
|
375
|
+
return Array.from({ length: fieldNameCount }, (_value, index) => {
|
|
376
|
+
const referencedColumnName = referencedColumnNames[index];
|
|
377
|
+
const pkProp = referencedColumnName !== void 0 ? primaryProps.find((candidate) => candidate.fieldNames.includes(referencedColumnName)) : void 0;
|
|
378
|
+
const resolvedProp = pkProp ?? primaryProps[index] ?? primaryProps[0];
|
|
379
|
+
const rawType = resolvedProp?.type ?? "integer";
|
|
380
|
+
const pkIndex = resolvedProp !== void 0 ? primaryProps.indexOf(resolvedProp) : 0;
|
|
381
|
+
return resolveScalarType(rawType, metaByClass, pkIndex);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
function resolveScalarType(type, metaByClass, pkIndex = 0, depth = 0) {
|
|
385
|
+
if (depth >= 5) {
|
|
221
386
|
return "integer";
|
|
222
387
|
}
|
|
223
|
-
const
|
|
224
|
-
|
|
388
|
+
const refMeta = metaByClass.get(type);
|
|
389
|
+
if (!refMeta) {
|
|
390
|
+
return type;
|
|
391
|
+
}
|
|
392
|
+
const primaryProps = getPrimaryProps(refMeta);
|
|
393
|
+
if (primaryProps.length === 0) {
|
|
394
|
+
return type;
|
|
395
|
+
}
|
|
396
|
+
const targetProp = primaryProps[pkIndex] ?? primaryProps[0];
|
|
397
|
+
const nextType = targetProp?.type ?? type;
|
|
398
|
+
if (nextType === type) {
|
|
399
|
+
return type;
|
|
400
|
+
}
|
|
401
|
+
return resolveScalarType(nextType, metaByClass, pkIndex, depth + 1);
|
|
402
|
+
}
|
|
403
|
+
function getPrimaryProps(meta) {
|
|
404
|
+
const primaryKeys = meta.primaryKeys ?? [];
|
|
405
|
+
const orderedPrimaryProps = primaryKeys.map((key) => meta.properties[String(key)]).filter((prop) => prop !== void 0);
|
|
406
|
+
if (orderedPrimaryProps.length > 0) {
|
|
407
|
+
return orderedPrimaryProps;
|
|
408
|
+
}
|
|
409
|
+
return Object.values(meta.properties).filter((prop) => prop.primary === true);
|
|
225
410
|
}
|
|
226
411
|
function buildConstraints(meta) {
|
|
227
412
|
const result = [];
|
|
@@ -229,7 +414,7 @@ function buildConstraints(meta) {
|
|
|
229
414
|
const props = idx.properties;
|
|
230
415
|
result.push({
|
|
231
416
|
type: "index",
|
|
232
|
-
properties:
|
|
417
|
+
properties: resolveConstraintProperties(meta, props),
|
|
233
418
|
...idx.name !== void 0 && { name: idx.name }
|
|
234
419
|
});
|
|
235
420
|
}
|
|
@@ -237,7 +422,7 @@ function buildConstraints(meta) {
|
|
|
237
422
|
const props = uniq.properties;
|
|
238
423
|
result.push({
|
|
239
424
|
type: "unique",
|
|
240
|
-
properties:
|
|
425
|
+
properties: resolveConstraintProperties(meta, props),
|
|
241
426
|
...uniq.name !== void 0 && { name: uniq.name }
|
|
242
427
|
});
|
|
243
428
|
}
|
|
@@ -254,6 +439,19 @@ function buildConstraints(meta) {
|
|
|
254
439
|
}
|
|
255
440
|
return result;
|
|
256
441
|
}
|
|
442
|
+
function resolveConstraintProperties(meta, props) {
|
|
443
|
+
const propNames = Array.isArray(props) ? props : props !== void 0 ? [props] : [];
|
|
444
|
+
return propNames.flatMap((propName) => {
|
|
445
|
+
const prop = meta.properties[String(propName)];
|
|
446
|
+
if (prop === void 0) {
|
|
447
|
+
return [String(propName)];
|
|
448
|
+
}
|
|
449
|
+
if (prop.fieldNames !== void 0 && prop.fieldNames.length > 0) {
|
|
450
|
+
return prop.fieldNames;
|
|
451
|
+
}
|
|
452
|
+
return [prop.name];
|
|
453
|
+
});
|
|
454
|
+
}
|
|
257
455
|
function buildRelationEdges(metas) {
|
|
258
456
|
const edges = [];
|
|
259
457
|
for (const meta of metas) {
|
|
@@ -272,6 +470,9 @@ function buildRelationEdges(metas) {
|
|
|
272
470
|
function buildEdge(fromEntity, prop) {
|
|
273
471
|
const isNullable = prop.nullable === true;
|
|
274
472
|
if (prop.kind === ReferenceKind.MANY_TO_ONE) {
|
|
473
|
+
if (prop.type === fromEntity) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
275
476
|
return {
|
|
276
477
|
fromEntity,
|
|
277
478
|
toEntity: prop.type,
|
|
@@ -300,17 +501,41 @@ function buildEdge(fromEntity, prop) {
|
|
|
300
501
|
}
|
|
301
502
|
return null;
|
|
302
503
|
}
|
|
504
|
+
function normalizeType(type) {
|
|
505
|
+
const t = type.toLowerCase().trim();
|
|
506
|
+
if (t === "uuid" || t === "text" || t === "string" || t.startsWith("varchar")) {
|
|
507
|
+
return "string";
|
|
508
|
+
}
|
|
509
|
+
if (t === "timestamptz" || t === "timestamp" || t === "datetime") {
|
|
510
|
+
return "datetime";
|
|
511
|
+
}
|
|
512
|
+
if (t === "integer" || t === "int" || t === "bigint" || t === "smallint") {
|
|
513
|
+
return "integer";
|
|
514
|
+
}
|
|
515
|
+
if (t === "doubletype" || t === "double precision" || t === "double" || t === "float" || t === "decimal") {
|
|
516
|
+
return "float";
|
|
517
|
+
}
|
|
518
|
+
if (t === "boolean" || t === "bool") {
|
|
519
|
+
return "boolean";
|
|
520
|
+
}
|
|
521
|
+
if (t === "jsonb") {
|
|
522
|
+
return "json";
|
|
523
|
+
}
|
|
524
|
+
return type;
|
|
525
|
+
}
|
|
303
526
|
function renderErDiagram(model) {
|
|
304
527
|
const lines = ["erDiagram"];
|
|
305
528
|
for (const entity of model.entities) {
|
|
306
|
-
lines.push(` ${entity.className} {`);
|
|
529
|
+
lines.push(` ${toMermaidIdentifier(entity.className)} {`);
|
|
307
530
|
for (const col of entity.columns) {
|
|
308
531
|
lines.push(` ${renderColumnLine(col)}`);
|
|
309
532
|
}
|
|
310
533
|
lines.push(" }");
|
|
311
534
|
}
|
|
312
535
|
for (const rel of model.relations) {
|
|
313
|
-
lines.push(
|
|
536
|
+
lines.push(
|
|
537
|
+
` ${toMermaidIdentifier(rel.fromEntity)} ${rel.fromCardinality}--${rel.toCardinality} ${toMermaidIdentifier(rel.toEntity)} : "${escapeMermaidQuotedText(rel.label)}"`
|
|
538
|
+
);
|
|
314
539
|
}
|
|
315
540
|
return lines.join("\n");
|
|
316
541
|
}
|
|
@@ -318,8 +543,6 @@ function renderColumnLine(col) {
|
|
|
318
543
|
let qualifier = "";
|
|
319
544
|
if (col.isPrimary) {
|
|
320
545
|
qualifier = " PK";
|
|
321
|
-
} else if (col.isForeignKey) {
|
|
322
|
-
qualifier = " FK";
|
|
323
546
|
} else if (col.isUnique) {
|
|
324
547
|
qualifier = " UK";
|
|
325
548
|
}
|
|
@@ -330,27 +553,38 @@ function renderColumnLine(col) {
|
|
|
330
553
|
comment = "discriminator";
|
|
331
554
|
} else if (col.embeddedIn !== void 0) {
|
|
332
555
|
comment = `[${col.embeddedIn}]`;
|
|
333
|
-
} else if (col.
|
|
334
|
-
comment =
|
|
556
|
+
} else if (col.isSelfReference) {
|
|
557
|
+
comment = "self-ref";
|
|
335
558
|
}
|
|
336
|
-
const commentStr = comment !== void 0 ? ` "${comment}"` : "";
|
|
337
|
-
return `${col.type} ${col.fieldName}${qualifier}${commentStr}`;
|
|
338
|
-
}
|
|
339
|
-
function normalizeType(type) {
|
|
340
|
-
return type.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
559
|
+
const commentStr = comment !== void 0 ? ` "${escapeMermaidQuotedText(comment)}"` : "";
|
|
560
|
+
return `${toMermaidIdentifier(normalizeType(col.type))} ${toMermaidIdentifier(col.fieldName)}${qualifier}${commentStr}`;
|
|
341
561
|
}
|
|
342
562
|
|
|
343
563
|
// src/model/build.ts
|
|
344
|
-
|
|
345
|
-
|
|
564
|
+
var FROM_ONE_OR_MORE = "}|";
|
|
565
|
+
var TO_ONE_OR_MORE = "|{";
|
|
566
|
+
function buildDocumentModel(metas, jsDocResult, title, description, onWarn) {
|
|
567
|
+
const { entities: diagramEntities, relations } = buildDiagramModel(metas);
|
|
568
|
+
const allRelations = applyAtLeastOne(relations, metas, jsDocResult.props, onWarn);
|
|
569
|
+
const hiddenClasses = /* @__PURE__ */ new Set();
|
|
570
|
+
for (const model of diagramEntities) {
|
|
571
|
+
if (jsDocResult.entities.get(model.className)?.hidden) {
|
|
572
|
+
hiddenClasses.add(model.className);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
346
575
|
const enrichedByClass = /* @__PURE__ */ new Map();
|
|
347
576
|
for (const model of diagramEntities) {
|
|
348
577
|
const jsDoc = jsDocResult.entities.get(model.className);
|
|
349
578
|
if (jsDoc?.hidden) {
|
|
350
579
|
continue;
|
|
351
580
|
}
|
|
352
|
-
const
|
|
353
|
-
|
|
581
|
+
const columns = model.columns.filter(
|
|
582
|
+
(col) => !(col.isForeignKey && col.referencedEntity !== void 0 && hiddenClasses.has(col.referencedEntity))
|
|
583
|
+
);
|
|
584
|
+
const visibleModel = columns.length === model.columns.length ? model : { ...model, columns };
|
|
585
|
+
const ownPropDocs = jsDocResult.props.get(model.className) ?? /* @__PURE__ */ new Map();
|
|
586
|
+
const propDocs = withEmbeddedPropDocs(ownPropDocs, visibleModel.columns, jsDocResult.props);
|
|
587
|
+
enrichedByClass.set(model.className, { model: visibleModel, jsDoc, propDocs });
|
|
354
588
|
}
|
|
355
589
|
const groupNames = /* @__PURE__ */ new Set();
|
|
356
590
|
let anyUntagged = false;
|
|
@@ -370,9 +604,16 @@ function buildDocumentModel(metas, jsDocResult, title, description) {
|
|
|
370
604
|
const groups = [];
|
|
371
605
|
for (const groupName of groupNames) {
|
|
372
606
|
const isDefault = groupName === "default";
|
|
373
|
-
const erdEntities = [...enrichedByClass.values()].filter(
|
|
374
|
-
|
|
375
|
-
|
|
607
|
+
const erdEntities = [...enrichedByClass.values()].filter(({ jsDoc }) => belongsToGroupForErd(jsDoc, groupName, isDefault)).map((entity) => {
|
|
608
|
+
if (isCrossNamespaceInGroup(entity.jsDoc, groupName, isDefault)) {
|
|
609
|
+
const pkColumns = entity.model.columns.filter((col) => col.isPrimary);
|
|
610
|
+
if (pkColumns.length === 0) {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
return { ...entity, model: { ...entity.model, columns: pkColumns } };
|
|
614
|
+
}
|
|
615
|
+
return entity;
|
|
616
|
+
}).filter((entity) => entity !== null);
|
|
376
617
|
const textEntities = [...enrichedByClass.values()].filter(
|
|
377
618
|
({ jsDoc }) => belongsToGroupForText(jsDoc, groupName, isDefault)
|
|
378
619
|
);
|
|
@@ -391,6 +632,65 @@ function buildDocumentModel(metas, jsDocResult, title, description) {
|
|
|
391
632
|
});
|
|
392
633
|
return { title, groups, ...description !== void 0 && { description } };
|
|
393
634
|
}
|
|
635
|
+
function withEmbeddedPropDocs(ownPropDocs, columns, allPropDocs) {
|
|
636
|
+
const merged = new Map(ownPropDocs);
|
|
637
|
+
for (const col of columns) {
|
|
638
|
+
if (merged.has(col.propName) || col.embeddedIn === void 0 || col.embeddedPropName === void 0) {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
const info = allPropDocs.get(col.embeddedIn)?.get(col.embeddedPropName);
|
|
642
|
+
if (info) {
|
|
643
|
+
merged.set(col.propName, info);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return merged;
|
|
647
|
+
}
|
|
648
|
+
function applyAtLeastOne(relations, metas, props, onWarn) {
|
|
649
|
+
const adjusted = relations.map((edge) => ({ ...edge }));
|
|
650
|
+
const metaByClass = new Map(metas.map((m) => [m.className, m]));
|
|
651
|
+
for (const [className, propMap] of props) {
|
|
652
|
+
const meta = metaByClass.get(className);
|
|
653
|
+
if (!meta) {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
for (const [propName, info] of propMap) {
|
|
657
|
+
if (!info.atLeastOne) {
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const prop = meta.properties[propName];
|
|
661
|
+
if (!prop) {
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
let edge;
|
|
665
|
+
if (prop.kind === ReferenceKind2.ONE_TO_MANY && prop.mappedBy) {
|
|
666
|
+
edge = adjusted.find(
|
|
667
|
+
(e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
|
|
668
|
+
);
|
|
669
|
+
if (edge) {
|
|
670
|
+
edge.fromCardinality = FROM_ONE_OR_MORE;
|
|
671
|
+
}
|
|
672
|
+
} else if (prop.kind === ReferenceKind2.MANY_TO_MANY && prop.owner === true) {
|
|
673
|
+
edge = adjusted.find((e) => e.fromEntity === className && e.toEntity === prop.type && e.label === propName);
|
|
674
|
+
if (edge) {
|
|
675
|
+
edge.toCardinality = TO_ONE_OR_MORE;
|
|
676
|
+
}
|
|
677
|
+
} else if (prop.kind === ReferenceKind2.MANY_TO_MANY && prop.mappedBy) {
|
|
678
|
+
edge = adjusted.find(
|
|
679
|
+
(e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
|
|
680
|
+
);
|
|
681
|
+
if (edge) {
|
|
682
|
+
edge.fromCardinality = FROM_ONE_OR_MORE;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (!edge) {
|
|
686
|
+
onWarn?.(
|
|
687
|
+
`@atLeastOne on ${className}.${propName} had no effect: no matching relation edge was found. It applies only to collection relations that can be matched to a rendered edge: @OneToMany with mappedBy, or @ManyToMany on either the owning side or an inverse mappedBy side.`
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return adjusted;
|
|
693
|
+
}
|
|
394
694
|
function hasNoNamespaceTags(jsDoc) {
|
|
395
695
|
if (!jsDoc) {
|
|
396
696
|
return true;
|
|
@@ -415,20 +715,62 @@ function belongsToGroupForText(jsDoc, groupName, isDefault) {
|
|
|
415
715
|
}
|
|
416
716
|
return jsDoc.namespaces.includes(groupName) || jsDoc.describeNamespaces.includes(groupName);
|
|
417
717
|
}
|
|
718
|
+
function isCrossNamespaceInGroup(jsDoc, groupName, isDefault) {
|
|
719
|
+
if (isDefault || !jsDoc) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
return jsDoc.erdNamespaces.includes(groupName) && !jsDoc.namespaces.includes(groupName) && !jsDoc.describeNamespaces.includes(groupName);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/provider.ts
|
|
726
|
+
async function withTsMorphMetadataProvider(options, onWarn) {
|
|
727
|
+
if (options.metadataProvider !== void 0) {
|
|
728
|
+
return options;
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
const { TsMorphMetadataProvider } = await import("@mikro-orm/reflection");
|
|
732
|
+
return { ...options, metadataProvider: TsMorphMetadataProvider };
|
|
733
|
+
} catch (err) {
|
|
734
|
+
const code = err !== null && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
735
|
+
const isNotInstalled = code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
|
|
736
|
+
if (!isNotInstalled && onWarn) {
|
|
737
|
+
onWarn(
|
|
738
|
+
`@mikro-orm/reflection is installed but failed to load: ${err instanceof Error ? err.message : String(err)}. Ensure all @mikro-orm/* packages are installed at the same version.`
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
return options;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
418
744
|
|
|
419
745
|
// src/render/markdown.ts
|
|
420
746
|
function renderMarkdown(docModel) {
|
|
421
|
-
const sections = [`# ${docModel.title}`];
|
|
747
|
+
const sections = [`# ${escapeMarkdownInline(docModel.title)}`];
|
|
422
748
|
if (docModel.description) {
|
|
423
|
-
sections.push(docModel.description);
|
|
749
|
+
sections.push(escapeMarkdownParagraph(docModel.description));
|
|
750
|
+
}
|
|
751
|
+
if (docModel.groups.length > 1) {
|
|
752
|
+
sections.push(renderTableOfContents(docModel.groups));
|
|
424
753
|
}
|
|
425
754
|
for (const group of docModel.groups) {
|
|
426
755
|
sections.push(renderGroupSection(group));
|
|
427
756
|
}
|
|
428
757
|
return sections.join("\n\n");
|
|
429
758
|
}
|
|
759
|
+
function renderBulletSection(header, items, renderItem) {
|
|
760
|
+
const lines = [header, ""];
|
|
761
|
+
for (const item of items) {
|
|
762
|
+
lines.push(renderItem(item));
|
|
763
|
+
}
|
|
764
|
+
return lines.join("\n");
|
|
765
|
+
}
|
|
766
|
+
function renderTableOfContents(groups) {
|
|
767
|
+
return renderBulletSection("## Contents", groups, (group) => {
|
|
768
|
+
const label = escapeMarkdownInline(group.name).replace(/[[\]]/g, "\\$&");
|
|
769
|
+
return `- [${label}](#${toMarkdownAnchor(group.name)})`;
|
|
770
|
+
});
|
|
771
|
+
}
|
|
430
772
|
function renderGroupSection(group) {
|
|
431
|
-
const parts = [`## ${group.name}`];
|
|
773
|
+
const parts = [`## ${escapeMarkdownInline(group.name)}`];
|
|
432
774
|
if (group.erdEntities.length > 0) {
|
|
433
775
|
const diagramModel = {
|
|
434
776
|
entities: group.erdEntities.map((e) => e.model),
|
|
@@ -442,14 +784,18 @@ function renderGroupSection(group) {
|
|
|
442
784
|
return parts.join("\n\n");
|
|
443
785
|
}
|
|
444
786
|
function renderEntitySection(entity) {
|
|
445
|
-
const parts = [`### ${entity.model.className}`];
|
|
787
|
+
const parts = [`### ${escapeMarkdownInline(entity.model.className)}`];
|
|
788
|
+
parts.push(`*Table: ${renderMarkdownInlineCode(entity.model.tableName)}*`);
|
|
446
789
|
if (entity.jsDoc?.description) {
|
|
447
|
-
parts.push(
|
|
790
|
+
parts.push(renderMarkdownBlockQuote(entity.jsDoc.description));
|
|
448
791
|
}
|
|
449
792
|
if (entity.model.discriminatorColumn) {
|
|
450
|
-
parts.push(`*STI root \u2014 discriminator column:
|
|
793
|
+
parts.push(`*STI root \u2014 discriminator column: ${renderMarkdownInlineCode(entity.model.discriminatorColumn)}*`);
|
|
451
794
|
} else if (entity.model.extendsEntity) {
|
|
452
|
-
|
|
795
|
+
const discValue = entity.model.discriminatorValue !== void 0 ? `, discriminator value: ${renderMarkdownInlineCode(entity.model.discriminatorValue)}` : "";
|
|
796
|
+
parts.push(
|
|
797
|
+
`*Extends ${renderMarkdownInlineCode(entity.model.extendsEntity)} (Single Table Inheritance${discValue})*`
|
|
798
|
+
);
|
|
453
799
|
}
|
|
454
800
|
if (entity.model.columns.length > 0) {
|
|
455
801
|
parts.push(renderColumnTable(entity));
|
|
@@ -457,6 +803,10 @@ function renderEntitySection(entity) {
|
|
|
457
803
|
if (entity.model.constraints.length > 0) {
|
|
458
804
|
parts.push(renderConstraints(entity.model.constraints));
|
|
459
805
|
}
|
|
806
|
+
const computedColumns = entity.model.columns.filter((col) => col.formula !== void 0);
|
|
807
|
+
if (computedColumns.length > 0) {
|
|
808
|
+
parts.push(renderComputedColumns(computedColumns));
|
|
809
|
+
}
|
|
460
810
|
return parts.join("\n\n");
|
|
461
811
|
}
|
|
462
812
|
function renderColumnTable(entity) {
|
|
@@ -465,24 +815,27 @@ function renderColumnTable(entity) {
|
|
|
465
815
|
const rows = entity.model.columns.map((col) => {
|
|
466
816
|
const key = resolveColumnKey(col);
|
|
467
817
|
const nullable = col.isNullable && !col.isPrimary ? "Y" : "";
|
|
468
|
-
const
|
|
469
|
-
|
|
818
|
+
const docDesc = entity.propDocs.get(col.propName)?.description ?? col.comment ?? "";
|
|
819
|
+
const enumDesc = col.enumItems !== void 0 ? `One of: ${col.enumItems.join(", ")}` : "";
|
|
820
|
+
const desc = [docDesc, enumDesc].filter((part) => part !== "").join("\n");
|
|
821
|
+
return `| ${escapeMarkdownTableCell(col.fieldName)} | ${escapeMarkdownTableCell(normalizeType(col.type))} | ${escapeMarkdownTableCell(key)} | ${nullable} | ${escapeMarkdownTableCell(desc)} |`;
|
|
470
822
|
});
|
|
471
823
|
return [header, sep, ...rows].join("\n");
|
|
472
824
|
}
|
|
473
825
|
function resolveColumnKey(col) {
|
|
826
|
+
const fkKey = col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
|
|
827
|
+
if (col.isPrimary && col.isForeignKey) {
|
|
828
|
+
return `PK, ${fkKey}`;
|
|
829
|
+
}
|
|
474
830
|
if (col.isPrimary) {
|
|
475
831
|
return "PK";
|
|
476
832
|
}
|
|
477
833
|
if (col.isForeignKey) {
|
|
478
|
-
return
|
|
834
|
+
return fkKey;
|
|
479
835
|
}
|
|
480
836
|
if (col.isUnique) {
|
|
481
837
|
return "UK";
|
|
482
838
|
}
|
|
483
|
-
if (col.formula !== void 0) {
|
|
484
|
-
return `formula: ${col.formula}`;
|
|
485
|
-
}
|
|
486
839
|
if (col.isDiscriminator) {
|
|
487
840
|
return "discriminator";
|
|
488
841
|
}
|
|
@@ -491,43 +844,227 @@ function resolveColumnKey(col) {
|
|
|
491
844
|
}
|
|
492
845
|
return "";
|
|
493
846
|
}
|
|
847
|
+
function renderComputedColumns(columns) {
|
|
848
|
+
return renderBulletSection("**Computed columns:**", columns, (col) => {
|
|
849
|
+
const expr = col.formula ? `: ${renderMarkdownInlineCode(col.formula)}` : "";
|
|
850
|
+
return `- ${renderMarkdownInlineCode(col.fieldName)}${expr}`;
|
|
851
|
+
});
|
|
852
|
+
}
|
|
494
853
|
function renderConstraints(constraints) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const
|
|
854
|
+
return renderBulletSection("**Constraints:**", constraints, (c) => {
|
|
855
|
+
const name = c.name ? ` ${renderMarkdownInlineCode(c.name)}` : "";
|
|
856
|
+
const properties = c.properties.map(escapeMarkdownInline).join(", ");
|
|
498
857
|
if (c.type === "index") {
|
|
499
|
-
|
|
500
|
-
} else if (c.type === "unique") {
|
|
501
|
-
lines.push(`- Unique${name}: (${c.properties.join(", ")})`);
|
|
502
|
-
} else if (c.type === "check") {
|
|
503
|
-
lines.push(`- Check${name}: \`${c.expression ?? ""}\``);
|
|
858
|
+
return `- Index${name}: (${properties})`;
|
|
504
859
|
}
|
|
505
|
-
|
|
506
|
-
|
|
860
|
+
if (c.type === "unique") {
|
|
861
|
+
return `- Unique${name}: (${properties})`;
|
|
862
|
+
}
|
|
863
|
+
return `- Check${name}: ${renderMarkdownInlineCode(c.expression ?? "")}`;
|
|
864
|
+
});
|
|
507
865
|
}
|
|
508
866
|
|
|
509
867
|
// src/index.ts
|
|
868
|
+
var COMPILED_JS = /\.(c|m)?js$/i;
|
|
869
|
+
function resolveJsDocSources(sourcePaths, src, onWarn) {
|
|
870
|
+
if (src !== void 0 && src.length > 0) {
|
|
871
|
+
return src;
|
|
872
|
+
}
|
|
873
|
+
if (sourcePaths.some((p) => COMPILED_JS.test(p)) && onWarn) {
|
|
874
|
+
onWarn(
|
|
875
|
+
'Entities were discovered from compiled JavaScript, so JSDoc descriptions and @namespace/@hidden tags cannot be read (build tools strip comments). Hidden entities may be exposed. Pass --src "<glob to your .ts sources>" (or the `src` option) to read JSDoc from the original TypeScript files.'
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
return sourcePaths;
|
|
879
|
+
}
|
|
880
|
+
function assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn) {
|
|
881
|
+
if (jsDocResult.sourceFileCount === 0) {
|
|
882
|
+
throw new Error(
|
|
883
|
+
`No source files matched the explicit src paths: ${src.join(", ")}
|
|
884
|
+
Check the --src glob/path (or the \`src\` option). Without matching TypeScript sources, JSDoc tags such as @namespace and @hidden cannot be read.`
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
const isRenderable = (meta) => !meta.pivotTable && !meta.embeddable;
|
|
888
|
+
const missingConcrete = metas.filter((meta) => isRenderable(meta) && !meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
|
|
889
|
+
if (missingConcrete.length > 0) {
|
|
890
|
+
throw new Error(
|
|
891
|
+
`Explicit src paths did not include source declarations for discovered entities: ${missingConcrete.join(", ")}
|
|
892
|
+
Check that --src (or the \`src\` option) points at all TypeScript entity files. JSDoc tags such as @namespace and @hidden for missing entities cannot be read.`
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
if (onWarn) {
|
|
896
|
+
const missingAbstract = metas.filter((meta) => isRenderable(meta) && meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
|
|
897
|
+
if (missingAbstract.length > 0) {
|
|
898
|
+
onWarn(
|
|
899
|
+
`Abstract STI parent entities were not found in the explicit src paths: ${missingAbstract.join(", ")}
|
|
900
|
+
@hidden and @namespace tags for these entities will not be applied. Include their source files in --src to enable JSDoc tags for them.`
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function errorMessages(err) {
|
|
906
|
+
const messages = [];
|
|
907
|
+
const seen = /* @__PURE__ */ new Set();
|
|
908
|
+
let current = err;
|
|
909
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
910
|
+
seen.add(current);
|
|
911
|
+
messages.push(current.message);
|
|
912
|
+
current = current.cause;
|
|
913
|
+
}
|
|
914
|
+
return messages;
|
|
915
|
+
}
|
|
916
|
+
function isMissingTsMorphSourceFile(err) {
|
|
917
|
+
return errorMessages(err).some((message) => message.includes("Source file") && message.includes("not found"));
|
|
918
|
+
}
|
|
919
|
+
async function loadEntityMetadataWithTsMorphFallback(originalOrm, effectiveOrm) {
|
|
920
|
+
try {
|
|
921
|
+
return await loadEntityMetadata(effectiveOrm);
|
|
922
|
+
} catch (err) {
|
|
923
|
+
const wasAutoInjected = originalOrm.metadataProvider === void 0 && effectiveOrm.metadataProvider !== void 0;
|
|
924
|
+
if (!wasAutoInjected || !isMissingTsMorphSourceFile(err)) {
|
|
925
|
+
throw err;
|
|
926
|
+
}
|
|
927
|
+
try {
|
|
928
|
+
return await loadEntityMetadata(originalOrm);
|
|
929
|
+
} catch {
|
|
930
|
+
throw err;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
510
934
|
async function generateMarkdown(options) {
|
|
511
|
-
const { orm, title = "Database Schema", description, src
|
|
512
|
-
const
|
|
513
|
-
const
|
|
514
|
-
const
|
|
935
|
+
const { orm, title = "Database Schema", description, src, onWarn } = options;
|
|
936
|
+
const effectiveOrm = await withTsMorphMetadataProvider(orm, onWarn);
|
|
937
|
+
const { metas, sourcePaths } = await loadEntityMetadataWithTsMorphFallback(orm, effectiveOrm);
|
|
938
|
+
const jsDocResult = loadJsDoc(resolveJsDocSources(sourcePaths, src, onWarn));
|
|
939
|
+
if (src !== void 0 && src.length > 0) {
|
|
940
|
+
assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn);
|
|
941
|
+
}
|
|
942
|
+
const docModel = buildDocumentModel(metas, jsDocResult, title, description, onWarn);
|
|
515
943
|
return renderMarkdown(docModel);
|
|
516
944
|
}
|
|
517
945
|
|
|
518
946
|
// src/cli.ts
|
|
947
|
+
function toConfigImportSpecifier(configPath) {
|
|
948
|
+
return pathToFileURL(path2.resolve(configPath)).href;
|
|
949
|
+
}
|
|
950
|
+
function findNearestTsconfig(fromPath) {
|
|
951
|
+
let dir = path2.dirname(path2.resolve(fromPath));
|
|
952
|
+
for (; ; ) {
|
|
953
|
+
const candidate = path2.join(dir, "tsconfig.json");
|
|
954
|
+
if (syncFs.existsSync(candidate)) {
|
|
955
|
+
return candidate;
|
|
956
|
+
}
|
|
957
|
+
const parent = path2.dirname(dir);
|
|
958
|
+
if (parent === dir) {
|
|
959
|
+
return void 0;
|
|
960
|
+
}
|
|
961
|
+
dir = parent;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
async function loadOrmOptions(configPath, tsconfigPath) {
|
|
965
|
+
const isTypeScriptConfig = configPath.endsWith(".ts");
|
|
966
|
+
if (isTypeScriptConfig) {
|
|
967
|
+
let register;
|
|
968
|
+
try {
|
|
969
|
+
({ register } = await import("tsx/esm/api"));
|
|
970
|
+
} catch {
|
|
971
|
+
throw new Error('TypeScript config files require the "tsx" package.\nInstall it with: npm install -D tsx');
|
|
972
|
+
}
|
|
973
|
+
let tsconfig;
|
|
974
|
+
if (tsconfigPath !== void 0) {
|
|
975
|
+
tsconfig = path2.resolve(tsconfigPath);
|
|
976
|
+
if (!syncFs.existsSync(tsconfig)) {
|
|
977
|
+
throw new Error(`--tsconfig file not found: ${tsconfig}`);
|
|
978
|
+
}
|
|
979
|
+
} else {
|
|
980
|
+
tsconfig = findNearestTsconfig(configPath);
|
|
981
|
+
}
|
|
982
|
+
register(tsconfig !== void 0 ? { tsconfig } : {});
|
|
983
|
+
}
|
|
984
|
+
const configUrl = toConfigImportSpecifier(configPath);
|
|
985
|
+
let mod;
|
|
986
|
+
try {
|
|
987
|
+
mod = await import(
|
|
988
|
+
/* @vite-ignore */
|
|
989
|
+
configUrl
|
|
990
|
+
);
|
|
991
|
+
} catch (cause) {
|
|
992
|
+
if (isTypeScriptConfig) {
|
|
993
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
994
|
+
throw new Error(
|
|
995
|
+
`Failed to load TypeScript config.
|
|
996
|
+
${detail}
|
|
997
|
+
|
|
998
|
+
If this looks like a decorator/metadata error, the tsconfig applied to your entity files is likely missing "experimentalDecorators" / "emitDecoratorMetadata".
|
|
999
|
+
Make sure a tsconfig.json with those options sits next to your config file, or pass one explicitly with --tsconfig <path>.`,
|
|
1000
|
+
{ cause }
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
throw cause;
|
|
1004
|
+
}
|
|
1005
|
+
if (mod.default === void 0) {
|
|
1006
|
+
throw new Error("Config file must use a default export, e.g. `export default defineConfig({ ... })`.");
|
|
1007
|
+
}
|
|
1008
|
+
const config = mod.default;
|
|
1009
|
+
if (typeof config === "function" || config instanceof Promise) {
|
|
1010
|
+
throw new Error(
|
|
1011
|
+
"Config file default export must be a configuration object, not a function or Promise.\nResolve it first, or use the programmatic API instead (see README)."
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
1015
|
+
throw new Error(
|
|
1016
|
+
"Config file default export must be a configuration object, not a primitive value or array.\nExport a plain MikroORM options object instead."
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
const options = config;
|
|
1020
|
+
const withPreferTs = isTypeScriptConfig && options.preferTs === void 0 ? { ...options, preferTs: true } : options;
|
|
1021
|
+
return withPreferTs;
|
|
1022
|
+
}
|
|
1023
|
+
function formatErrorChain(err) {
|
|
1024
|
+
const lines = [];
|
|
1025
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1026
|
+
let current = err;
|
|
1027
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
1028
|
+
seen.add(current);
|
|
1029
|
+
lines.push(current.message);
|
|
1030
|
+
current = current.cause;
|
|
1031
|
+
}
|
|
1032
|
+
if (current !== void 0 && !(current instanceof Error)) {
|
|
1033
|
+
lines.push(String(current));
|
|
1034
|
+
}
|
|
1035
|
+
return lines.map((line, i) => i === 0 ? line : ` \u21B3 caused by: ${line}`).join("\n");
|
|
1036
|
+
}
|
|
1037
|
+
var REFLECTION_METADATA_HINT = "\n\nNote: this tool loads configs via tsx (esbuild), which does not emit 'emitDecoratorMetadata' reflection data, so enabling it will not help here.\nEither give each entity property an explicit 'type:'/'entity:' attribute, or install '@mikro-orm/reflection' \u2014 this tool then reads types from your TypeScript sources automatically.";
|
|
1038
|
+
function formatDiscoveryError(err) {
|
|
1039
|
+
const chain = formatErrorChain(err);
|
|
1040
|
+
return chain.includes("emitDecoratorMetadata") ? chain + REFLECTION_METADATA_HINT : chain;
|
|
1041
|
+
}
|
|
1042
|
+
function formatFileSystemError(cause) {
|
|
1043
|
+
if (cause instanceof Error) {
|
|
1044
|
+
const code = "code" in cause && typeof cause.code === "string" ? ` (${cause.code})` : "";
|
|
1045
|
+
return `${cause.message}${code}`;
|
|
1046
|
+
}
|
|
1047
|
+
return String(cause);
|
|
1048
|
+
}
|
|
1049
|
+
async function writeMarkdownFile(outPath, markdown) {
|
|
1050
|
+
try {
|
|
1051
|
+
await fs.mkdir(path2.dirname(outPath), { recursive: true });
|
|
1052
|
+
await fs.writeFile(outPath, markdown, "utf-8");
|
|
1053
|
+
} catch (cause) {
|
|
1054
|
+
throw new Error(`Cannot write output file: ${outPath}
|
|
1055
|
+
${formatFileSystemError(cause)}`, { cause });
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
519
1058
|
async function run(opts) {
|
|
520
|
-
const configPath =
|
|
521
|
-
const outPath =
|
|
1059
|
+
const configPath = path2.resolve(opts.config);
|
|
1060
|
+
const outPath = path2.resolve(opts.out);
|
|
522
1061
|
let ormOptions;
|
|
523
1062
|
try {
|
|
524
|
-
|
|
525
|
-
ormOptions = mod.default ?? mod;
|
|
1063
|
+
ormOptions = await loadOrmOptions(configPath, opts.tsconfig);
|
|
526
1064
|
} catch (err) {
|
|
527
|
-
const hint = configPath.endsWith(".ts") ? "\nHint: TypeScript configs require tsx or ts-node:\n npx tsx ./node_modules/.bin/mikro-orm-markdown ..." : "";
|
|
528
1065
|
process.stderr.write(
|
|
529
1066
|
`Error: Cannot load config: ${configPath}
|
|
530
|
-
${err instanceof Error ? err.message : String(err)}
|
|
1067
|
+
${err instanceof Error ? err.message : String(err)}
|
|
531
1068
|
`
|
|
532
1069
|
);
|
|
533
1070
|
process.exit(1);
|
|
@@ -537,23 +1074,57 @@ ${err instanceof Error ? err.message : String(err)}${hint}
|
|
|
537
1074
|
markdown = await generateMarkdown({
|
|
538
1075
|
orm: ormOptions,
|
|
539
1076
|
title: opts.title,
|
|
540
|
-
|
|
541
|
-
...opts.
|
|
1077
|
+
...opts.description !== void 0 && { description: opts.description },
|
|
1078
|
+
...opts.src !== void 0 && { src: opts.src },
|
|
1079
|
+
onWarn: (message) => void process.stderr.write(`Warning: ${message}
|
|
1080
|
+
`)
|
|
542
1081
|
});
|
|
543
1082
|
} catch (err) {
|
|
544
|
-
|
|
545
|
-
process.stderr.write(`Error: ${msg}
|
|
1083
|
+
process.stderr.write(`Error: ${formatDiscoveryError(err)}
|
|
546
1084
|
`);
|
|
547
1085
|
process.exit(1);
|
|
548
1086
|
}
|
|
549
|
-
|
|
550
|
-
|
|
1087
|
+
try {
|
|
1088
|
+
await writeMarkdownFile(outPath, markdown);
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
551
1091
|
`);
|
|
1092
|
+
process.exit(1);
|
|
1093
|
+
}
|
|
1094
|
+
process.stdout.write(`\u2713 Written to ${path2.relative(process.cwd(), outPath)}
|
|
1095
|
+
`);
|
|
1096
|
+
}
|
|
1097
|
+
var program = new Command().name("mikro-orm-markdown").description("Generate Mermaid ERD + markdown docs from MikroORM entities").requiredOption("-c, --config <path>", "MikroORM config file path").option("-o, --out <path>", "Output markdown file path", "./ERD.md").option("-t, --title <string>", "Document title", "Database Schema").option("-d, --description <string>", "Optional description paragraph shown below the title").option(
|
|
1098
|
+
"--tsconfig <path>",
|
|
1099
|
+
"tsconfig.json to use when loading a .ts config (defaults to the nearest one beside the config file)"
|
|
1100
|
+
).option(
|
|
1101
|
+
"--src <paths...>",
|
|
1102
|
+
"Source .ts file globs to read JSDoc from when entities run from compiled .js (comments are stripped at build time)"
|
|
1103
|
+
).action(run);
|
|
1104
|
+
function isDirectCliExecution() {
|
|
1105
|
+
const entryPoint = process.argv[1];
|
|
1106
|
+
if (entryPoint === void 0) {
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
1109
|
+
try {
|
|
1110
|
+
return syncFs.realpathSync(path2.resolve(entryPoint)) === syncFs.realpathSync(fileURLToPath(import.meta.url));
|
|
1111
|
+
} catch {
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
552
1114
|
}
|
|
553
|
-
|
|
554
|
-
program.parseAsync(process.argv).catch((err) => {
|
|
555
|
-
|
|
1115
|
+
if (isDirectCliExecution()) {
|
|
1116
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1117
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
556
1118
|
`);
|
|
557
|
-
|
|
558
|
-
});
|
|
1119
|
+
process.exit(1);
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
export {
|
|
1123
|
+
findNearestTsconfig,
|
|
1124
|
+
formatDiscoveryError,
|
|
1125
|
+
formatErrorChain,
|
|
1126
|
+
loadOrmOptions,
|
|
1127
|
+
toConfigImportSpecifier,
|
|
1128
|
+
writeMarkdownFile
|
|
1129
|
+
};
|
|
559
1130
|
//# sourceMappingURL=cli.js.map
|