@matserdam/prisma-neighborhood 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -4
- package/dist/_tests_/traverser.test.d.ts +2 -2
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli.js +247 -89
- package/dist/index.js +248 -90
- package/dist/parser/index.d.ts +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/schema-parser.d.ts +3 -1
- package/dist/parser/schema-parser.d.ts.map +1 -1
- package/dist/parser/types.d.ts +28 -2
- package/dist/parser/types.d.ts.map +1 -1
- package/dist/renderer/mermaid-erd.d.ts +5 -5
- package/dist/renderer/mermaid-erd.d.ts.map +1 -1
- package/dist/renderer/mermaid-renderer.d.ts +7 -6
- package/dist/renderer/mermaid-renderer.d.ts.map +1 -1
- package/dist/renderer/types.d.ts +6 -6
- package/dist/renderer/types.d.ts.map +1 -1
- package/dist/renderer/vector-renderer.d.ts +2 -2
- package/dist/renderer/vector-renderer.d.ts.map +1 -1
- package/dist/traversal/entity-traverser.d.ts +37 -0
- package/dist/traversal/entity-traverser.d.ts.map +1 -0
- package/dist/traversal/index.d.ts +3 -3
- package/dist/traversal/index.d.ts.map +1 -1
- package/dist/traversal/types.d.ts +23 -13
- package/dist/traversal/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# prisma-neighbourhood
|
|
2
2
|
|
|
3
|
-
Generate focused ERD diagrams from a Prisma schema by starting from
|
|
3
|
+
Generate focused ERD diagrams from a Prisma schema by starting from any entity (model, view, or enum) and traversing relationships up to a configurable depth.
|
|
4
4
|
|
|
5
5
|
The npm package is **`@matserdam/prisma-neighborhood`** and it exposes two CLI commands:
|
|
6
6
|
|
|
@@ -20,6 +20,12 @@ bunx @matserdam/prisma-neighborhood -s ./prisma/schema.prisma -m User
|
|
|
20
20
|
# Limit to direct relationships only (depth 1)
|
|
21
21
|
bunx @matserdam/prisma-neighborhood -s ./prisma/schema.prisma -m User -d 1
|
|
22
22
|
|
|
23
|
+
# Start from a view (shows view + related enums/models)
|
|
24
|
+
bunx @matserdam/prisma-neighborhood -s ./prisma/schema.prisma -m UserSummary -d 2
|
|
25
|
+
|
|
26
|
+
# Start from an enum (shows enum + all models/views using it)
|
|
27
|
+
bunx @matserdam/prisma-neighborhood -s ./prisma/schema.prisma -m UserRole -d 2
|
|
28
|
+
|
|
23
29
|
# Write Mermaid to a file (use .mmd or .md)
|
|
24
30
|
bunx @matserdam/prisma-neighborhood -s ./prisma/schema.prisma -m User -o erd.mmd
|
|
25
31
|
|
|
@@ -50,7 +56,7 @@ prisma-hood -s ./prisma/schema.prisma -m User
|
|
|
50
56
|
| Option | Alias | Description | Default |
|
|
51
57
|
|--------|-------|-------------|---------|
|
|
52
58
|
| `--schema <path>` | `-s` | Path to the Prisma schema file | required |
|
|
53
|
-
| `--model <name>` | `-m` |
|
|
59
|
+
| `--model <name>` | `-m` | Entity to start traversal from (model, view, or enum) | required |
|
|
54
60
|
| `--depth <n>` | `-d` | Relationship levels to traverse | `3` |
|
|
55
61
|
| `--renderer <name>` | `-r` | Renderer to use | `vector` |
|
|
56
62
|
| `--output <file>` | `-o` | Write to a file instead of stdout | stdout |
|
|
@@ -69,6 +75,32 @@ The output format is determined by the file extension:
|
|
|
69
75
|
| `.png` | PNG image |
|
|
70
76
|
| `.pdf` | PDF |
|
|
71
77
|
|
|
72
|
-
##
|
|
78
|
+
## Entity types
|
|
79
|
+
|
|
80
|
+
### Models
|
|
81
|
+
Standard Prisma models are rendered as plain ERD entities.
|
|
82
|
+
|
|
83
|
+
### Views
|
|
84
|
+
Views (requires `previewFeatures = ["views"]`) are rendered with a `[view]` prefix:
|
|
85
|
+
```
|
|
86
|
+
"[view] UserSummary" {
|
|
87
|
+
Int id UK
|
|
88
|
+
String email
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Enums
|
|
93
|
+
Enums are rendered with an `[enum]` prefix, with the enum name as the type for each value:
|
|
94
|
+
```
|
|
95
|
+
"[enum] UserRole" {
|
|
96
|
+
UserRole ADMIN
|
|
97
|
+
UserRole USER
|
|
98
|
+
UserRole GUEST
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Traversal behavior
|
|
73
103
|
|
|
74
|
-
|
|
104
|
+
- **From a model**: Traverses relations to other models/views, and enum fields to enums
|
|
105
|
+
- **From a view**: Traverses relations to models/views, and enum fields to enums
|
|
106
|
+
- **From an enum**: Finds all models/views using this enum, then traverses their relations
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Tests for the
|
|
3
|
-
* Tests cover BFS traversal
|
|
2
|
+
* @fileoverview Tests for the entity traverser.
|
|
3
|
+
* Tests cover BFS traversal of models, views, and enums with depth limiting.
|
|
4
4
|
*/
|
|
5
5
|
export {};
|
|
6
6
|
//# sourceMappingURL=traverser.test.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/cli/commands.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmCpC;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,OAAO,
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/cli/commands.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmCpC;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,OAAO,CA8BvC;AAqID;;;;GAIG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1D"}
|
package/dist/cli.js
CHANGED
|
@@ -2155,6 +2155,48 @@ function isPrimaryKeyField(field) {
|
|
|
2155
2155
|
function isUniqueField(field) {
|
|
2156
2156
|
return field.isUnique;
|
|
2157
2157
|
}
|
|
2158
|
+
function extractViewNames(schemaContent) {
|
|
2159
|
+
const viewNames = new Set;
|
|
2160
|
+
const viewRegex = /^\s*view\s+(\w+)\s*\{/gm;
|
|
2161
|
+
const matches = schemaContent.matchAll(viewRegex);
|
|
2162
|
+
for (const match of matches) {
|
|
2163
|
+
viewNames.add(match[1]);
|
|
2164
|
+
}
|
|
2165
|
+
return viewNames;
|
|
2166
|
+
}
|
|
2167
|
+
function parseFieldsAndRelations(dmmfEntity, entityLookup) {
|
|
2168
|
+
const fields = [];
|
|
2169
|
+
const relations = [];
|
|
2170
|
+
for (const dmmfField of dmmfEntity.fields) {
|
|
2171
|
+
const field = {
|
|
2172
|
+
name: dmmfField.name,
|
|
2173
|
+
type: dmmfField.type,
|
|
2174
|
+
isRequired: dmmfField.isRequired,
|
|
2175
|
+
isList: dmmfField.isList,
|
|
2176
|
+
isPrimaryKey: isPrimaryKeyField(dmmfField),
|
|
2177
|
+
isUnique: isUniqueField(dmmfField),
|
|
2178
|
+
isRelation: isRelationField(dmmfField)
|
|
2179
|
+
};
|
|
2180
|
+
fields.push(field);
|
|
2181
|
+
if (isRelationField(dmmfField)) {
|
|
2182
|
+
const relatedEntity = entityLookup.get(dmmfField.type);
|
|
2183
|
+
const relatedField = relatedEntity?.fields.find((f) => f.relationName === dmmfField.relationName && f.name !== dmmfField.name);
|
|
2184
|
+
const relationType = determineRelationType({ isList: dmmfField.isList, isRequired: dmmfField.isRequired }, relatedField ? {
|
|
2185
|
+
isList: relatedField.isList,
|
|
2186
|
+
isRequired: relatedField.isRequired
|
|
2187
|
+
} : undefined);
|
|
2188
|
+
const isOwner = dmmfField.relationFromFields !== undefined && dmmfField.relationFromFields.length > 0;
|
|
2189
|
+
const relation = {
|
|
2190
|
+
relatedModel: dmmfField.type,
|
|
2191
|
+
type: relationType,
|
|
2192
|
+
fieldName: dmmfField.name,
|
|
2193
|
+
isOwner
|
|
2194
|
+
};
|
|
2195
|
+
relations.push(relation);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
return { fields, relations };
|
|
2199
|
+
}
|
|
2158
2200
|
async function parseSchema(options) {
|
|
2159
2201
|
const { schemaPath } = options;
|
|
2160
2202
|
try {
|
|
@@ -2168,53 +2210,44 @@ async function parseSchema(options) {
|
|
|
2168
2210
|
};
|
|
2169
2211
|
}
|
|
2170
2212
|
const dmmf = await import_internals.getDMMF({ datamodel: schemaContent });
|
|
2213
|
+
const viewNamesFromSchema = extractViewNames(schemaContent);
|
|
2171
2214
|
const dmmfModels = dmmf.datamodel.models;
|
|
2172
|
-
const
|
|
2215
|
+
const dmmfEnums = dmmf.datamodel.enums ?? [];
|
|
2216
|
+
const entityLookup = new Map;
|
|
2173
2217
|
for (const model of dmmfModels) {
|
|
2174
|
-
|
|
2218
|
+
entityLookup.set(model.name, model);
|
|
2175
2219
|
}
|
|
2176
2220
|
const models = new Map;
|
|
2221
|
+
const views = new Map;
|
|
2177
2222
|
for (const dmmfModel of dmmfModels) {
|
|
2178
|
-
const fields =
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
isRequired: dmmfField.isRequired,
|
|
2185
|
-
isList: dmmfField.isList,
|
|
2186
|
-
isPrimaryKey: isPrimaryKeyField(dmmfField),
|
|
2187
|
-
isUnique: isUniqueField(dmmfField),
|
|
2188
|
-
isRelation: isRelationField(dmmfField)
|
|
2223
|
+
const { fields, relations } = parseFieldsAndRelations(dmmfModel, entityLookup);
|
|
2224
|
+
if (viewNamesFromSchema.has(dmmfModel.name)) {
|
|
2225
|
+
const view = {
|
|
2226
|
+
name: dmmfModel.name,
|
|
2227
|
+
fields,
|
|
2228
|
+
relations
|
|
2189
2229
|
};
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
const isOwner = dmmfField.relationFromFields !== undefined && dmmfField.relationFromFields.length > 0;
|
|
2199
|
-
const relation = {
|
|
2200
|
-
relatedModel: dmmfField.type,
|
|
2201
|
-
type: relationType,
|
|
2202
|
-
fieldName: dmmfField.name,
|
|
2203
|
-
isOwner
|
|
2204
|
-
};
|
|
2205
|
-
relations.push(relation);
|
|
2206
|
-
}
|
|
2230
|
+
views.set(view.name, view);
|
|
2231
|
+
} else {
|
|
2232
|
+
const model = {
|
|
2233
|
+
name: dmmfModel.name,
|
|
2234
|
+
fields,
|
|
2235
|
+
relations
|
|
2236
|
+
};
|
|
2237
|
+
models.set(model.name, model);
|
|
2207
2238
|
}
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2239
|
+
}
|
|
2240
|
+
const enums = new Map;
|
|
2241
|
+
for (const dmmfEnum of dmmfEnums) {
|
|
2242
|
+
const enumDef = {
|
|
2243
|
+
name: dmmfEnum.name,
|
|
2244
|
+
values: dmmfEnum.values.map((v) => v.name)
|
|
2212
2245
|
};
|
|
2213
|
-
|
|
2246
|
+
enums.set(enumDef.name, enumDef);
|
|
2214
2247
|
}
|
|
2215
2248
|
return {
|
|
2216
2249
|
success: true,
|
|
2217
|
-
schema: { models }
|
|
2250
|
+
schema: { models, views, enums }
|
|
2218
2251
|
};
|
|
2219
2252
|
} catch (error) {
|
|
2220
2253
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
@@ -2230,43 +2263,88 @@ var RELATION_SYMBOLS = {
|
|
|
2230
2263
|
ONE_TO_MANY: "||--o{",
|
|
2231
2264
|
MANY_TO_MANY: "}o--o{"
|
|
2232
2265
|
};
|
|
2233
|
-
|
|
2266
|
+
var ENUM_USAGE_SYMBOL = "}o--||";
|
|
2267
|
+
function renderModelOrView(entity, isView, lines) {
|
|
2268
|
+
const displayName = isView ? `"[view] ${entity.name}"` : entity.name;
|
|
2269
|
+
lines.push(` ${displayName} {`);
|
|
2270
|
+
for (const field of entity.fields) {
|
|
2271
|
+
if (field.isRelation) {
|
|
2272
|
+
continue;
|
|
2273
|
+
}
|
|
2274
|
+
const modifiers = [];
|
|
2275
|
+
if (field.isPrimaryKey) {
|
|
2276
|
+
modifiers.push("PK");
|
|
2277
|
+
}
|
|
2278
|
+
if (field.isUnique && !field.isPrimaryKey) {
|
|
2279
|
+
modifiers.push("UK");
|
|
2280
|
+
}
|
|
2281
|
+
const modifierStr = modifiers.length > 0 ? ` ${modifiers.join(",")}` : "";
|
|
2282
|
+
lines.push(` ${field.type} ${field.name}${modifierStr}`);
|
|
2283
|
+
}
|
|
2284
|
+
lines.push(" }");
|
|
2285
|
+
}
|
|
2286
|
+
function renderEnum(enumDef, lines) {
|
|
2287
|
+
lines.push(` "[enum] ${enumDef.name}" {`);
|
|
2288
|
+
for (const value of enumDef.values) {
|
|
2289
|
+
lines.push(` ${enumDef.name} ${value}`);
|
|
2290
|
+
}
|
|
2291
|
+
lines.push(" }");
|
|
2292
|
+
}
|
|
2293
|
+
function renderMermaidErd(entities) {
|
|
2234
2294
|
const lines = ["erDiagram"];
|
|
2235
2295
|
const renderedRelations = new Set;
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
}
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
const modifierStr = modifiers.length > 0 ? ` ${modifiers.join(",")}` : "";
|
|
2250
|
-
lines.push(` ${field.type} ${field.name}${modifierStr}`);
|
|
2296
|
+
const modelViewNames = new Set;
|
|
2297
|
+
const enumNames = new Set;
|
|
2298
|
+
const displayNames = new Map;
|
|
2299
|
+
for (const { entity, kind } of entities) {
|
|
2300
|
+
if (kind === "enum") {
|
|
2301
|
+
enumNames.add(entity.name);
|
|
2302
|
+
displayNames.set(entity.name, `"[enum] ${entity.name}"`);
|
|
2303
|
+
} else if (kind === "view") {
|
|
2304
|
+
modelViewNames.add(entity.name);
|
|
2305
|
+
displayNames.set(entity.name, `"[view] ${entity.name}"`);
|
|
2306
|
+
} else {
|
|
2307
|
+
modelViewNames.add(entity.name);
|
|
2308
|
+
displayNames.set(entity.name, entity.name);
|
|
2251
2309
|
}
|
|
2252
|
-
lines.push(" }");
|
|
2253
2310
|
}
|
|
2254
|
-
const
|
|
2255
|
-
for (const {
|
|
2256
|
-
|
|
2257
|
-
|
|
2311
|
+
const getDisplayName = (name) => displayNames.get(name) ?? name;
|
|
2312
|
+
for (const { entity, kind } of entities) {
|
|
2313
|
+
if (kind === "enum") {
|
|
2314
|
+
renderEnum(entity, lines);
|
|
2315
|
+
} else {
|
|
2316
|
+
renderModelOrView(entity, kind === "view", lines);
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
for (const { entity, kind } of entities) {
|
|
2320
|
+
if (kind === "enum") {
|
|
2321
|
+
continue;
|
|
2322
|
+
}
|
|
2323
|
+
const modelOrView = entity;
|
|
2324
|
+
for (const relation of modelOrView.relations) {
|
|
2325
|
+
if (!modelViewNames.has(relation.relatedModel)) {
|
|
2258
2326
|
continue;
|
|
2259
2327
|
}
|
|
2260
|
-
const sortedNames = [
|
|
2261
|
-
const relationKey =
|
|
2328
|
+
const sortedNames = [entity.name, relation.relatedModel].sort();
|
|
2329
|
+
const relationKey = `model:${sortedNames[0]}-${sortedNames[1]}`;
|
|
2262
2330
|
if (renderedRelations.has(relationKey)) {
|
|
2263
2331
|
continue;
|
|
2264
2332
|
}
|
|
2265
2333
|
const symbol = RELATION_SYMBOLS[relation.type];
|
|
2266
|
-
|
|
2267
|
-
lines.push(` ${model.name} ${symbol} ${relation.relatedModel} : "${label}"`);
|
|
2334
|
+
lines.push(` ${getDisplayName(entity.name)} ${symbol} ${getDisplayName(relation.relatedModel)} : "${relation.fieldName}"`);
|
|
2268
2335
|
renderedRelations.add(relationKey);
|
|
2269
2336
|
}
|
|
2337
|
+
for (const field of modelOrView.fields) {
|
|
2338
|
+
if (!enumNames.has(field.type)) {
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2341
|
+
const enumRelationKey = `enum:${entity.name}-${field.type}-${field.name}`;
|
|
2342
|
+
if (renderedRelations.has(enumRelationKey)) {
|
|
2343
|
+
continue;
|
|
2344
|
+
}
|
|
2345
|
+
lines.push(` ${getDisplayName(entity.name)} ${ENUM_USAGE_SYMBOL} ${getDisplayName(field.type)} : "${field.name}"`);
|
|
2346
|
+
renderedRelations.add(enumRelationKey);
|
|
2347
|
+
}
|
|
2270
2348
|
}
|
|
2271
2349
|
return lines.join(`
|
|
2272
2350
|
`);
|
|
@@ -2328,8 +2406,8 @@ async function runMermaidCli(mermaidContent, options) {
|
|
|
2328
2406
|
class MermaidRenderer {
|
|
2329
2407
|
name = "mermaid";
|
|
2330
2408
|
description = "Mermaid ERD syntax (text). Supports export via mermaid-cli.";
|
|
2331
|
-
render(
|
|
2332
|
-
return renderMermaidErd(
|
|
2409
|
+
render(entities) {
|
|
2410
|
+
return renderMermaidErd(entities);
|
|
2333
2411
|
}
|
|
2334
2412
|
async export(content, outputPath, format) {
|
|
2335
2413
|
await runMermaidCli(content, { outputFormat: format, outputPath });
|
|
@@ -2385,8 +2463,8 @@ var import_sharp = __toESM(require("sharp"));
|
|
|
2385
2463
|
class VectorRenderer {
|
|
2386
2464
|
name = "vector";
|
|
2387
2465
|
description = "Vector-first Mermaid ERD renderer (default). Exports SVG/PNG/PDF using mermaid-cli with sharp for high-DPI output.";
|
|
2388
|
-
render(
|
|
2389
|
-
return renderMermaidErd(
|
|
2466
|
+
render(entities) {
|
|
2467
|
+
return renderMermaidErd(entities);
|
|
2390
2468
|
}
|
|
2391
2469
|
async export(content, outputPath, format) {
|
|
2392
2470
|
const svg = await runMermaidCli(content, {
|
|
@@ -2443,47 +2521,127 @@ if (!rendererRegistry.has("mermaid")) {
|
|
|
2443
2521
|
});
|
|
2444
2522
|
}
|
|
2445
2523
|
|
|
2446
|
-
// src/traversal/
|
|
2524
|
+
// src/traversal/entity-traverser.ts
|
|
2447
2525
|
var DEFAULT_MAX_DEPTH = 3;
|
|
2448
|
-
function
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2526
|
+
function entityKey(kind, name) {
|
|
2527
|
+
return `${kind}:${name}`;
|
|
2528
|
+
}
|
|
2529
|
+
function findEntity(schema, name) {
|
|
2530
|
+
const model = schema.models.get(name);
|
|
2531
|
+
if (model) {
|
|
2532
|
+
return { entity: model, kind: "model" };
|
|
2533
|
+
}
|
|
2534
|
+
const view = schema.views.get(name);
|
|
2535
|
+
if (view) {
|
|
2536
|
+
return { entity: view, kind: "view" };
|
|
2537
|
+
}
|
|
2538
|
+
const enumDef = schema.enums.get(name);
|
|
2539
|
+
if (enumDef) {
|
|
2540
|
+
return { entity: enumDef, kind: "enum" };
|
|
2541
|
+
}
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
function findEntitiesUsingEnum(schema, enumName) {
|
|
2545
|
+
const result = [];
|
|
2546
|
+
for (const model of schema.models.values()) {
|
|
2547
|
+
const usesEnum = model.fields.some((f) => f.type === enumName);
|
|
2548
|
+
if (usesEnum) {
|
|
2549
|
+
result.push({ entity: model, kind: "model" });
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
for (const view of schema.views.values()) {
|
|
2553
|
+
const usesEnum = view.fields.some((f) => f.type === enumName);
|
|
2554
|
+
if (usesEnum) {
|
|
2555
|
+
result.push({ entity: view, kind: "view" });
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
return result;
|
|
2559
|
+
}
|
|
2560
|
+
function getReferencedEnums(entity, schema) {
|
|
2561
|
+
const enums = [];
|
|
2562
|
+
for (const field of entity.fields) {
|
|
2563
|
+
const enumDef = schema.enums.get(field.type);
|
|
2564
|
+
if (enumDef) {
|
|
2565
|
+
enums.push(enumDef);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
return enums;
|
|
2569
|
+
}
|
|
2570
|
+
function getRelatedEntities(entity, schema) {
|
|
2571
|
+
const result = [];
|
|
2572
|
+
for (const relation of entity.relations) {
|
|
2573
|
+
const relatedModel = schema.models.get(relation.relatedModel);
|
|
2574
|
+
if (relatedModel) {
|
|
2575
|
+
result.push({ entity: relatedModel, kind: "model" });
|
|
2576
|
+
continue;
|
|
2577
|
+
}
|
|
2578
|
+
const relatedView = schema.views.get(relation.relatedModel);
|
|
2579
|
+
if (relatedView) {
|
|
2580
|
+
result.push({ entity: relatedView, kind: "view" });
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
return result;
|
|
2584
|
+
}
|
|
2585
|
+
function traverseEntities(schema, options) {
|
|
2586
|
+
const { startEntity: startEntityName, maxDepth = DEFAULT_MAX_DEPTH } = options;
|
|
2587
|
+
const startFound = findEntity(schema, startEntityName);
|
|
2588
|
+
if (!startFound) {
|
|
2452
2589
|
return {
|
|
2453
2590
|
success: false,
|
|
2454
|
-
error: `
|
|
2591
|
+
error: `Entity "${startEntityName}" not found in schema (searched models, views, and enums)`
|
|
2455
2592
|
};
|
|
2456
2593
|
}
|
|
2594
|
+
const { entity: startEntity, kind: startKind } = startFound;
|
|
2457
2595
|
const visited = new Set;
|
|
2458
2596
|
const result = [];
|
|
2459
2597
|
const queue = [];
|
|
2460
|
-
queue.push({
|
|
2461
|
-
visited.add(
|
|
2598
|
+
queue.push({ entity: startEntity, kind: startKind, depth: 0 });
|
|
2599
|
+
visited.add(entityKey(startKind, startEntity.name));
|
|
2462
2600
|
while (queue.length > 0) {
|
|
2463
2601
|
const current = queue.shift();
|
|
2464
2602
|
if (!current)
|
|
2465
2603
|
break;
|
|
2466
|
-
const {
|
|
2467
|
-
result.push({
|
|
2604
|
+
const { entity, kind, depth } = current;
|
|
2605
|
+
result.push({ entity, kind, depth });
|
|
2468
2606
|
if (depth >= maxDepth) {
|
|
2469
2607
|
continue;
|
|
2470
2608
|
}
|
|
2471
|
-
|
|
2472
|
-
const
|
|
2473
|
-
|
|
2474
|
-
|
|
2609
|
+
if (kind === "enum") {
|
|
2610
|
+
const users = findEntitiesUsingEnum(schema, entity.name);
|
|
2611
|
+
for (const { entity: user, kind: userKind } of users) {
|
|
2612
|
+
const key = entityKey(userKind, user.name);
|
|
2613
|
+
if (!visited.has(key)) {
|
|
2614
|
+
visited.add(key);
|
|
2615
|
+
queue.push({ entity: user, kind: userKind, depth: depth + 1 });
|
|
2616
|
+
}
|
|
2475
2617
|
}
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2618
|
+
} else {
|
|
2619
|
+
const modelOrView = entity;
|
|
2620
|
+
const related = getRelatedEntities(modelOrView, schema);
|
|
2621
|
+
for (const { entity: relatedEntity, kind: relatedKind } of related) {
|
|
2622
|
+
const key = entityKey(relatedKind, relatedEntity.name);
|
|
2623
|
+
if (!visited.has(key)) {
|
|
2624
|
+
visited.add(key);
|
|
2625
|
+
queue.push({
|
|
2626
|
+
entity: relatedEntity,
|
|
2627
|
+
kind: relatedKind,
|
|
2628
|
+
depth: depth + 1
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
const enums = getReferencedEnums(modelOrView, schema);
|
|
2633
|
+
for (const enumDef of enums) {
|
|
2634
|
+
const key = entityKey("enum", enumDef.name);
|
|
2635
|
+
if (!visited.has(key)) {
|
|
2636
|
+
visited.add(key);
|
|
2637
|
+
queue.push({ entity: enumDef, kind: "enum", depth: depth + 1 });
|
|
2638
|
+
}
|
|
2479
2639
|
}
|
|
2480
|
-
visited.add(relatedModelName);
|
|
2481
|
-
queue.push({ model: relatedModel, depth: depth + 1 });
|
|
2482
2640
|
}
|
|
2483
2641
|
}
|
|
2484
2642
|
return {
|
|
2485
2643
|
success: true,
|
|
2486
|
-
|
|
2644
|
+
entities: result
|
|
2487
2645
|
};
|
|
2488
2646
|
}
|
|
2489
2647
|
// src/cli/types.ts
|
|
@@ -2509,7 +2667,7 @@ function isSupportedExtension(ext) {
|
|
|
2509
2667
|
}
|
|
2510
2668
|
function createProgram() {
|
|
2511
2669
|
const program2 = new Command;
|
|
2512
|
-
program2.name("prisma-neighborhood").description("Generate Entity-Relationship Diagrams from Prisma schemas").version("0.
|
|
2670
|
+
program2.name("prisma-neighborhood").description("Generate Entity-Relationship Diagrams from Prisma schemas").version("0.3.0").option("-s, --schema <path>", "Path to the Prisma schema file").option("-m, --model <name>", "Name of the entity (model, view, or enum) to start traversal from").option("-d, --depth <n>", "Traversal depth", String(DEFAULT_CLI_OPTIONS.depth)).option("-r, --renderer <name>", "Diagram renderer", DEFAULT_CLI_OPTIONS.renderer).option("-o, --output <file>", "Output file: .mmd, .md (text), .svg, .png, .pdf (export)").option("--list-renderers", "Show available renderers").action(runCommand);
|
|
2513
2671
|
return program2;
|
|
2514
2672
|
}
|
|
2515
2673
|
function listRenderers() {
|
|
@@ -2565,15 +2723,15 @@ async function runCommand(options) {
|
|
|
2565
2723
|
process.exit(1);
|
|
2566
2724
|
}
|
|
2567
2725
|
const depth = parseInt(options.depth, 10);
|
|
2568
|
-
const traversalResult =
|
|
2569
|
-
|
|
2726
|
+
const traversalResult = traverseEntities(parseResult.schema, {
|
|
2727
|
+
startEntity: options.model,
|
|
2570
2728
|
maxDepth: depth
|
|
2571
2729
|
});
|
|
2572
2730
|
if (!traversalResult.success) {
|
|
2573
2731
|
console.error(`Error: ${traversalResult.error}`);
|
|
2574
2732
|
process.exit(1);
|
|
2575
2733
|
}
|
|
2576
|
-
const output = renderer.render(traversalResult.
|
|
2734
|
+
const output = renderer.render(traversalResult.entities);
|
|
2577
2735
|
if (options.output) {
|
|
2578
2736
|
if (format && renderer.export) {
|
|
2579
2737
|
await renderer.export(output, options.output, format);
|