mikro-orm-markdown 0.1.0-alpha.1 → 0.1.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/CODE_OF_CONDUCT.md +25 -0
- package/LICENSE +1 -1
- package/README.ko.md +268 -57
- package/README.md +267 -56
- package/SECURITY.md +28 -0
- package/dist/cli.cjs +745 -161
- 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 +725 -163
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +577 -143
- 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 +566 -143
- package/dist/index.js.map +1 -1
- package/package.json +44 -16
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
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
|
-
|
|
15
|
+
const classNames = /* @__PURE__ */ new Set();
|
|
16
|
+
if (filePaths.length === 0) {
|
|
17
|
+
return { entities, props, sourceFileCount: 0, classNames };
|
|
18
|
+
}
|
|
14
19
|
const project = new Project({
|
|
15
20
|
skipAddingFilesFromTsConfig: true,
|
|
16
21
|
skipLoadingLibFiles: true,
|
|
@@ -19,30 +24,44 @@ function loadJsDoc(srcGlobs) {
|
|
|
19
24
|
skipLibCheck: true
|
|
20
25
|
}
|
|
21
26
|
});
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
+
continue;
|
|
40
|
+
}
|
|
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);
|
|
38
59
|
}
|
|
39
60
|
}
|
|
40
|
-
|
|
41
|
-
props.set(className, propMap);
|
|
42
|
-
}
|
|
61
|
+
} catch {
|
|
43
62
|
}
|
|
44
63
|
}
|
|
45
|
-
return { entities, props };
|
|
64
|
+
return { entities, props, sourceFileCount: sourceFiles.length, classNames };
|
|
46
65
|
}
|
|
47
66
|
function parseEntityJsDoc(jsDocs) {
|
|
48
67
|
const namespaces = [];
|
|
@@ -52,14 +71,21 @@ function parseEntityJsDoc(jsDocs) {
|
|
|
52
71
|
let description;
|
|
53
72
|
for (const doc of jsDocs) {
|
|
54
73
|
const desc = doc.getDescription().trim();
|
|
55
|
-
if (desc && description === void 0)
|
|
74
|
+
if (desc && description === void 0) {
|
|
75
|
+
description = desc;
|
|
76
|
+
}
|
|
56
77
|
for (const tag of doc.getTags()) {
|
|
57
78
|
const tagName = tag.getTagName();
|
|
58
79
|
const comment = tag.getCommentText()?.trim();
|
|
59
|
-
if (tagName === "namespace" && comment)
|
|
60
|
-
|
|
61
|
-
else if (tagName === "
|
|
62
|
-
|
|
80
|
+
if (tagName === "namespace" && comment) {
|
|
81
|
+
namespaces.push(comment);
|
|
82
|
+
} else if (tagName === "erd" && comment) {
|
|
83
|
+
erdNamespaces.push(comment);
|
|
84
|
+
} else if (tagName === "describe" && comment) {
|
|
85
|
+
describeNamespaces.push(comment);
|
|
86
|
+
} else if (tagName === "hidden") {
|
|
87
|
+
hidden = true;
|
|
88
|
+
}
|
|
63
89
|
}
|
|
64
90
|
}
|
|
65
91
|
return {
|
|
@@ -70,16 +96,26 @@ function parseEntityJsDoc(jsDocs) {
|
|
|
70
96
|
hidden
|
|
71
97
|
};
|
|
72
98
|
}
|
|
73
|
-
function
|
|
99
|
+
function parsePropJsDoc(jsDocs) {
|
|
100
|
+
let description;
|
|
101
|
+
let atLeastOne = false;
|
|
74
102
|
for (const doc of jsDocs) {
|
|
75
103
|
const desc = doc.getDescription().trim();
|
|
76
|
-
if (desc)
|
|
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
|
+
}
|
|
111
|
+
}
|
|
77
112
|
}
|
|
78
|
-
return void 0;
|
|
113
|
+
return { ...description !== void 0 && { description }, atLeastOne };
|
|
79
114
|
}
|
|
80
115
|
|
|
81
116
|
// src/metadata/load.ts
|
|
82
|
-
import
|
|
117
|
+
import * as path from "path";
|
|
118
|
+
import { EntitySchema, MikroORM } from "@mikro-orm/core";
|
|
83
119
|
var MetadataLoadError = class extends Error {
|
|
84
120
|
constructor(message, cause) {
|
|
85
121
|
super(message);
|
|
@@ -87,12 +123,46 @@ var MetadataLoadError = class extends Error {
|
|
|
87
123
|
this.name = "MetadataLoadError";
|
|
88
124
|
}
|
|
89
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
|
+
}
|
|
90
154
|
async function loadEntityMetadata(options) {
|
|
155
|
+
assertNoEntitySchemaEntities(options);
|
|
91
156
|
let orm;
|
|
92
157
|
try {
|
|
93
158
|
orm = await MikroORM.init({
|
|
94
159
|
...options,
|
|
95
|
-
debug: false
|
|
160
|
+
debug: 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 }
|
|
96
166
|
});
|
|
97
167
|
} catch (cause) {
|
|
98
168
|
throw new MetadataLoadError(
|
|
@@ -107,20 +177,73 @@ async function loadEntityMetadata(options) {
|
|
|
107
177
|
"No entities were discovered. Check that your config specifies at least one entity path or class."
|
|
108
178
|
);
|
|
109
179
|
}
|
|
110
|
-
|
|
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 };
|
|
111
183
|
} finally {
|
|
112
|
-
await orm
|
|
184
|
+
await closeDiscoveryResources(orm);
|
|
113
185
|
}
|
|
114
186
|
}
|
|
115
187
|
|
|
188
|
+
// src/model/build.ts
|
|
189
|
+
import { ReferenceKind as ReferenceKind2 } from "@mikro-orm/core";
|
|
190
|
+
|
|
116
191
|
// src/render/mermaid.ts
|
|
117
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
|
|
118
240
|
var FORMULA_DUMMY_TABLE = {
|
|
119
241
|
alias: "e0",
|
|
120
242
|
name: "",
|
|
121
243
|
qualifiedName: "",
|
|
122
244
|
toString: () => "e0"
|
|
123
245
|
};
|
|
246
|
+
var UNRESOLVED_FORMULA = "<unresolved>";
|
|
124
247
|
function buildDiagramModel(metas) {
|
|
125
248
|
const metaByClass = new Map(metas.map((m) => [m.className, m]));
|
|
126
249
|
const entities = metas.filter((meta) => !meta.pivotTable && !meta.embeddable).map((meta) => buildEntityModel(meta, metaByClass));
|
|
@@ -128,13 +251,14 @@ function buildDiagramModel(metas) {
|
|
|
128
251
|
return { entities, relations };
|
|
129
252
|
}
|
|
130
253
|
function buildEntityModel(meta, metaByClass) {
|
|
131
|
-
const isStiRoot = meta.discriminatorColumn !== void 0 && !meta.
|
|
254
|
+
const isStiRoot = meta.discriminatorColumn !== void 0 && !meta.extends;
|
|
132
255
|
const isStiChild = Boolean(meta.extends) && meta.discriminatorValue !== void 0;
|
|
133
256
|
const columns = [];
|
|
134
257
|
for (const prop of Object.values(meta.properties)) {
|
|
135
|
-
if (isStiRoot && prop.inherited === true)
|
|
136
|
-
|
|
137
|
-
|
|
258
|
+
if (isStiRoot && prop.inherited === true) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
columns.push(...buildColumns(prop, metaByClass, meta));
|
|
138
262
|
}
|
|
139
263
|
return {
|
|
140
264
|
className: meta.className,
|
|
@@ -144,61 +268,123 @@ function buildEntityModel(meta, metaByClass) {
|
|
|
144
268
|
isEmbeddable: meta.embeddable === true,
|
|
145
269
|
...isStiRoot && { discriminatorColumn: meta.discriminatorColumn },
|
|
146
270
|
...isStiChild && { extendsEntity: meta.extends },
|
|
271
|
+
...isStiChild && meta.discriminatorValue !== void 0 && { discriminatorValue: String(meta.discriminatorValue) },
|
|
147
272
|
constraints: buildConstraints(meta)
|
|
148
273
|
};
|
|
149
274
|
}
|
|
150
|
-
function
|
|
151
|
-
if (prop.kind === ReferenceKind.EMBEDDED)
|
|
275
|
+
function buildColumns(prop, metaByClass, owningMeta) {
|
|
276
|
+
if (prop.kind === ReferenceKind.EMBEDDED) {
|
|
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 [];
|
|
293
|
+
}
|
|
152
294
|
if (prop.kind === ReferenceKind.SCALAR) {
|
|
295
|
+
if (prop.object === true && prop.embedded !== void 0) {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
153
298
|
const formulaExpr = prop.formula !== void 0 ? resolveFormulaExpr(prop.formula) : void 0;
|
|
154
299
|
let embeddedIn;
|
|
300
|
+
let embeddedPropName;
|
|
155
301
|
if (prop.embedded !== void 0) {
|
|
156
302
|
const parentPropName = prop.embedded[0];
|
|
157
303
|
embeddedIn = owningMeta.properties[parentPropName]?.type;
|
|
304
|
+
embeddedPropName = prop.embedded[1];
|
|
158
305
|
}
|
|
159
306
|
const isDiscriminator = owningMeta.discriminatorColumn !== void 0 && prop.name === owningMeta.discriminatorColumn;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
];
|
|
172
329
|
}
|
|
173
330
|
if (prop.kind === ReferenceKind.MANY_TO_ONE || prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner === true) {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
isPrimary: false,
|
|
180
|
-
isForeignKey: true,
|
|
181
|
-
isUnique: prop.unique === true,
|
|
182
|
-
isNullable: prop.nullable === true
|
|
183
|
-
};
|
|
331
|
+
const cols = buildForeignKeyColumns(prop, metaByClass);
|
|
332
|
+
if (prop.type === owningMeta.className) {
|
|
333
|
+
return cols.map((col) => ({ ...col, isSelfReference: true }));
|
|
334
|
+
}
|
|
335
|
+
return cols;
|
|
184
336
|
}
|
|
185
|
-
return
|
|
337
|
+
return [];
|
|
186
338
|
}
|
|
187
339
|
function resolveFormulaExpr(cb) {
|
|
188
340
|
try {
|
|
189
|
-
const cols = new Proxy(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
341
|
+
const cols = new Proxy(
|
|
342
|
+
{},
|
|
343
|
+
{
|
|
344
|
+
get: (_target, key) => typeof key === "string" ? key : ""
|
|
345
|
+
}
|
|
346
|
+
);
|
|
347
|
+
const result = cb(FORMULA_DUMMY_TABLE, cols);
|
|
348
|
+
return typeof result === "string" ? result : String(result);
|
|
193
349
|
} catch {
|
|
194
|
-
return
|
|
350
|
+
return UNRESOLVED_FORMULA;
|
|
195
351
|
}
|
|
196
352
|
}
|
|
197
|
-
function
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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);
|
|
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
|
+
return (pkProp ?? primaryProps[index] ?? primaryProps[0])?.type ?? "integer";
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
function getPrimaryProps(meta) {
|
|
382
|
+
const primaryKeys = meta.primaryKeys ?? [];
|
|
383
|
+
const orderedPrimaryProps = primaryKeys.map((key) => meta.properties[String(key)]).filter((prop) => prop !== void 0);
|
|
384
|
+
if (orderedPrimaryProps.length > 0) {
|
|
385
|
+
return orderedPrimaryProps;
|
|
386
|
+
}
|
|
387
|
+
return Object.values(meta.properties).filter((prop) => prop.primary === true);
|
|
202
388
|
}
|
|
203
389
|
function buildConstraints(meta) {
|
|
204
390
|
const result = [];
|
|
@@ -206,7 +392,7 @@ function buildConstraints(meta) {
|
|
|
206
392
|
const props = idx.properties;
|
|
207
393
|
result.push({
|
|
208
394
|
type: "index",
|
|
209
|
-
properties:
|
|
395
|
+
properties: resolveConstraintProperties(meta, props),
|
|
210
396
|
...idx.name !== void 0 && { name: idx.name }
|
|
211
397
|
});
|
|
212
398
|
}
|
|
@@ -214,12 +400,14 @@ function buildConstraints(meta) {
|
|
|
214
400
|
const props = uniq.properties;
|
|
215
401
|
result.push({
|
|
216
402
|
type: "unique",
|
|
217
|
-
properties:
|
|
403
|
+
properties: resolveConstraintProperties(meta, props),
|
|
218
404
|
...uniq.name !== void 0 && { name: uniq.name }
|
|
219
405
|
});
|
|
220
406
|
}
|
|
221
407
|
for (const check of meta.checks ?? []) {
|
|
222
|
-
if (typeof check.expression !== "string")
|
|
408
|
+
if (typeof check.expression !== "string") {
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
223
411
|
result.push({
|
|
224
412
|
type: "check",
|
|
225
413
|
properties: [],
|
|
@@ -229,13 +417,30 @@ function buildConstraints(meta) {
|
|
|
229
417
|
}
|
|
230
418
|
return result;
|
|
231
419
|
}
|
|
420
|
+
function resolveConstraintProperties(meta, props) {
|
|
421
|
+
const propNames = Array.isArray(props) ? props : props !== void 0 ? [props] : [];
|
|
422
|
+
return propNames.flatMap((propName) => {
|
|
423
|
+
const prop = meta.properties[String(propName)];
|
|
424
|
+
if (prop === void 0) {
|
|
425
|
+
return [String(propName)];
|
|
426
|
+
}
|
|
427
|
+
if (prop.fieldNames !== void 0 && prop.fieldNames.length > 0) {
|
|
428
|
+
return prop.fieldNames;
|
|
429
|
+
}
|
|
430
|
+
return [prop.name];
|
|
431
|
+
});
|
|
432
|
+
}
|
|
232
433
|
function buildRelationEdges(metas) {
|
|
233
434
|
const edges = [];
|
|
234
435
|
for (const meta of metas) {
|
|
235
|
-
if (meta.pivotTable || meta.embeddable)
|
|
436
|
+
if (meta.pivotTable || meta.embeddable) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
236
439
|
for (const prop of Object.values(meta.properties)) {
|
|
237
440
|
const edge = buildEdge(meta.className, prop);
|
|
238
|
-
if (edge !== null)
|
|
441
|
+
if (edge !== null) {
|
|
442
|
+
edges.push(edge);
|
|
443
|
+
}
|
|
239
444
|
}
|
|
240
445
|
}
|
|
241
446
|
return edges;
|
|
@@ -243,6 +448,9 @@ function buildRelationEdges(metas) {
|
|
|
243
448
|
function buildEdge(fromEntity, prop) {
|
|
244
449
|
const isNullable = prop.nullable === true;
|
|
245
450
|
if (prop.kind === ReferenceKind.MANY_TO_ONE) {
|
|
451
|
+
if (prop.type === fromEntity) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
246
454
|
return {
|
|
247
455
|
fromEntity,
|
|
248
456
|
toEntity: prop.type,
|
|
@@ -274,7 +482,7 @@ function buildEdge(fromEntity, prop) {
|
|
|
274
482
|
function renderErDiagram(model) {
|
|
275
483
|
const lines = ["erDiagram"];
|
|
276
484
|
for (const entity of model.entities) {
|
|
277
|
-
lines.push(` ${entity.className} {`);
|
|
485
|
+
lines.push(` ${toMermaidIdentifier(entity.className)} {`);
|
|
278
486
|
for (const col of entity.columns) {
|
|
279
487
|
lines.push(` ${renderColumnLine(col)}`);
|
|
280
488
|
}
|
|
@@ -282,16 +490,18 @@ function renderErDiagram(model) {
|
|
|
282
490
|
}
|
|
283
491
|
for (const rel of model.relations) {
|
|
284
492
|
lines.push(
|
|
285
|
-
` ${rel.fromEntity} ${rel.fromCardinality}--${rel.toCardinality} ${rel.toEntity} : "${rel.label}"`
|
|
493
|
+
` ${toMermaidIdentifier(rel.fromEntity)} ${rel.fromCardinality}--${rel.toCardinality} ${toMermaidIdentifier(rel.toEntity)} : "${escapeMermaidQuotedText(rel.label)}"`
|
|
286
494
|
);
|
|
287
495
|
}
|
|
288
496
|
return lines.join("\n");
|
|
289
497
|
}
|
|
290
498
|
function renderColumnLine(col) {
|
|
291
499
|
let qualifier = "";
|
|
292
|
-
if (col.isPrimary)
|
|
293
|
-
|
|
294
|
-
else if (col.isUnique)
|
|
500
|
+
if (col.isPrimary) {
|
|
501
|
+
qualifier = " PK";
|
|
502
|
+
} else if (col.isUnique) {
|
|
503
|
+
qualifier = " UK";
|
|
504
|
+
}
|
|
295
505
|
let comment;
|
|
296
506
|
if (col.formula !== void 0) {
|
|
297
507
|
comment = col.formula ? `formula: ${col.formula}` : "formula";
|
|
@@ -299,41 +509,54 @@ function renderColumnLine(col) {
|
|
|
299
509
|
comment = "discriminator";
|
|
300
510
|
} else if (col.embeddedIn !== void 0) {
|
|
301
511
|
comment = `[${col.embeddedIn}]`;
|
|
302
|
-
} else if (col.
|
|
303
|
-
comment =
|
|
512
|
+
} else if (col.isSelfReference) {
|
|
513
|
+
comment = "self-ref";
|
|
304
514
|
}
|
|
305
|
-
const commentStr = comment !== void 0 ? ` "${comment}"` : "";
|
|
306
|
-
return `${col.type} ${col.fieldName}${qualifier}${commentStr}`;
|
|
307
|
-
}
|
|
308
|
-
function normalizeType(type) {
|
|
309
|
-
return type.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
515
|
+
const commentStr = comment !== void 0 ? ` "${escapeMermaidQuotedText(comment)}"` : "";
|
|
516
|
+
return `${toMermaidIdentifier(col.type)} ${toMermaidIdentifier(col.fieldName)}${qualifier}${commentStr}`;
|
|
310
517
|
}
|
|
311
518
|
|
|
312
519
|
// src/model/build.ts
|
|
313
|
-
|
|
314
|
-
|
|
520
|
+
var FROM_ONE_OR_MORE = "}|";
|
|
521
|
+
var TO_ONE_OR_MORE = "|{";
|
|
522
|
+
function buildDocumentModel(metas, jsDocResult, title, description, onWarn) {
|
|
523
|
+
const { entities: diagramEntities, relations } = buildDiagramModel(metas);
|
|
524
|
+
const allRelations = applyAtLeastOne(relations, metas, jsDocResult.props, onWarn);
|
|
525
|
+
const hiddenClasses = /* @__PURE__ */ new Set();
|
|
526
|
+
for (const model of diagramEntities) {
|
|
527
|
+
if (jsDocResult.entities.get(model.className)?.hidden) {
|
|
528
|
+
hiddenClasses.add(model.className);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
315
531
|
const enrichedByClass = /* @__PURE__ */ new Map();
|
|
316
532
|
for (const model of diagramEntities) {
|
|
317
533
|
const jsDoc = jsDocResult.entities.get(model.className);
|
|
318
|
-
if (jsDoc?.hidden)
|
|
319
|
-
|
|
320
|
-
|
|
534
|
+
if (jsDoc?.hidden) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const columns = model.columns.filter(
|
|
538
|
+
(col) => !(col.isForeignKey && col.referencedEntity !== void 0 && hiddenClasses.has(col.referencedEntity))
|
|
539
|
+
);
|
|
540
|
+
const visibleModel = columns.length === model.columns.length ? model : { ...model, columns };
|
|
541
|
+
const ownPropDocs = jsDocResult.props.get(model.className) ?? /* @__PURE__ */ new Map();
|
|
542
|
+
const propDocs = withEmbeddedPropDocs(ownPropDocs, visibleModel.columns, jsDocResult.props);
|
|
543
|
+
enrichedByClass.set(model.className, { model: visibleModel, jsDoc, propDocs });
|
|
321
544
|
}
|
|
322
545
|
const groupNames = /* @__PURE__ */ new Set();
|
|
323
546
|
let anyUntagged = false;
|
|
324
547
|
for (const { jsDoc } of enrichedByClass.values()) {
|
|
325
|
-
const allNs = [
|
|
326
|
-
...jsDoc?.namespaces ?? [],
|
|
327
|
-
...jsDoc?.erdNamespaces ?? [],
|
|
328
|
-
...jsDoc?.describeNamespaces ?? []
|
|
329
|
-
];
|
|
548
|
+
const allNs = [...jsDoc?.namespaces ?? [], ...jsDoc?.erdNamespaces ?? [], ...jsDoc?.describeNamespaces ?? []];
|
|
330
549
|
if (allNs.length === 0) {
|
|
331
550
|
anyUntagged = true;
|
|
332
551
|
} else {
|
|
333
|
-
for (const ns of allNs)
|
|
552
|
+
for (const ns of allNs) {
|
|
553
|
+
groupNames.add(ns);
|
|
554
|
+
}
|
|
334
555
|
}
|
|
335
556
|
}
|
|
336
|
-
if (anyUntagged)
|
|
557
|
+
if (anyUntagged) {
|
|
558
|
+
groupNames.add("default");
|
|
559
|
+
}
|
|
337
560
|
const groups = [];
|
|
338
561
|
for (const groupName of groupNames) {
|
|
339
562
|
const isDefault = groupName === "default";
|
|
@@ -344,46 +567,153 @@ function buildDocumentModel(metas, jsDocResult, title, description) {
|
|
|
344
567
|
({ jsDoc }) => belongsToGroupForText(jsDoc, groupName, isDefault)
|
|
345
568
|
);
|
|
346
569
|
const erdClassNames = new Set(erdEntities.map((e) => e.model.className));
|
|
347
|
-
const erdRelations = allRelations.filter(
|
|
348
|
-
(r) => erdClassNames.has(r.fromEntity) && erdClassNames.has(r.toEntity)
|
|
349
|
-
);
|
|
570
|
+
const erdRelations = allRelations.filter((r) => erdClassNames.has(r.fromEntity) && erdClassNames.has(r.toEntity));
|
|
350
571
|
groups.push({ name: groupName, erdEntities, textEntities, erdRelations });
|
|
351
572
|
}
|
|
352
573
|
groups.sort((a, b) => {
|
|
353
|
-
if (a.name === "default")
|
|
354
|
-
|
|
574
|
+
if (a.name === "default") {
|
|
575
|
+
return 1;
|
|
576
|
+
}
|
|
577
|
+
if (b.name === "default") {
|
|
578
|
+
return -1;
|
|
579
|
+
}
|
|
355
580
|
return a.name.localeCompare(b.name);
|
|
356
581
|
});
|
|
357
582
|
return { title, groups, ...description !== void 0 && { description } };
|
|
358
583
|
}
|
|
584
|
+
function withEmbeddedPropDocs(ownPropDocs, columns, allPropDocs) {
|
|
585
|
+
const merged = new Map(ownPropDocs);
|
|
586
|
+
for (const col of columns) {
|
|
587
|
+
if (merged.has(col.propName) || col.embeddedIn === void 0 || col.embeddedPropName === void 0) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const info = allPropDocs.get(col.embeddedIn)?.get(col.embeddedPropName);
|
|
591
|
+
if (info) {
|
|
592
|
+
merged.set(col.propName, info);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return merged;
|
|
596
|
+
}
|
|
597
|
+
function applyAtLeastOne(relations, metas, props, onWarn) {
|
|
598
|
+
const adjusted = relations.map((edge) => ({ ...edge }));
|
|
599
|
+
const metaByClass = new Map(metas.map((m) => [m.className, m]));
|
|
600
|
+
for (const [className, propMap] of props) {
|
|
601
|
+
const meta = metaByClass.get(className);
|
|
602
|
+
if (!meta) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
for (const [propName, info] of propMap) {
|
|
606
|
+
if (!info.atLeastOne) {
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const prop = meta.properties[propName];
|
|
610
|
+
if (!prop) {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
let edge;
|
|
614
|
+
if (prop.kind === ReferenceKind2.ONE_TO_MANY && prop.mappedBy) {
|
|
615
|
+
edge = adjusted.find(
|
|
616
|
+
(e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
|
|
617
|
+
);
|
|
618
|
+
if (edge) {
|
|
619
|
+
edge.fromCardinality = FROM_ONE_OR_MORE;
|
|
620
|
+
}
|
|
621
|
+
} else if (prop.kind === ReferenceKind2.MANY_TO_MANY && prop.owner === true) {
|
|
622
|
+
edge = adjusted.find((e) => e.fromEntity === className && e.toEntity === prop.type && e.label === propName);
|
|
623
|
+
if (edge) {
|
|
624
|
+
edge.toCardinality = TO_ONE_OR_MORE;
|
|
625
|
+
}
|
|
626
|
+
} else if (prop.kind === ReferenceKind2.MANY_TO_MANY && prop.mappedBy) {
|
|
627
|
+
edge = adjusted.find(
|
|
628
|
+
(e) => e.fromEntity === prop.type && e.toEntity === className && e.label === prop.mappedBy
|
|
629
|
+
);
|
|
630
|
+
if (edge) {
|
|
631
|
+
edge.fromCardinality = FROM_ONE_OR_MORE;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (!edge) {
|
|
635
|
+
onWarn?.(
|
|
636
|
+
`@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.`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return adjusted;
|
|
642
|
+
}
|
|
359
643
|
function hasNoNamespaceTags(jsDoc) {
|
|
360
|
-
if (!jsDoc)
|
|
644
|
+
if (!jsDoc) {
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
361
647
|
return jsDoc.namespaces.length === 0 && jsDoc.erdNamespaces.length === 0 && jsDoc.describeNamespaces.length === 0;
|
|
362
648
|
}
|
|
363
649
|
function belongsToGroupForErd(jsDoc, groupName, isDefault) {
|
|
364
|
-
if (isDefault)
|
|
365
|
-
|
|
650
|
+
if (isDefault) {
|
|
651
|
+
return hasNoNamespaceTags(jsDoc);
|
|
652
|
+
}
|
|
653
|
+
if (!jsDoc) {
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
366
656
|
return jsDoc.namespaces.includes(groupName) || jsDoc.erdNamespaces.includes(groupName);
|
|
367
657
|
}
|
|
368
658
|
function belongsToGroupForText(jsDoc, groupName, isDefault) {
|
|
369
|
-
if (isDefault)
|
|
370
|
-
|
|
659
|
+
if (isDefault) {
|
|
660
|
+
return hasNoNamespaceTags(jsDoc);
|
|
661
|
+
}
|
|
662
|
+
if (!jsDoc) {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
371
665
|
return jsDoc.namespaces.includes(groupName) || jsDoc.describeNamespaces.includes(groupName);
|
|
372
666
|
}
|
|
373
667
|
|
|
668
|
+
// src/provider.ts
|
|
669
|
+
async function withTsMorphMetadataProvider(options, onWarn) {
|
|
670
|
+
if (options.metadataProvider !== void 0) {
|
|
671
|
+
return options;
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
const { TsMorphMetadataProvider } = await import("@mikro-orm/reflection");
|
|
675
|
+
return { ...options, metadataProvider: TsMorphMetadataProvider };
|
|
676
|
+
} catch (err) {
|
|
677
|
+
const code = err !== null && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
678
|
+
const isNotInstalled = code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
|
|
679
|
+
if (!isNotInstalled && onWarn) {
|
|
680
|
+
onWarn(
|
|
681
|
+
`@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.`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
return options;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
374
688
|
// src/render/markdown.ts
|
|
375
689
|
function renderMarkdown(docModel) {
|
|
376
|
-
const sections = [`# ${docModel.title}`];
|
|
690
|
+
const sections = [`# ${escapeMarkdownInline(docModel.title)}`];
|
|
377
691
|
if (docModel.description) {
|
|
378
|
-
sections.push(docModel.description);
|
|
692
|
+
sections.push(escapeMarkdownParagraph(docModel.description));
|
|
693
|
+
}
|
|
694
|
+
if (docModel.groups.length > 1) {
|
|
695
|
+
sections.push(renderTableOfContents(docModel.groups));
|
|
379
696
|
}
|
|
380
697
|
for (const group of docModel.groups) {
|
|
381
698
|
sections.push(renderGroupSection(group));
|
|
382
699
|
}
|
|
383
700
|
return sections.join("\n\n");
|
|
384
701
|
}
|
|
702
|
+
function renderBulletSection(header, items, renderItem) {
|
|
703
|
+
const lines = [header, ""];
|
|
704
|
+
for (const item of items) {
|
|
705
|
+
lines.push(renderItem(item));
|
|
706
|
+
}
|
|
707
|
+
return lines.join("\n");
|
|
708
|
+
}
|
|
709
|
+
function renderTableOfContents(groups) {
|
|
710
|
+
return renderBulletSection("## Contents", groups, (group) => {
|
|
711
|
+
const label = escapeMarkdownInline(group.name).replace(/[[\]]/g, "\\$&");
|
|
712
|
+
return `- [${label}](#${toMarkdownAnchor(group.name)})`;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
385
715
|
function renderGroupSection(group) {
|
|
386
|
-
const parts = [`## ${group.name}`];
|
|
716
|
+
const parts = [`## ${escapeMarkdownInline(group.name)}`];
|
|
387
717
|
if (group.erdEntities.length > 0) {
|
|
388
718
|
const diagramModel = {
|
|
389
719
|
entities: group.erdEntities.map((e) => e.model),
|
|
@@ -397,16 +727,18 @@ function renderGroupSection(group) {
|
|
|
397
727
|
return parts.join("\n\n");
|
|
398
728
|
}
|
|
399
729
|
function renderEntitySection(entity) {
|
|
400
|
-
const parts = [`### ${entity.model.className}`];
|
|
730
|
+
const parts = [`### ${escapeMarkdownInline(entity.model.className)}`];
|
|
731
|
+
parts.push(`*Table: ${renderMarkdownInlineCode(entity.model.tableName)}*`);
|
|
401
732
|
if (entity.jsDoc?.description) {
|
|
402
|
-
parts.push(
|
|
733
|
+
parts.push(renderMarkdownBlockQuote(entity.jsDoc.description));
|
|
403
734
|
}
|
|
404
735
|
if (entity.model.discriminatorColumn) {
|
|
736
|
+
parts.push(`*STI root \u2014 discriminator column: ${renderMarkdownInlineCode(entity.model.discriminatorColumn)}*`);
|
|
737
|
+
} else if (entity.model.extendsEntity) {
|
|
738
|
+
const discValue = entity.model.discriminatorValue !== void 0 ? `, discriminator value: ${renderMarkdownInlineCode(entity.model.discriminatorValue)}` : "";
|
|
405
739
|
parts.push(
|
|
406
|
-
`*
|
|
740
|
+
`*Extends ${renderMarkdownInlineCode(entity.model.extendsEntity)} (Single Table Inheritance${discValue})*`
|
|
407
741
|
);
|
|
408
|
-
} else if (entity.model.extendsEntity) {
|
|
409
|
-
parts.push(`*Extends \`${entity.model.extendsEntity}\` (Single Table Inheritance)*`);
|
|
410
742
|
}
|
|
411
743
|
if (entity.model.columns.length > 0) {
|
|
412
744
|
parts.push(renderColumnTable(entity));
|
|
@@ -414,6 +746,10 @@ function renderEntitySection(entity) {
|
|
|
414
746
|
if (entity.model.constraints.length > 0) {
|
|
415
747
|
parts.push(renderConstraints(entity.model.constraints));
|
|
416
748
|
}
|
|
749
|
+
const computedColumns = entity.model.columns.filter((col) => col.formula !== void 0);
|
|
750
|
+
if (computedColumns.length > 0) {
|
|
751
|
+
parts.push(renderComputedColumns(computedColumns));
|
|
752
|
+
}
|
|
417
753
|
return parts.join("\n\n");
|
|
418
754
|
}
|
|
419
755
|
function renderColumnTable(entity) {
|
|
@@ -422,59 +758,256 @@ function renderColumnTable(entity) {
|
|
|
422
758
|
const rows = entity.model.columns.map((col) => {
|
|
423
759
|
const key = resolveColumnKey(col);
|
|
424
760
|
const nullable = col.isNullable && !col.isPrimary ? "Y" : "";
|
|
425
|
-
const
|
|
426
|
-
|
|
761
|
+
const docDesc = entity.propDocs.get(col.propName)?.description ?? col.comment ?? "";
|
|
762
|
+
const enumDesc = col.enumItems !== void 0 ? `One of: ${col.enumItems.join(", ")}` : "";
|
|
763
|
+
const desc = [docDesc, enumDesc].filter((part) => part !== "").join("\n");
|
|
764
|
+
return `| ${escapeMarkdownTableCell(col.fieldName)} | ${escapeMarkdownTableCell(col.type)} | ${escapeMarkdownTableCell(key)} | ${nullable} | ${escapeMarkdownTableCell(desc)} |`;
|
|
427
765
|
});
|
|
428
766
|
return [header, sep, ...rows].join("\n");
|
|
429
767
|
}
|
|
430
768
|
function resolveColumnKey(col) {
|
|
431
|
-
|
|
769
|
+
const fkKey = col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
|
|
770
|
+
if (col.isPrimary && col.isForeignKey) {
|
|
771
|
+
return `PK, ${fkKey}`;
|
|
772
|
+
}
|
|
773
|
+
if (col.isPrimary) {
|
|
774
|
+
return "PK";
|
|
775
|
+
}
|
|
432
776
|
if (col.isForeignKey) {
|
|
433
|
-
return
|
|
777
|
+
return fkKey;
|
|
778
|
+
}
|
|
779
|
+
if (col.isUnique) {
|
|
780
|
+
return "UK";
|
|
781
|
+
}
|
|
782
|
+
if (col.isDiscriminator) {
|
|
783
|
+
return "discriminator";
|
|
784
|
+
}
|
|
785
|
+
if (col.embeddedIn !== void 0) {
|
|
786
|
+
return `[${col.embeddedIn}]`;
|
|
434
787
|
}
|
|
435
|
-
if (col.isUnique) return "UK";
|
|
436
|
-
if (col.formula !== void 0) return `formula: ${col.formula}`;
|
|
437
|
-
if (col.isDiscriminator) return "discriminator";
|
|
438
|
-
if (col.embeddedIn !== void 0) return `[${col.embeddedIn}]`;
|
|
439
788
|
return "";
|
|
440
789
|
}
|
|
790
|
+
function renderComputedColumns(columns) {
|
|
791
|
+
return renderBulletSection("**Computed columns:**", columns, (col) => {
|
|
792
|
+
const expr = col.formula ? `: ${renderMarkdownInlineCode(col.formula)}` : "";
|
|
793
|
+
return `- ${renderMarkdownInlineCode(col.fieldName)}${expr}`;
|
|
794
|
+
});
|
|
795
|
+
}
|
|
441
796
|
function renderConstraints(constraints) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const
|
|
797
|
+
return renderBulletSection("**Constraints:**", constraints, (c) => {
|
|
798
|
+
const name = c.name ? ` ${renderMarkdownInlineCode(c.name)}` : "";
|
|
799
|
+
const properties = c.properties.map(escapeMarkdownInline).join(", ");
|
|
445
800
|
if (c.type === "index") {
|
|
446
|
-
|
|
447
|
-
} else if (c.type === "unique") {
|
|
448
|
-
lines.push(`- Unique${name}: (${c.properties.join(", ")})`);
|
|
449
|
-
} else if (c.type === "check") {
|
|
450
|
-
lines.push(`- Check${name}: \`${c.expression ?? ""}\``);
|
|
801
|
+
return `- Index${name}: (${properties})`;
|
|
451
802
|
}
|
|
452
|
-
|
|
453
|
-
|
|
803
|
+
if (c.type === "unique") {
|
|
804
|
+
return `- Unique${name}: (${properties})`;
|
|
805
|
+
}
|
|
806
|
+
return `- Check${name}: ${renderMarkdownInlineCode(c.expression ?? "")}`;
|
|
807
|
+
});
|
|
454
808
|
}
|
|
455
809
|
|
|
456
810
|
// src/index.ts
|
|
811
|
+
var COMPILED_JS = /\.(c|m)?js$/i;
|
|
812
|
+
function resolveJsDocSources(sourcePaths, src, onWarn) {
|
|
813
|
+
if (src !== void 0 && src.length > 0) {
|
|
814
|
+
return src;
|
|
815
|
+
}
|
|
816
|
+
if (sourcePaths.some((p) => COMPILED_JS.test(p)) && onWarn) {
|
|
817
|
+
onWarn(
|
|
818
|
+
'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.'
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
return sourcePaths;
|
|
822
|
+
}
|
|
823
|
+
function assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn) {
|
|
824
|
+
if (jsDocResult.sourceFileCount === 0) {
|
|
825
|
+
throw new Error(
|
|
826
|
+
`No source files matched the explicit src paths: ${src.join(", ")}
|
|
827
|
+
Check the --src glob/path (or the \`src\` option). Without matching TypeScript sources, JSDoc tags such as @namespace and @hidden cannot be read.`
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
const isRenderable = (meta) => !meta.pivotTable && !meta.embeddable;
|
|
831
|
+
const missingConcrete = metas.filter((meta) => isRenderable(meta) && !meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
|
|
832
|
+
if (missingConcrete.length > 0) {
|
|
833
|
+
throw new Error(
|
|
834
|
+
`Explicit src paths did not include source declarations for discovered entities: ${missingConcrete.join(", ")}
|
|
835
|
+
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.`
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
if (onWarn) {
|
|
839
|
+
const missingAbstract = metas.filter((meta) => isRenderable(meta) && meta.abstract).map((meta) => meta.className).filter((className) => !jsDocResult.classNames.has(className));
|
|
840
|
+
if (missingAbstract.length > 0) {
|
|
841
|
+
onWarn(
|
|
842
|
+
`Abstract STI parent entities were not found in the explicit src paths: ${missingAbstract.join(", ")}
|
|
843
|
+
@hidden and @namespace tags for these entities will not be applied. Include their source files in --src to enable JSDoc tags for them.`
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
function errorMessages(err) {
|
|
849
|
+
const messages = [];
|
|
850
|
+
const seen = /* @__PURE__ */ new Set();
|
|
851
|
+
let current = err;
|
|
852
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
853
|
+
seen.add(current);
|
|
854
|
+
messages.push(current.message);
|
|
855
|
+
current = current.cause;
|
|
856
|
+
}
|
|
857
|
+
return messages;
|
|
858
|
+
}
|
|
859
|
+
function isMissingTsMorphSourceFile(err) {
|
|
860
|
+
return errorMessages(err).some((message) => message.includes("Source file") && message.includes("not found"));
|
|
861
|
+
}
|
|
862
|
+
async function loadEntityMetadataWithTsMorphFallback(originalOrm, effectiveOrm) {
|
|
863
|
+
try {
|
|
864
|
+
return await loadEntityMetadata(effectiveOrm);
|
|
865
|
+
} catch (err) {
|
|
866
|
+
const wasAutoInjected = originalOrm.metadataProvider === void 0 && effectiveOrm.metadataProvider !== void 0;
|
|
867
|
+
if (!wasAutoInjected || !isMissingTsMorphSourceFile(err)) {
|
|
868
|
+
throw err;
|
|
869
|
+
}
|
|
870
|
+
try {
|
|
871
|
+
return await loadEntityMetadata(originalOrm);
|
|
872
|
+
} catch {
|
|
873
|
+
throw err;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
457
877
|
async function generateMarkdown(options) {
|
|
458
|
-
const { orm, title = "Database Schema", description, src
|
|
459
|
-
const
|
|
460
|
-
const
|
|
461
|
-
const
|
|
878
|
+
const { orm, title = "Database Schema", description, src, onWarn } = options;
|
|
879
|
+
const effectiveOrm = await withTsMorphMetadataProvider(orm, onWarn);
|
|
880
|
+
const { metas, sourcePaths } = await loadEntityMetadataWithTsMorphFallback(orm, effectiveOrm);
|
|
881
|
+
const jsDocResult = loadJsDoc(resolveJsDocSources(sourcePaths, src, onWarn));
|
|
882
|
+
if (src !== void 0 && src.length > 0) {
|
|
883
|
+
assertExplicitJsDocSourceCoverage(metas, jsDocResult, src, onWarn);
|
|
884
|
+
}
|
|
885
|
+
const docModel = buildDocumentModel(metas, jsDocResult, title, description, onWarn);
|
|
462
886
|
return renderMarkdown(docModel);
|
|
463
887
|
}
|
|
464
888
|
|
|
465
889
|
// src/cli.ts
|
|
890
|
+
function toConfigImportSpecifier(configPath) {
|
|
891
|
+
return pathToFileURL(path2.resolve(configPath)).href;
|
|
892
|
+
}
|
|
893
|
+
function findNearestTsconfig(fromPath) {
|
|
894
|
+
let dir = path2.dirname(path2.resolve(fromPath));
|
|
895
|
+
for (; ; ) {
|
|
896
|
+
const candidate = path2.join(dir, "tsconfig.json");
|
|
897
|
+
if (syncFs.existsSync(candidate)) {
|
|
898
|
+
return candidate;
|
|
899
|
+
}
|
|
900
|
+
const parent = path2.dirname(dir);
|
|
901
|
+
if (parent === dir) {
|
|
902
|
+
return void 0;
|
|
903
|
+
}
|
|
904
|
+
dir = parent;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
async function loadOrmOptions(configPath, tsconfigPath) {
|
|
908
|
+
const isTypeScriptConfig = configPath.endsWith(".ts");
|
|
909
|
+
if (isTypeScriptConfig) {
|
|
910
|
+
let register;
|
|
911
|
+
try {
|
|
912
|
+
({ register } = await import("tsx/esm/api"));
|
|
913
|
+
} catch {
|
|
914
|
+
throw new Error('TypeScript config files require the "tsx" package.\nInstall it with: npm install -D tsx');
|
|
915
|
+
}
|
|
916
|
+
let tsconfig;
|
|
917
|
+
if (tsconfigPath !== void 0) {
|
|
918
|
+
tsconfig = path2.resolve(tsconfigPath);
|
|
919
|
+
if (!syncFs.existsSync(tsconfig)) {
|
|
920
|
+
throw new Error(`--tsconfig file not found: ${tsconfig}`);
|
|
921
|
+
}
|
|
922
|
+
} else {
|
|
923
|
+
tsconfig = findNearestTsconfig(configPath);
|
|
924
|
+
}
|
|
925
|
+
register(tsconfig !== void 0 ? { tsconfig } : {});
|
|
926
|
+
}
|
|
927
|
+
const configUrl = toConfigImportSpecifier(configPath);
|
|
928
|
+
let mod;
|
|
929
|
+
try {
|
|
930
|
+
mod = await import(
|
|
931
|
+
/* @vite-ignore */
|
|
932
|
+
configUrl
|
|
933
|
+
);
|
|
934
|
+
} catch (cause) {
|
|
935
|
+
if (isTypeScriptConfig) {
|
|
936
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
937
|
+
throw new Error(
|
|
938
|
+
`Failed to load TypeScript config.
|
|
939
|
+
${detail}
|
|
940
|
+
|
|
941
|
+
If this looks like a decorator/metadata error, the tsconfig applied to your entity files is likely missing "experimentalDecorators" / "emitDecoratorMetadata".
|
|
942
|
+
Make sure a tsconfig.json with those options sits next to your config file, or pass one explicitly with --tsconfig <path>.`,
|
|
943
|
+
{ cause }
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
throw cause;
|
|
947
|
+
}
|
|
948
|
+
if (mod.default === void 0) {
|
|
949
|
+
throw new Error("Config file must use a default export, e.g. `export default defineConfig({ ... })`.");
|
|
950
|
+
}
|
|
951
|
+
const config = mod.default;
|
|
952
|
+
if (typeof config === "function" || config instanceof Promise) {
|
|
953
|
+
throw new Error(
|
|
954
|
+
"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)."
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
958
|
+
throw new Error(
|
|
959
|
+
"Config file default export must be a configuration object, not a primitive value or array.\nExport a plain MikroORM options object instead."
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
const options = config;
|
|
963
|
+
const withPreferTs = isTypeScriptConfig && options.preferTs === void 0 ? { ...options, preferTs: true } : options;
|
|
964
|
+
return withPreferTs;
|
|
965
|
+
}
|
|
966
|
+
function formatErrorChain(err) {
|
|
967
|
+
const lines = [];
|
|
968
|
+
const seen = /* @__PURE__ */ new Set();
|
|
969
|
+
let current = err;
|
|
970
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
971
|
+
seen.add(current);
|
|
972
|
+
lines.push(current.message);
|
|
973
|
+
current = current.cause;
|
|
974
|
+
}
|
|
975
|
+
if (current !== void 0 && !(current instanceof Error)) {
|
|
976
|
+
lines.push(String(current));
|
|
977
|
+
}
|
|
978
|
+
return lines.map((line, i) => i === 0 ? line : ` \u21B3 caused by: ${line}`).join("\n");
|
|
979
|
+
}
|
|
980
|
+
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.";
|
|
981
|
+
function formatDiscoveryError(err) {
|
|
982
|
+
const chain = formatErrorChain(err);
|
|
983
|
+
return chain.includes("emitDecoratorMetadata") ? chain + REFLECTION_METADATA_HINT : chain;
|
|
984
|
+
}
|
|
985
|
+
function formatFileSystemError(cause) {
|
|
986
|
+
if (cause instanceof Error) {
|
|
987
|
+
const code = "code" in cause && typeof cause.code === "string" ? ` (${cause.code})` : "";
|
|
988
|
+
return `${cause.message}${code}`;
|
|
989
|
+
}
|
|
990
|
+
return String(cause);
|
|
991
|
+
}
|
|
992
|
+
async function writeMarkdownFile(outPath, markdown) {
|
|
993
|
+
try {
|
|
994
|
+
await fs.mkdir(path2.dirname(outPath), { recursive: true });
|
|
995
|
+
await fs.writeFile(outPath, markdown, "utf-8");
|
|
996
|
+
} catch (cause) {
|
|
997
|
+
throw new Error(`Cannot write output file: ${outPath}
|
|
998
|
+
${formatFileSystemError(cause)}`, { cause });
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
466
1001
|
async function run(opts) {
|
|
467
|
-
const configPath =
|
|
468
|
-
const outPath =
|
|
1002
|
+
const configPath = path2.resolve(opts.config);
|
|
1003
|
+
const outPath = path2.resolve(opts.out);
|
|
469
1004
|
let ormOptions;
|
|
470
1005
|
try {
|
|
471
|
-
|
|
472
|
-
ormOptions = mod.default ?? mod;
|
|
1006
|
+
ormOptions = await loadOrmOptions(configPath, opts.tsconfig);
|
|
473
1007
|
} catch (err) {
|
|
474
|
-
const hint = configPath.endsWith(".ts") ? "\nHint: TypeScript configs require tsx or ts-node:\n npx tsx ./node_modules/.bin/mikro-orm-markdown ..." : "";
|
|
475
1008
|
process.stderr.write(
|
|
476
1009
|
`Error: Cannot load config: ${configPath}
|
|
477
|
-
${err instanceof Error ? err.message : String(err)}
|
|
1010
|
+
${err instanceof Error ? err.message : String(err)}
|
|
478
1011
|
`
|
|
479
1012
|
);
|
|
480
1013
|
process.exit(1);
|
|
@@ -484,28 +1017,57 @@ ${err instanceof Error ? err.message : String(err)}${hint}
|
|
|
484
1017
|
markdown = await generateMarkdown({
|
|
485
1018
|
orm: ormOptions,
|
|
486
1019
|
title: opts.title,
|
|
487
|
-
|
|
488
|
-
...opts.
|
|
1020
|
+
...opts.description !== void 0 && { description: opts.description },
|
|
1021
|
+
...opts.src !== void 0 && { src: opts.src },
|
|
1022
|
+
onWarn: (message) => void process.stderr.write(`Warning: ${message}
|
|
1023
|
+
`)
|
|
489
1024
|
});
|
|
490
1025
|
} catch (err) {
|
|
491
|
-
|
|
492
|
-
process.stderr.write(`Error: ${msg}
|
|
1026
|
+
process.stderr.write(`Error: ${formatDiscoveryError(err)}
|
|
493
1027
|
`);
|
|
494
1028
|
process.exit(1);
|
|
495
1029
|
}
|
|
496
|
-
|
|
497
|
-
|
|
1030
|
+
try {
|
|
1031
|
+
await writeMarkdownFile(outPath, markdown);
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1034
|
+
`);
|
|
1035
|
+
process.exit(1);
|
|
1036
|
+
}
|
|
1037
|
+
process.stdout.write(`\u2713 Written to ${path2.relative(process.cwd(), outPath)}
|
|
498
1038
|
`);
|
|
499
1039
|
}
|
|
500
1040
|
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(
|
|
501
|
-
"
|
|
502
|
-
"
|
|
503
|
-
|
|
504
|
-
|
|
1041
|
+
"--tsconfig <path>",
|
|
1042
|
+
"tsconfig.json to use when loading a .ts config (defaults to the nearest one beside the config file)"
|
|
1043
|
+
).option(
|
|
1044
|
+
"--src <paths...>",
|
|
1045
|
+
"Source .ts file globs to read JSDoc from when entities run from compiled .js (comments are stripped at build time)"
|
|
505
1046
|
).action(run);
|
|
506
|
-
|
|
507
|
-
|
|
1047
|
+
function isDirectCliExecution() {
|
|
1048
|
+
const entryPoint = process.argv[1];
|
|
1049
|
+
if (entryPoint === void 0) {
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
try {
|
|
1053
|
+
return syncFs.realpathSync(path2.resolve(entryPoint)) === syncFs.realpathSync(fileURLToPath(import.meta.url));
|
|
1054
|
+
} catch {
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (isDirectCliExecution()) {
|
|
1059
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1060
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
508
1061
|
`);
|
|
509
|
-
|
|
510
|
-
});
|
|
1062
|
+
process.exit(1);
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
export {
|
|
1066
|
+
findNearestTsconfig,
|
|
1067
|
+
formatDiscoveryError,
|
|
1068
|
+
formatErrorChain,
|
|
1069
|
+
loadOrmOptions,
|
|
1070
|
+
toConfigImportSpecifier,
|
|
1071
|
+
writeMarkdownFile
|
|
1072
|
+
};
|
|
511
1073
|
//# sourceMappingURL=cli.js.map
|