mikro-orm-markdown 0.1.0-alpha.2 → 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 +248 -41
- package/README.md +248 -41
- package/SECURITY.md +28 -0
- package/dist/cli.cjs +670 -134
- 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 +650 -136
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +500 -119
- 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 +489 -119
- 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,47 @@ 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) {
|
|
221
|
-
return "integer";
|
|
371
|
+
return Array.from({ length: fieldNameCount }, () => "integer");
|
|
222
372
|
}
|
|
223
|
-
const
|
|
224
|
-
|
|
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);
|
|
225
388
|
}
|
|
226
389
|
function buildConstraints(meta) {
|
|
227
390
|
const result = [];
|
|
@@ -229,7 +392,7 @@ function buildConstraints(meta) {
|
|
|
229
392
|
const props = idx.properties;
|
|
230
393
|
result.push({
|
|
231
394
|
type: "index",
|
|
232
|
-
properties:
|
|
395
|
+
properties: resolveConstraintProperties(meta, props),
|
|
233
396
|
...idx.name !== void 0 && { name: idx.name }
|
|
234
397
|
});
|
|
235
398
|
}
|
|
@@ -237,7 +400,7 @@ function buildConstraints(meta) {
|
|
|
237
400
|
const props = uniq.properties;
|
|
238
401
|
result.push({
|
|
239
402
|
type: "unique",
|
|
240
|
-
properties:
|
|
403
|
+
properties: resolveConstraintProperties(meta, props),
|
|
241
404
|
...uniq.name !== void 0 && { name: uniq.name }
|
|
242
405
|
});
|
|
243
406
|
}
|
|
@@ -254,6 +417,19 @@ function buildConstraints(meta) {
|
|
|
254
417
|
}
|
|
255
418
|
return result;
|
|
256
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
|
+
}
|
|
257
433
|
function buildRelationEdges(metas) {
|
|
258
434
|
const edges = [];
|
|
259
435
|
for (const meta of metas) {
|
|
@@ -272,6 +448,9 @@ function buildRelationEdges(metas) {
|
|
|
272
448
|
function buildEdge(fromEntity, prop) {
|
|
273
449
|
const isNullable = prop.nullable === true;
|
|
274
450
|
if (prop.kind === ReferenceKind.MANY_TO_ONE) {
|
|
451
|
+
if (prop.type === fromEntity) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
275
454
|
return {
|
|
276
455
|
fromEntity,
|
|
277
456
|
toEntity: prop.type,
|
|
@@ -303,14 +482,16 @@ function buildEdge(fromEntity, prop) {
|
|
|
303
482
|
function renderErDiagram(model) {
|
|
304
483
|
const lines = ["erDiagram"];
|
|
305
484
|
for (const entity of model.entities) {
|
|
306
|
-
lines.push(` ${entity.className} {`);
|
|
485
|
+
lines.push(` ${toMermaidIdentifier(entity.className)} {`);
|
|
307
486
|
for (const col of entity.columns) {
|
|
308
487
|
lines.push(` ${renderColumnLine(col)}`);
|
|
309
488
|
}
|
|
310
489
|
lines.push(" }");
|
|
311
490
|
}
|
|
312
491
|
for (const rel of model.relations) {
|
|
313
|
-
lines.push(
|
|
492
|
+
lines.push(
|
|
493
|
+
` ${toMermaidIdentifier(rel.fromEntity)} ${rel.fromCardinality}--${rel.toCardinality} ${toMermaidIdentifier(rel.toEntity)} : "${escapeMermaidQuotedText(rel.label)}"`
|
|
494
|
+
);
|
|
314
495
|
}
|
|
315
496
|
return lines.join("\n");
|
|
316
497
|
}
|
|
@@ -318,8 +499,6 @@ function renderColumnLine(col) {
|
|
|
318
499
|
let qualifier = "";
|
|
319
500
|
if (col.isPrimary) {
|
|
320
501
|
qualifier = " PK";
|
|
321
|
-
} else if (col.isForeignKey) {
|
|
322
|
-
qualifier = " FK";
|
|
323
502
|
} else if (col.isUnique) {
|
|
324
503
|
qualifier = " UK";
|
|
325
504
|
}
|
|
@@ -330,27 +509,38 @@ function renderColumnLine(col) {
|
|
|
330
509
|
comment = "discriminator";
|
|
331
510
|
} else if (col.embeddedIn !== void 0) {
|
|
332
511
|
comment = `[${col.embeddedIn}]`;
|
|
333
|
-
} else if (col.
|
|
334
|
-
comment =
|
|
512
|
+
} else if (col.isSelfReference) {
|
|
513
|
+
comment = "self-ref";
|
|
335
514
|
}
|
|
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, "_");
|
|
515
|
+
const commentStr = comment !== void 0 ? ` "${escapeMermaidQuotedText(comment)}"` : "";
|
|
516
|
+
return `${toMermaidIdentifier(col.type)} ${toMermaidIdentifier(col.fieldName)}${qualifier}${commentStr}`;
|
|
341
517
|
}
|
|
342
518
|
|
|
343
519
|
// src/model/build.ts
|
|
344
|
-
|
|
345
|
-
|
|
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
|
+
}
|
|
346
531
|
const enrichedByClass = /* @__PURE__ */ new Map();
|
|
347
532
|
for (const model of diagramEntities) {
|
|
348
533
|
const jsDoc = jsDocResult.entities.get(model.className);
|
|
349
534
|
if (jsDoc?.hidden) {
|
|
350
535
|
continue;
|
|
351
536
|
}
|
|
352
|
-
const
|
|
353
|
-
|
|
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 });
|
|
354
544
|
}
|
|
355
545
|
const groupNames = /* @__PURE__ */ new Set();
|
|
356
546
|
let anyUntagged = false;
|
|
@@ -391,6 +581,65 @@ function buildDocumentModel(metas, jsDocResult, title, description) {
|
|
|
391
581
|
});
|
|
392
582
|
return { title, groups, ...description !== void 0 && { description } };
|
|
393
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
|
+
}
|
|
394
643
|
function hasNoNamespaceTags(jsDoc) {
|
|
395
644
|
if (!jsDoc) {
|
|
396
645
|
return true;
|
|
@@ -416,19 +665,55 @@ function belongsToGroupForText(jsDoc, groupName, isDefault) {
|
|
|
416
665
|
return jsDoc.namespaces.includes(groupName) || jsDoc.describeNamespaces.includes(groupName);
|
|
417
666
|
}
|
|
418
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
|
+
|
|
419
688
|
// src/render/markdown.ts
|
|
420
689
|
function renderMarkdown(docModel) {
|
|
421
|
-
const sections = [`# ${docModel.title}`];
|
|
690
|
+
const sections = [`# ${escapeMarkdownInline(docModel.title)}`];
|
|
422
691
|
if (docModel.description) {
|
|
423
|
-
sections.push(docModel.description);
|
|
692
|
+
sections.push(escapeMarkdownParagraph(docModel.description));
|
|
693
|
+
}
|
|
694
|
+
if (docModel.groups.length > 1) {
|
|
695
|
+
sections.push(renderTableOfContents(docModel.groups));
|
|
424
696
|
}
|
|
425
697
|
for (const group of docModel.groups) {
|
|
426
698
|
sections.push(renderGroupSection(group));
|
|
427
699
|
}
|
|
428
700
|
return sections.join("\n\n");
|
|
429
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
|
+
}
|
|
430
715
|
function renderGroupSection(group) {
|
|
431
|
-
const parts = [`## ${group.name}`];
|
|
716
|
+
const parts = [`## ${escapeMarkdownInline(group.name)}`];
|
|
432
717
|
if (group.erdEntities.length > 0) {
|
|
433
718
|
const diagramModel = {
|
|
434
719
|
entities: group.erdEntities.map((e) => e.model),
|
|
@@ -442,14 +727,18 @@ function renderGroupSection(group) {
|
|
|
442
727
|
return parts.join("\n\n");
|
|
443
728
|
}
|
|
444
729
|
function renderEntitySection(entity) {
|
|
445
|
-
const parts = [`### ${entity.model.className}`];
|
|
730
|
+
const parts = [`### ${escapeMarkdownInline(entity.model.className)}`];
|
|
731
|
+
parts.push(`*Table: ${renderMarkdownInlineCode(entity.model.tableName)}*`);
|
|
446
732
|
if (entity.jsDoc?.description) {
|
|
447
|
-
parts.push(
|
|
733
|
+
parts.push(renderMarkdownBlockQuote(entity.jsDoc.description));
|
|
448
734
|
}
|
|
449
735
|
if (entity.model.discriminatorColumn) {
|
|
450
|
-
parts.push(`*STI root \u2014 discriminator column:
|
|
736
|
+
parts.push(`*STI root \u2014 discriminator column: ${renderMarkdownInlineCode(entity.model.discriminatorColumn)}*`);
|
|
451
737
|
} else if (entity.model.extendsEntity) {
|
|
452
|
-
|
|
738
|
+
const discValue = entity.model.discriminatorValue !== void 0 ? `, discriminator value: ${renderMarkdownInlineCode(entity.model.discriminatorValue)}` : "";
|
|
739
|
+
parts.push(
|
|
740
|
+
`*Extends ${renderMarkdownInlineCode(entity.model.extendsEntity)} (Single Table Inheritance${discValue})*`
|
|
741
|
+
);
|
|
453
742
|
}
|
|
454
743
|
if (entity.model.columns.length > 0) {
|
|
455
744
|
parts.push(renderColumnTable(entity));
|
|
@@ -457,6 +746,10 @@ function renderEntitySection(entity) {
|
|
|
457
746
|
if (entity.model.constraints.length > 0) {
|
|
458
747
|
parts.push(renderConstraints(entity.model.constraints));
|
|
459
748
|
}
|
|
749
|
+
const computedColumns = entity.model.columns.filter((col) => col.formula !== void 0);
|
|
750
|
+
if (computedColumns.length > 0) {
|
|
751
|
+
parts.push(renderComputedColumns(computedColumns));
|
|
752
|
+
}
|
|
460
753
|
return parts.join("\n\n");
|
|
461
754
|
}
|
|
462
755
|
function renderColumnTable(entity) {
|
|
@@ -465,24 +758,27 @@ function renderColumnTable(entity) {
|
|
|
465
758
|
const rows = entity.model.columns.map((col) => {
|
|
466
759
|
const key = resolveColumnKey(col);
|
|
467
760
|
const nullable = col.isNullable && !col.isPrimary ? "Y" : "";
|
|
468
|
-
const
|
|
469
|
-
|
|
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)} |`;
|
|
470
765
|
});
|
|
471
766
|
return [header, sep, ...rows].join("\n");
|
|
472
767
|
}
|
|
473
768
|
function resolveColumnKey(col) {
|
|
769
|
+
const fkKey = col.fieldName !== col.propName ? `FK (${col.propName})` : "FK";
|
|
770
|
+
if (col.isPrimary && col.isForeignKey) {
|
|
771
|
+
return `PK, ${fkKey}`;
|
|
772
|
+
}
|
|
474
773
|
if (col.isPrimary) {
|
|
475
774
|
return "PK";
|
|
476
775
|
}
|
|
477
776
|
if (col.isForeignKey) {
|
|
478
|
-
return
|
|
777
|
+
return fkKey;
|
|
479
778
|
}
|
|
480
779
|
if (col.isUnique) {
|
|
481
780
|
return "UK";
|
|
482
781
|
}
|
|
483
|
-
if (col.formula !== void 0) {
|
|
484
|
-
return `formula: ${col.formula}`;
|
|
485
|
-
}
|
|
486
782
|
if (col.isDiscriminator) {
|
|
487
783
|
return "discriminator";
|
|
488
784
|
}
|
|
@@ -491,43 +787,227 @@ function resolveColumnKey(col) {
|
|
|
491
787
|
}
|
|
492
788
|
return "";
|
|
493
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
|
+
}
|
|
494
796
|
function renderConstraints(constraints) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const
|
|
797
|
+
return renderBulletSection("**Constraints:**", constraints, (c) => {
|
|
798
|
+
const name = c.name ? ` ${renderMarkdownInlineCode(c.name)}` : "";
|
|
799
|
+
const properties = c.properties.map(escapeMarkdownInline).join(", ");
|
|
498
800
|
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 ?? ""}\``);
|
|
801
|
+
return `- Index${name}: (${properties})`;
|
|
504
802
|
}
|
|
505
|
-
|
|
506
|
-
|
|
803
|
+
if (c.type === "unique") {
|
|
804
|
+
return `- Unique${name}: (${properties})`;
|
|
805
|
+
}
|
|
806
|
+
return `- Check${name}: ${renderMarkdownInlineCode(c.expression ?? "")}`;
|
|
807
|
+
});
|
|
507
808
|
}
|
|
508
809
|
|
|
509
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
|
+
}
|
|
510
877
|
async function generateMarkdown(options) {
|
|
511
|
-
const { orm, title = "Database Schema", description, src
|
|
512
|
-
const
|
|
513
|
-
const
|
|
514
|
-
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);
|
|
515
886
|
return renderMarkdown(docModel);
|
|
516
887
|
}
|
|
517
888
|
|
|
518
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
|
+
}
|
|
519
1001
|
async function run(opts) {
|
|
520
|
-
const configPath =
|
|
521
|
-
const outPath =
|
|
1002
|
+
const configPath = path2.resolve(opts.config);
|
|
1003
|
+
const outPath = path2.resolve(opts.out);
|
|
522
1004
|
let ormOptions;
|
|
523
1005
|
try {
|
|
524
|
-
|
|
525
|
-
ormOptions = mod.default ?? mod;
|
|
1006
|
+
ormOptions = await loadOrmOptions(configPath, opts.tsconfig);
|
|
526
1007
|
} 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
1008
|
process.stderr.write(
|
|
529
1009
|
`Error: Cannot load config: ${configPath}
|
|
530
|
-
${err instanceof Error ? err.message : String(err)}
|
|
1010
|
+
${err instanceof Error ? err.message : String(err)}
|
|
531
1011
|
`
|
|
532
1012
|
);
|
|
533
1013
|
process.exit(1);
|
|
@@ -537,23 +1017,57 @@ ${err instanceof Error ? err.message : String(err)}${hint}
|
|
|
537
1017
|
markdown = await generateMarkdown({
|
|
538
1018
|
orm: ormOptions,
|
|
539
1019
|
title: opts.title,
|
|
540
|
-
|
|
541
|
-
...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
|
+
`)
|
|
542
1024
|
});
|
|
543
1025
|
} catch (err) {
|
|
544
|
-
|
|
545
|
-
|
|
1026
|
+
process.stderr.write(`Error: ${formatDiscoveryError(err)}
|
|
1027
|
+
`);
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
}
|
|
1030
|
+
try {
|
|
1031
|
+
await writeMarkdownFile(outPath, markdown);
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
546
1034
|
`);
|
|
547
1035
|
process.exit(1);
|
|
548
1036
|
}
|
|
549
|
-
|
|
550
|
-
process.stdout.write(`\u2713 Written to ${path.relative(process.cwd(), outPath)}
|
|
1037
|
+
process.stdout.write(`\u2713 Written to ${path2.relative(process.cwd(), outPath)}
|
|
551
1038
|
`);
|
|
552
1039
|
}
|
|
553
|
-
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(
|
|
554
|
-
|
|
555
|
-
|
|
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(
|
|
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)"
|
|
1046
|
+
).action(run);
|
|
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)}
|
|
556
1061
|
`);
|
|
557
|
-
|
|
558
|
-
});
|
|
1062
|
+
process.exit(1);
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
export {
|
|
1066
|
+
findNearestTsconfig,
|
|
1067
|
+
formatDiscoveryError,
|
|
1068
|
+
formatErrorChain,
|
|
1069
|
+
loadOrmOptions,
|
|
1070
|
+
toConfigImportSpecifier,
|
|
1071
|
+
writeMarkdownFile
|
|
1072
|
+
};
|
|
559
1073
|
//# sourceMappingURL=cli.js.map
|