@rocksoliddigital/payload-lineage 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 +12 -5
- package/dist/client.js +291 -98
- package/dist/index.d.ts +11 -1
- package/dist/index.js +127 -1
- package/dist/lineage.css +18 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -21,6 +21,10 @@ Both are built from the running config on each visit (well under a millisecond),
|
|
|
21
21
|
out of date. A "Lineage" link in the nav opens them. To try them, run the
|
|
22
22
|
[example](https://github.com/rock-solid/lineage-kit/tree/main/examples/payload).
|
|
23
23
|
|
|
24
|
+
**Copy Mermaid**, on both pages, copies what the page shows, with its filters, as a Mermaid diagram:
|
|
25
|
+
the lineage as a flowchart, the data model as an `erDiagram` in crow's-foot notation. Paste it into
|
|
26
|
+
a PR, a doc or a ticket, and GitHub, GitLab and most docs tools draw it.
|
|
27
|
+
|
|
24
28
|
Works with Payload 3 on Next.js 15 or 16, with React 19.
|
|
25
29
|
|
|
26
30
|
> **Status:** early. Both pages come from the config only. Which pages use which blocks (from the
|
|
@@ -29,9 +33,6 @@ Works with Payload 3 on Next.js 15 or 16, with React 19.
|
|
|
29
33
|
|
|
30
34
|
## 📥 Install
|
|
31
35
|
|
|
32
|
-
Not published yet: the first release, 0.2.0, is on the
|
|
33
|
-
[roadmap](https://github.com/rock-solid/lineage-kit/blob/main/ROADMAP.md#-payload-plugin). Then:
|
|
34
|
-
|
|
35
36
|
```sh
|
|
36
37
|
pnpm add @rocksoliddigital/payload-lineage
|
|
37
38
|
```
|
|
@@ -85,14 +86,20 @@ lineagePlugin({
|
|
|
85
86
|
});
|
|
86
87
|
```
|
|
87
88
|
|
|
88
|
-
Or build either yourself, e.g. in a script:
|
|
89
|
+
Or build either yourself, e.g. in a script, and keep a diagram in your docs up to date:
|
|
89
90
|
|
|
90
91
|
```ts
|
|
91
|
-
import {
|
|
92
|
+
import {
|
|
93
|
+
buildContentGraph,
|
|
94
|
+
buildSchema,
|
|
95
|
+
contentGraphToMermaid,
|
|
96
|
+
schemaToMermaid,
|
|
97
|
+
} from '@rocksoliddigital/payload-lineage';
|
|
92
98
|
import config from '@payload-config';
|
|
93
99
|
|
|
94
100
|
const graph = buildContentGraph(await config);
|
|
95
101
|
const schema = buildSchema(await config);
|
|
102
|
+
const diagram = schemaToMermaid(schema); // or contentGraphToMermaid(graph)
|
|
96
103
|
```
|
|
97
104
|
|
|
98
105
|
## 🔒 Access
|
package/dist/client.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "@xyflow/react";
|
|
17
17
|
|
|
18
18
|
// ../lineage-core/src/react.ts
|
|
19
|
-
import { useEffect, useState } from "react";
|
|
19
|
+
import { useCallback, useEffect, useState } from "react";
|
|
20
20
|
|
|
21
21
|
// ../lineage-core/src/layout.ts
|
|
22
22
|
import ELK from "elkjs/lib/elk.bundled.js";
|
|
@@ -72,10 +72,222 @@ function useLayout(nodes, edges, options, onLayout) {
|
|
|
72
72
|
}, [key]);
|
|
73
73
|
return positions;
|
|
74
74
|
}
|
|
75
|
+
async function copyText(text) {
|
|
76
|
+
try {
|
|
77
|
+
await navigator.clipboard.writeText(text);
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
const area = document.createElement("textarea");
|
|
81
|
+
area.value = text;
|
|
82
|
+
area.setAttribute("readonly", "");
|
|
83
|
+
area.style.position = "fixed";
|
|
84
|
+
area.style.opacity = "0";
|
|
85
|
+
document.body.appendChild(area);
|
|
86
|
+
area.select();
|
|
87
|
+
try {
|
|
88
|
+
return document.execCommand("copy");
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
} finally {
|
|
92
|
+
area.remove();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function useCopy(resetAfterMs = 2e3) {
|
|
97
|
+
const [status, setStatus] = useState("idle");
|
|
98
|
+
const copy = useCallback(async (text) => {
|
|
99
|
+
setStatus(await copyText(text) ? "copied" : "failed");
|
|
100
|
+
}, []);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (status === "idle") return;
|
|
103
|
+
const timer = setTimeout(() => setStatus("idle"), resetAfterMs);
|
|
104
|
+
return () => clearTimeout(timer);
|
|
105
|
+
}, [status, resetAfterMs]);
|
|
106
|
+
return { status, copy };
|
|
107
|
+
}
|
|
75
108
|
|
|
76
109
|
// src/view/ErdCanvas.tsx
|
|
77
110
|
import { useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
|
|
78
111
|
|
|
112
|
+
// ../lineage-core/src/traverse.ts
|
|
113
|
+
function adjacency(links) {
|
|
114
|
+
const up = /* @__PURE__ */ new Map();
|
|
115
|
+
const down = /* @__PURE__ */ new Map();
|
|
116
|
+
const add = (map, key, value) => {
|
|
117
|
+
const list = map.get(key);
|
|
118
|
+
if (list) list.push(value);
|
|
119
|
+
else map.set(key, [value]);
|
|
120
|
+
};
|
|
121
|
+
for (const { source, target } of links) {
|
|
122
|
+
add(up, target, source);
|
|
123
|
+
add(down, source, target);
|
|
124
|
+
}
|
|
125
|
+
return { up, down };
|
|
126
|
+
}
|
|
127
|
+
function walk(start, next, maxDepth = Infinity) {
|
|
128
|
+
const seen = /* @__PURE__ */ new Set();
|
|
129
|
+
let frontier = [start];
|
|
130
|
+
for (let level = 0; level < maxDepth && frontier.length; level++) {
|
|
131
|
+
const reached = [];
|
|
132
|
+
for (const id of frontier) {
|
|
133
|
+
for (const to of next.get(id) ?? []) {
|
|
134
|
+
if (to === start || seen.has(to)) continue;
|
|
135
|
+
seen.add(to);
|
|
136
|
+
reached.push(to);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
frontier = reached;
|
|
140
|
+
}
|
|
141
|
+
return seen;
|
|
142
|
+
}
|
|
143
|
+
function trace(id, links) {
|
|
144
|
+
const { up, down } = adjacency(links);
|
|
145
|
+
return /* @__PURE__ */ new Set([id, ...walk(id, up), ...walk(id, down)]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ../lineage-core/src/mermaid.ts
|
|
149
|
+
var MERMAID_READABLE_NODES = 50;
|
|
150
|
+
var ARROW = { solid: "-->", dashed: "-.->", thick: "==>" };
|
|
151
|
+
var MERMAID_COLOR = /^(#[0-9a-f]{3,8}|[a-z]+)$/i;
|
|
152
|
+
function toMermaidFlowchart({
|
|
153
|
+
nodes,
|
|
154
|
+
edges,
|
|
155
|
+
groups = [],
|
|
156
|
+
direction = "LR"
|
|
157
|
+
}) {
|
|
158
|
+
const ids = new Map(nodes.map((n, i) => [n.id, `n${i}`]));
|
|
159
|
+
const lines = [`flowchart ${direction}`];
|
|
160
|
+
for (const n of nodes) lines.push(` ${ids.get(n.id)}["${escapeLabel(n.label)}"]`);
|
|
161
|
+
for (const e of edges) {
|
|
162
|
+
const from = ids.get(e.source);
|
|
163
|
+
const to = ids.get(e.target);
|
|
164
|
+
if (!from || !to) continue;
|
|
165
|
+
const arrow = ARROW[e.style ?? "solid"];
|
|
166
|
+
const label = e.label ? `|"${escapeLabel(e.label)}"|` : "";
|
|
167
|
+
lines.push(` ${from} ${arrow}${label} ${to}`);
|
|
168
|
+
}
|
|
169
|
+
const used = new Set(nodes.map((n) => n.group).filter(Boolean));
|
|
170
|
+
groups.forEach((g, i) => {
|
|
171
|
+
if (!used.has(g.id) || !MERMAID_COLOR.test(g.color)) return;
|
|
172
|
+
const members = nodes.filter((n) => n.group === g.id).map((n) => ids.get(n.id));
|
|
173
|
+
lines.push(` classDef g${i} fill:${g.color},stroke:${g.color},color:#fff`);
|
|
174
|
+
lines.push(` class ${members.join(",")} g${i}`);
|
|
175
|
+
});
|
|
176
|
+
return `${lines.join("\n")}
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
function escapeLabel(text) {
|
|
180
|
+
return text.replace(/#/g, "#35;").replace(/"/g, "#quot;").replace(/</g, "#lt;").replace(/>/g, "#gt;");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/mermaid.ts
|
|
184
|
+
var KIND_COLORS = {
|
|
185
|
+
collection: "#0f7c74",
|
|
186
|
+
block: "#b4630c",
|
|
187
|
+
global: "#6b4bb8"
|
|
188
|
+
};
|
|
189
|
+
var EDGE_STYLE = { relationship: "solid", upload: "dashed", blocks: "thick" };
|
|
190
|
+
function contentGraphToMermaid(graph) {
|
|
191
|
+
return toMermaidFlowchart({
|
|
192
|
+
nodes: graph.nodes.map((n) => ({ id: n.id, label: n.label, group: n.kind })),
|
|
193
|
+
edges: graph.edges.map((e) => ({
|
|
194
|
+
source: e.source,
|
|
195
|
+
target: e.target,
|
|
196
|
+
label: e.fields.join(", "),
|
|
197
|
+
style: EDGE_STYLE[e.kind]
|
|
198
|
+
})),
|
|
199
|
+
groups: Object.entries(KIND_COLORS).map(([id, color]) => ({ id, color }))
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function schemaToMermaid(schema) {
|
|
203
|
+
const names = entityNames(schema.entities);
|
|
204
|
+
const byId = new Map(schema.entities.map((e) => [e.id, e]));
|
|
205
|
+
const lines = ["erDiagram"];
|
|
206
|
+
for (const entity of schema.entities) {
|
|
207
|
+
const keys = new Set(schema.relations.filter((r) => r.from === entity.id).map((r) => r.field));
|
|
208
|
+
lines.push(` ${names.get(entity.id)}["${escapeLabel(entity.label)}"] {`);
|
|
209
|
+
for (const field of entity.fields) lines.push(` ${attribute(field, keys.has(field.name))}`);
|
|
210
|
+
lines.push(" }");
|
|
211
|
+
}
|
|
212
|
+
const lineGroups = /* @__PURE__ */ new Map();
|
|
213
|
+
for (const r of schema.relations) {
|
|
214
|
+
if (!names.has(r.from) || !names.has(r.to)) continue;
|
|
215
|
+
const key = `${r.from}
|
|
216
|
+
${r.field}
|
|
217
|
+
${r.to}`;
|
|
218
|
+
lineGroups.set(key, [...lineGroups.get(key) ?? [], r]);
|
|
219
|
+
}
|
|
220
|
+
for (const group of lineGroups.values()) {
|
|
221
|
+
const { from, to, field } = group[0];
|
|
222
|
+
const row = byId.get(from)?.fields.find((f) => f.name === field);
|
|
223
|
+
const label = [...new Set(group.map((r) => r.path))].join(", ");
|
|
224
|
+
lines.push(
|
|
225
|
+
` ${names.get(from)} ${crowsFoot(group, from, !!row?.required)} ${names.get(to)} : "${escapeLabel(label)}"`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return `${lines.join("\n")}
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
function entityNames(entities) {
|
|
232
|
+
const names = /* @__PURE__ */ new Map();
|
|
233
|
+
const taken = /* @__PURE__ */ new Set();
|
|
234
|
+
for (const e of entities) {
|
|
235
|
+
const base = `${e.kind === "global" ? "global_" : ""}${e.slug}`.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
236
|
+
let name = /^[A-Za-z]/.test(base) ? base : `e_${base}`;
|
|
237
|
+
for (let i = 2; taken.has(name); i++) name = `${base}_${i}`;
|
|
238
|
+
taken.add(name);
|
|
239
|
+
names.set(e.id, name);
|
|
240
|
+
}
|
|
241
|
+
return names;
|
|
242
|
+
}
|
|
243
|
+
function attribute(field, isReference) {
|
|
244
|
+
const type = `${safeWord(field.type)}${field.hasMany ? "[]" : ""}`;
|
|
245
|
+
const name = safeWord(field.name);
|
|
246
|
+
const key = field.type === "id" || field.name === "id" ? " PK" : isReference ? " FK" : "";
|
|
247
|
+
const notes = [
|
|
248
|
+
...name !== field.name ? [field.name] : [],
|
|
249
|
+
...field.required ? ["required"] : [],
|
|
250
|
+
...field.unique ? ["unique"] : [],
|
|
251
|
+
...field.relationTo ? [`\u2192 ${field.relationTo.join(" | ")}`] : [],
|
|
252
|
+
...field.blocks ? [`blocks: ${field.blocks.join(", ")}`] : []
|
|
253
|
+
];
|
|
254
|
+
const comment = notes.length ? ` "${escapeLabel(notes.join(", "))}"` : "";
|
|
255
|
+
return `${type} ${name}${key}${comment}`;
|
|
256
|
+
}
|
|
257
|
+
function safeWord(text) {
|
|
258
|
+
const word = text.replace(/[^A-Za-z0-9_\-[\]()]/g, "_");
|
|
259
|
+
return /^[A-Za-z]/.test(word) ? word : `f_${word}`;
|
|
260
|
+
}
|
|
261
|
+
function crowsFoot(relations, from, required) {
|
|
262
|
+
const many = relations.some((r) => r.many);
|
|
263
|
+
const atMostOne = from.startsWith("global:") || relations.every((r) => r.unique) && !many;
|
|
264
|
+
const fromSide = atMostOne ? "|o" : "}o";
|
|
265
|
+
const atLeastOne = required && relations.every((r) => !r.polymorphic && r.field === r.path);
|
|
266
|
+
const toSide = many ? atLeastOne ? "|{" : "o{" : atLeastOne ? "||" : "o|";
|
|
267
|
+
return `${fromSide}--${toSide}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/view/CopyMermaid.tsx
|
|
271
|
+
import { jsxs } from "react/jsx-runtime";
|
|
272
|
+
var CopyMermaid = ({ text, size }) => {
|
|
273
|
+
const { status, copy } = useCopy();
|
|
274
|
+
const large = size > MERMAID_READABLE_NODES;
|
|
275
|
+
return /* @__PURE__ */ jsxs(
|
|
276
|
+
"button",
|
|
277
|
+
{
|
|
278
|
+
type: "button",
|
|
279
|
+
className: "rs-lineage__copy",
|
|
280
|
+
onClick: () => copy(text()),
|
|
281
|
+
title: large ? `${size} is a lot for one Mermaid diagram; narrow it with the filters first for one that reads well` : "Copy what the page shows as a Mermaid diagram, for a PR, doc or ticket",
|
|
282
|
+
"aria-live": "polite",
|
|
283
|
+
children: [
|
|
284
|
+
status === "copied" ? "Copied" : status === "failed" ? "Couldn't copy" : "Copy Mermaid",
|
|
285
|
+
large && status === "idle" ? " (large)" : ""
|
|
286
|
+
]
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
};
|
|
290
|
+
|
|
79
291
|
// src/view/erdModel.ts
|
|
80
292
|
var TABLE_WIDTH = 260;
|
|
81
293
|
var HEADER_HEIGHT = 44;
|
|
@@ -188,7 +400,7 @@ function edgesBetween(nodes, edges) {
|
|
|
188
400
|
}
|
|
189
401
|
|
|
190
402
|
// src/view/ErdCanvas.tsx
|
|
191
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
403
|
+
import { jsx, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
192
404
|
var ErdCanvas = (props) => /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(Canvas, { ...props }) });
|
|
193
405
|
var Canvas = ({ schema, adminURLs }) => {
|
|
194
406
|
const { fitView } = useReactFlow();
|
|
@@ -311,8 +523,8 @@ var Canvas = ({ schema, adminURLs }) => {
|
|
|
311
523
|
const byId = useMemo(() => new Map(schema.entities.map((e) => [e.id, e])), [schema]);
|
|
312
524
|
const focused = selected ? byId.get(selected) : void 0;
|
|
313
525
|
const fieldCount = entities.reduce((n, e) => n + e.fields.length, 0);
|
|
314
|
-
return /* @__PURE__ */
|
|
315
|
-
/* @__PURE__ */ jsx("div", { className: "rs-lineage", "data-ready": positions ? "" : void 0, children: /* @__PURE__ */
|
|
526
|
+
return /* @__PURE__ */ jsxs2("div", { className: "rs-lineage-layout", ref: layoutRef, children: [
|
|
527
|
+
/* @__PURE__ */ jsx("div", { className: "rs-lineage", "data-ready": positions ? "" : void 0, children: /* @__PURE__ */ jsxs2(
|
|
316
528
|
ReactFlow,
|
|
317
529
|
{
|
|
318
530
|
nodes,
|
|
@@ -342,8 +554,8 @@ var Canvas = ({ schema, adminURLs }) => {
|
|
|
342
554
|
ariaLabel: "Data model overview"
|
|
343
555
|
}
|
|
344
556
|
),
|
|
345
|
-
/* @__PURE__ */ jsx(Panel, { position: "top-left", children: /* @__PURE__ */
|
|
346
|
-
/* @__PURE__ */
|
|
557
|
+
/* @__PURE__ */ jsx(Panel, { position: "top-left", children: /* @__PURE__ */ jsxs2("div", { className: "rs-lineage__toolbar", children: [
|
|
558
|
+
/* @__PURE__ */ jsxs2("label", { className: "rs-lineage__toggle", children: [
|
|
347
559
|
/* @__PURE__ */ jsx(
|
|
348
560
|
"input",
|
|
349
561
|
{
|
|
@@ -355,18 +567,30 @@ var Canvas = ({ schema, adminURLs }) => {
|
|
|
355
567
|
/* @__PURE__ */ jsx("span", { className: "rs-lineage__swatch rs-lineage__swatch--global" }),
|
|
356
568
|
"Globals"
|
|
357
569
|
] }),
|
|
358
|
-
/* @__PURE__ */
|
|
570
|
+
/* @__PURE__ */ jsxs2("label", { className: "rs-lineage__toggle", children: [
|
|
359
571
|
/* @__PURE__ */ jsx("input", { type: "checkbox", checked: keysOnly, onChange: () => setKeysOnly((v) => !v) }),
|
|
360
572
|
"Key fields only"
|
|
361
573
|
] }),
|
|
362
|
-
/* @__PURE__ */
|
|
574
|
+
/* @__PURE__ */ jsxs2("span", { className: "rs-lineage__count", children: [
|
|
363
575
|
entities.length,
|
|
364
576
|
" tables, ",
|
|
365
577
|
fieldCount,
|
|
366
578
|
" fields, ",
|
|
367
579
|
lines.length,
|
|
368
580
|
" references"
|
|
369
|
-
] })
|
|
581
|
+
] }),
|
|
582
|
+
/* @__PURE__ */ jsx(
|
|
583
|
+
CopyMermaid,
|
|
584
|
+
{
|
|
585
|
+
text: () => schemaToMermaid({
|
|
586
|
+
entities: tables.map(
|
|
587
|
+
(t) => keysOnly ? { ...t.entity, fields: t.rows } : t.entity
|
|
588
|
+
),
|
|
589
|
+
relations
|
|
590
|
+
}),
|
|
591
|
+
size: entities.length
|
|
592
|
+
}
|
|
593
|
+
)
|
|
370
594
|
] }) })
|
|
371
595
|
]
|
|
372
596
|
}
|
|
@@ -385,30 +609,30 @@ var Canvas = ({ schema, adminURLs }) => {
|
|
|
385
609
|
};
|
|
386
610
|
var Details = ({ entity, lines, byId, adminURLs, onSelect }) => {
|
|
387
611
|
if (!entity) {
|
|
388
|
-
return /* @__PURE__ */
|
|
612
|
+
return /* @__PURE__ */ jsxs2("aside", { className: "rs-lineage__details", children: [
|
|
389
613
|
/* @__PURE__ */ jsx("p", { className: "rs-lineage__hint", children: "Hover a table to see what it references and what references it. Click it for the field paths behind each line. Esc clears the selection." }),
|
|
390
|
-
/* @__PURE__ */
|
|
391
|
-
/* @__PURE__ */
|
|
614
|
+
/* @__PURE__ */ jsxs2("dl", { className: "rs-lineage__legend", children: [
|
|
615
|
+
/* @__PURE__ */ jsxs2("div", { children: [
|
|
392
616
|
/* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "many-to-one" }),
|
|
393
617
|
/* @__PURE__ */ jsx("dd", { children: "each document references one; many can share it" })
|
|
394
618
|
] }),
|
|
395
|
-
/* @__PURE__ */
|
|
619
|
+
/* @__PURE__ */ jsxs2("div", { children: [
|
|
396
620
|
/* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "many-to-many" }),
|
|
397
621
|
/* @__PURE__ */ jsx("dd", { children: "a list of references: hasMany, or inside an array or blocks" })
|
|
398
622
|
] }),
|
|
399
|
-
/* @__PURE__ */
|
|
623
|
+
/* @__PURE__ */ jsxs2("div", { children: [
|
|
400
624
|
/* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "one-to-one" }),
|
|
401
625
|
/* @__PURE__ */ jsx("dd", { children: "a unique reference, or a global's single one" })
|
|
402
626
|
] }),
|
|
403
|
-
/* @__PURE__ */
|
|
627
|
+
/* @__PURE__ */ jsxs2("div", { children: [
|
|
404
628
|
/* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "one-to-many" }),
|
|
405
629
|
/* @__PURE__ */ jsx("dd", { children: "a global's list of references" })
|
|
406
630
|
] }),
|
|
407
|
-
/* @__PURE__ */
|
|
631
|
+
/* @__PURE__ */ jsxs2("div", { children: [
|
|
408
632
|
/* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "\u21BB" }),
|
|
409
633
|
/* @__PURE__ */ jsx("dd", { children: "points at its own collection" })
|
|
410
634
|
] }),
|
|
411
|
-
/* @__PURE__ */
|
|
635
|
+
/* @__PURE__ */ jsxs2("div", { children: [
|
|
412
636
|
/* @__PURE__ */ jsx("dt", { className: "rs-erd__key", children: "*" }),
|
|
413
637
|
/* @__PURE__ */ jsx("dd", { children: "required" })
|
|
414
638
|
] })
|
|
@@ -422,8 +646,8 @@ var Details = ({ entity, lines, byId, adminURLs, onSelect }) => {
|
|
|
422
646
|
const other = byId.get(otherId);
|
|
423
647
|
const from = byId.get(line.from);
|
|
424
648
|
const to = byId.get(line.to);
|
|
425
|
-
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */
|
|
426
|
-
/* @__PURE__ */
|
|
649
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs2("button", { type: "button", className: "rs-lineage__link", onClick: () => onSelect(other.id), children: [
|
|
650
|
+
/* @__PURE__ */ jsxs2("span", { className: "rs-lineage__link-name", children: [
|
|
427
651
|
/* @__PURE__ */ jsx("span", { className: `rs-lineage__swatch rs-lineage__swatch--${other.kind}` }),
|
|
428
652
|
other.label,
|
|
429
653
|
/* @__PURE__ */ jsx("span", { className: "rs-erd__cardinality-tag", children: line.cardinality })
|
|
@@ -432,15 +656,15 @@ var Details = ({ entity, lines, byId, adminURLs, onSelect }) => {
|
|
|
432
656
|
/* @__PURE__ */ jsx("span", { className: "rs-lineage__via", children: line.relations.map((r) => r.path).join(", ") })
|
|
433
657
|
] }) }, line.id);
|
|
434
658
|
};
|
|
435
|
-
return /* @__PURE__ */
|
|
436
|
-
/* @__PURE__ */
|
|
437
|
-
/* @__PURE__ */
|
|
659
|
+
return /* @__PURE__ */ jsxs2("aside", { className: "rs-lineage__details", "aria-live": "polite", children: [
|
|
660
|
+
/* @__PURE__ */ jsxs2("div", { children: [
|
|
661
|
+
/* @__PURE__ */ jsxs2("span", { className: `rs-lineage__kind rs-lineage__kind--${entity.kind}`, children: [
|
|
438
662
|
entity.kind,
|
|
439
663
|
entity.auth ? " \xB7 auth" : "",
|
|
440
664
|
entity.upload ? " \xB7 upload" : ""
|
|
441
665
|
] }),
|
|
442
666
|
/* @__PURE__ */ jsx("h2", { className: "rs-lineage__name", children: entity.label }),
|
|
443
|
-
/* @__PURE__ */
|
|
667
|
+
/* @__PURE__ */ jsxs2("code", { className: "rs-lineage__id", children: [
|
|
444
668
|
entity.slug,
|
|
445
669
|
" \xB7 ",
|
|
446
670
|
entity.fields.length,
|
|
@@ -448,15 +672,15 @@ var Details = ({ entity, lines, byId, adminURLs, onSelect }) => {
|
|
|
448
672
|
] })
|
|
449
673
|
] }),
|
|
450
674
|
url && /* @__PURE__ */ jsx("a", { className: "rs-lineage__open", href: url, children: "Open in the admin" }),
|
|
451
|
-
/* @__PURE__ */
|
|
452
|
-
/* @__PURE__ */
|
|
675
|
+
/* @__PURE__ */ jsxs2("section", { children: [
|
|
676
|
+
/* @__PURE__ */ jsxs2("h3", { className: "rs-lineage__heading", children: [
|
|
453
677
|
"References \xB7 ",
|
|
454
678
|
out.length
|
|
455
679
|
] }),
|
|
456
680
|
out.length ? /* @__PURE__ */ jsx("ul", { className: "rs-lineage__list", children: out.map((l) => item(l, l.to)) }) : /* @__PURE__ */ jsx("p", { className: "rs-lineage__hint", children: "None of its fields reference another collection." })
|
|
457
681
|
] }),
|
|
458
|
-
/* @__PURE__ */
|
|
459
|
-
/* @__PURE__ */
|
|
682
|
+
/* @__PURE__ */ jsxs2("section", { children: [
|
|
683
|
+
/* @__PURE__ */ jsxs2("h3", { className: "rs-lineage__heading", children: [
|
|
460
684
|
"Referenced by \xB7 ",
|
|
461
685
|
into.length
|
|
462
686
|
] }),
|
|
@@ -478,7 +702,7 @@ var TableView = ({
|
|
|
478
702
|
onSelect,
|
|
479
703
|
onTrace
|
|
480
704
|
}
|
|
481
|
-
}) => /* @__PURE__ */
|
|
705
|
+
}) => /* @__PURE__ */ jsxs2("div", { className: `rs-erd__table rs-lineage__node--${entity.kind} rs-erd__table--${state}`, children: [
|
|
482
706
|
/* @__PURE__ */ jsx(
|
|
483
707
|
Handle,
|
|
484
708
|
{
|
|
@@ -489,7 +713,7 @@ var TableView = ({
|
|
|
489
713
|
isConnectable: false
|
|
490
714
|
}
|
|
491
715
|
),
|
|
492
|
-
/* @__PURE__ */
|
|
716
|
+
/* @__PURE__ */ jsxs2(
|
|
493
717
|
"button",
|
|
494
718
|
{
|
|
495
719
|
type: "button",
|
|
@@ -505,18 +729,18 @@ var TableView = ({
|
|
|
505
729
|
]
|
|
506
730
|
}
|
|
507
731
|
),
|
|
508
|
-
/* @__PURE__ */ jsx("ul", { className: "rs-erd__rows", children: rows.map((f) => /* @__PURE__ */
|
|
732
|
+
/* @__PURE__ */ jsx("ul", { className: "rs-erd__rows", children: rows.map((f) => /* @__PURE__ */ jsxs2(
|
|
509
733
|
"li",
|
|
510
734
|
{
|
|
511
735
|
className: `rs-erd__row${linked.has(f.name) ? " rs-erd__row--link" : ""}`,
|
|
512
736
|
title: describe(f),
|
|
513
737
|
children: [
|
|
514
|
-
/* @__PURE__ */
|
|
738
|
+
/* @__PURE__ */ jsxs2("span", { className: "rs-erd__field", children: [
|
|
515
739
|
f.name,
|
|
516
740
|
f.required && /* @__PURE__ */ jsx("span", { className: "rs-erd__required", children: "*" }),
|
|
517
741
|
selfRefs.has(f.name) && /* @__PURE__ */ jsx("span", { className: "rs-erd__self", title: "Points at its own collection", children: "\u21BB" })
|
|
518
742
|
] }),
|
|
519
|
-
/* @__PURE__ */
|
|
743
|
+
/* @__PURE__ */ jsxs2("span", { className: "rs-erd__type", children: [
|
|
520
744
|
f.type === "blocks" ? `blocks (${f.blocks?.length ?? 0})` : f.type,
|
|
521
745
|
f.hasMany ? "[]" : ""
|
|
522
746
|
] }),
|
|
@@ -573,46 +797,8 @@ import {
|
|
|
573
797
|
ReactFlowProvider as ReactFlowProvider2,
|
|
574
798
|
useReactFlow as useReactFlow2
|
|
575
799
|
} from "@xyflow/react";
|
|
576
|
-
|
|
577
|
-
// ../lineage-core/src/traverse.ts
|
|
578
|
-
function adjacency(links) {
|
|
579
|
-
const up = /* @__PURE__ */ new Map();
|
|
580
|
-
const down = /* @__PURE__ */ new Map();
|
|
581
|
-
const add = (map, key, value) => {
|
|
582
|
-
const list = map.get(key);
|
|
583
|
-
if (list) list.push(value);
|
|
584
|
-
else map.set(key, [value]);
|
|
585
|
-
};
|
|
586
|
-
for (const { source, target } of links) {
|
|
587
|
-
add(up, target, source);
|
|
588
|
-
add(down, source, target);
|
|
589
|
-
}
|
|
590
|
-
return { up, down };
|
|
591
|
-
}
|
|
592
|
-
function walk(start, next, maxDepth = Infinity) {
|
|
593
|
-
const seen = /* @__PURE__ */ new Set();
|
|
594
|
-
let frontier = [start];
|
|
595
|
-
for (let level = 0; level < maxDepth && frontier.length; level++) {
|
|
596
|
-
const reached = [];
|
|
597
|
-
for (const id of frontier) {
|
|
598
|
-
for (const to of next.get(id) ?? []) {
|
|
599
|
-
if (to === start || seen.has(to)) continue;
|
|
600
|
-
seen.add(to);
|
|
601
|
-
reached.push(to);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
frontier = reached;
|
|
605
|
-
}
|
|
606
|
-
return seen;
|
|
607
|
-
}
|
|
608
|
-
function trace(id, links) {
|
|
609
|
-
const { up, down } = adjacency(links);
|
|
610
|
-
return /* @__PURE__ */ new Set([id, ...walk(id, up), ...walk(id, down)]);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// src/view/LineageCanvas.tsx
|
|
614
800
|
import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "react";
|
|
615
|
-
import { jsx as jsx2, jsxs as
|
|
801
|
+
import { jsx as jsx2, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
616
802
|
var KINDS = [
|
|
617
803
|
{ kind: "collection", label: "Collections" },
|
|
618
804
|
{ kind: "block", label: "Blocks" },
|
|
@@ -703,8 +889,8 @@ var Canvas2 = ({ graph, adminURLs }) => {
|
|
|
703
889
|
const links = visible.edges.length;
|
|
704
890
|
return (
|
|
705
891
|
// `data-ready` once the first layout is done, for tests to wait on
|
|
706
|
-
/* @__PURE__ */
|
|
707
|
-
/* @__PURE__ */ jsx2("div", { className: "rs-lineage", "data-ready": positions ? "" : void 0, children: /* @__PURE__ */
|
|
892
|
+
/* @__PURE__ */ jsxs3("div", { className: "rs-lineage-layout", ref: layoutRef, children: [
|
|
893
|
+
/* @__PURE__ */ jsx2("div", { className: "rs-lineage", "data-ready": positions ? "" : void 0, children: /* @__PURE__ */ jsxs3(
|
|
708
894
|
ReactFlow2,
|
|
709
895
|
{
|
|
710
896
|
nodes,
|
|
@@ -734,8 +920,8 @@ var Canvas2 = ({ graph, adminURLs }) => {
|
|
|
734
920
|
ariaLabel: "Content lineage overview"
|
|
735
921
|
}
|
|
736
922
|
),
|
|
737
|
-
/* @__PURE__ */ jsx2(Panel2, { position: "top-left", children: /* @__PURE__ */
|
|
738
|
-
KINDS.map(({ kind, label }) => /* @__PURE__ */
|
|
923
|
+
/* @__PURE__ */ jsx2(Panel2, { position: "top-left", children: /* @__PURE__ */ jsxs3("div", { className: "rs-lineage__toolbar", children: [
|
|
924
|
+
KINDS.map(({ kind, label }) => /* @__PURE__ */ jsxs3("label", { className: "rs-lineage__toggle", children: [
|
|
739
925
|
/* @__PURE__ */ jsx2(
|
|
740
926
|
"input",
|
|
741
927
|
{
|
|
@@ -747,7 +933,7 @@ var Canvas2 = ({ graph, adminURLs }) => {
|
|
|
747
933
|
/* @__PURE__ */ jsx2("span", { className: `rs-lineage__swatch rs-lineage__swatch--${kind}` }),
|
|
748
934
|
label
|
|
749
935
|
] }, kind)),
|
|
750
|
-
/* @__PURE__ */
|
|
936
|
+
/* @__PURE__ */ jsxs3("label", { className: "rs-lineage__toggle", children: [
|
|
751
937
|
/* @__PURE__ */ jsx2(
|
|
752
938
|
"input",
|
|
753
939
|
{
|
|
@@ -758,13 +944,20 @@ var Canvas2 = ({ graph, adminURLs }) => {
|
|
|
758
944
|
),
|
|
759
945
|
"Hide unconnected"
|
|
760
946
|
] }),
|
|
761
|
-
/* @__PURE__ */
|
|
947
|
+
/* @__PURE__ */ jsxs3("span", { className: "rs-lineage__count", children: [
|
|
762
948
|
visible.nodes.length,
|
|
763
949
|
" nodes, ",
|
|
764
950
|
links,
|
|
765
951
|
" link",
|
|
766
952
|
links === 1 ? "" : "s"
|
|
767
|
-
] })
|
|
953
|
+
] }),
|
|
954
|
+
/* @__PURE__ */ jsx2(
|
|
955
|
+
CopyMermaid,
|
|
956
|
+
{
|
|
957
|
+
text: () => contentGraphToMermaid({ nodes: visible.nodes, edges: visible.edges }),
|
|
958
|
+
size: visible.nodes.length
|
|
959
|
+
}
|
|
960
|
+
)
|
|
768
961
|
] }) })
|
|
769
962
|
]
|
|
770
963
|
}
|
|
@@ -784,18 +977,18 @@ var Canvas2 = ({ graph, adminURLs }) => {
|
|
|
784
977
|
};
|
|
785
978
|
var Details2 = ({ node, edges, byId, adminURLs, onSelect }) => {
|
|
786
979
|
if (!node) {
|
|
787
|
-
return /* @__PURE__ */
|
|
980
|
+
return /* @__PURE__ */ jsxs3("aside", { className: "rs-lineage__details", children: [
|
|
788
981
|
/* @__PURE__ */ jsx2("p", { className: "rs-lineage__hint", children: "Hover a node to trace everything it's built from and everything that uses it. Click it to see the fields behind each link. Esc clears the selection." }),
|
|
789
|
-
/* @__PURE__ */
|
|
790
|
-
/* @__PURE__ */
|
|
982
|
+
/* @__PURE__ */ jsxs3("dl", { className: "rs-lineage__legend", children: [
|
|
983
|
+
/* @__PURE__ */ jsxs3("div", { children: [
|
|
791
984
|
/* @__PURE__ */ jsx2("dt", { className: "rs-lineage__line rs-lineage__line--relationship" }),
|
|
792
985
|
/* @__PURE__ */ jsx2("dd", { children: "relationship" })
|
|
793
986
|
] }),
|
|
794
|
-
/* @__PURE__ */
|
|
987
|
+
/* @__PURE__ */ jsxs3("div", { children: [
|
|
795
988
|
/* @__PURE__ */ jsx2("dt", { className: "rs-lineage__line rs-lineage__line--upload" }),
|
|
796
989
|
/* @__PURE__ */ jsx2("dd", { children: "upload" })
|
|
797
990
|
] }),
|
|
798
|
-
/* @__PURE__ */
|
|
991
|
+
/* @__PURE__ */ jsxs3("div", { children: [
|
|
799
992
|
/* @__PURE__ */ jsx2("dt", { className: "rs-lineage__line rs-lineage__line--blocks" }),
|
|
800
993
|
/* @__PURE__ */ jsx2("dd", { children: "blocks field" })
|
|
801
994
|
] })
|
|
@@ -807,34 +1000,34 @@ var Details2 = ({ node, edges, byId, adminURLs, onSelect }) => {
|
|
|
807
1000
|
const url = adminURLs[node.id];
|
|
808
1001
|
const item = (edge, otherId) => {
|
|
809
1002
|
const other = byId.get(otherId);
|
|
810
|
-
return /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */
|
|
811
|
-
/* @__PURE__ */
|
|
1003
|
+
return /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsxs3("button", { type: "button", className: "rs-lineage__link", onClick: () => onSelect(other.id), children: [
|
|
1004
|
+
/* @__PURE__ */ jsxs3("span", { className: "rs-lineage__link-name", children: [
|
|
812
1005
|
/* @__PURE__ */ jsx2("span", { className: `rs-lineage__swatch rs-lineage__swatch--${other.kind}` }),
|
|
813
1006
|
edge.source === edge.target ? "Itself" : other.label
|
|
814
1007
|
] }),
|
|
815
|
-
/* @__PURE__ */
|
|
1008
|
+
/* @__PURE__ */ jsxs3("span", { className: "rs-lineage__via", children: [
|
|
816
1009
|
edge.kind,
|
|
817
1010
|
" \xB7 ",
|
|
818
1011
|
edge.fields.join(", ")
|
|
819
1012
|
] })
|
|
820
1013
|
] }) }, `${edge.source}->${edge.target}:${edge.kind}`);
|
|
821
1014
|
};
|
|
822
|
-
return /* @__PURE__ */
|
|
823
|
-
/* @__PURE__ */
|
|
1015
|
+
return /* @__PURE__ */ jsxs3("aside", { className: "rs-lineage__details", "aria-live": "polite", children: [
|
|
1016
|
+
/* @__PURE__ */ jsxs3("div", { children: [
|
|
824
1017
|
/* @__PURE__ */ jsx2("span", { className: `rs-lineage__kind rs-lineage__kind--${node.kind}`, children: node.kind }),
|
|
825
1018
|
/* @__PURE__ */ jsx2("h2", { className: "rs-lineage__name", children: node.label }),
|
|
826
1019
|
/* @__PURE__ */ jsx2("code", { className: "rs-lineage__id", children: node.id })
|
|
827
1020
|
] }),
|
|
828
1021
|
url && /* @__PURE__ */ jsx2("a", { className: "rs-lineage__open", href: url, children: "Open in the admin" }),
|
|
829
|
-
/* @__PURE__ */
|
|
830
|
-
/* @__PURE__ */
|
|
1022
|
+
/* @__PURE__ */ jsxs3("section", { children: [
|
|
1023
|
+
/* @__PURE__ */ jsxs3("h3", { className: "rs-lineage__heading", children: [
|
|
831
1024
|
"Built from \xB7 ",
|
|
832
1025
|
builtFrom.length
|
|
833
1026
|
] }),
|
|
834
1027
|
builtFrom.length ? /* @__PURE__ */ jsx2("ul", { className: "rs-lineage__list", children: builtFrom.map((e) => item(e, e.source)) }) : /* @__PURE__ */ jsx2("p", { className: "rs-lineage__hint", children: "None of its fields point at another collection or block." })
|
|
835
1028
|
] }),
|
|
836
|
-
/* @__PURE__ */
|
|
837
|
-
/* @__PURE__ */
|
|
1029
|
+
/* @__PURE__ */ jsxs3("section", { children: [
|
|
1030
|
+
/* @__PURE__ */ jsxs3("h3", { className: "rs-lineage__heading", children: [
|
|
838
1031
|
"Used by \xB7 ",
|
|
839
1032
|
usedBy.length
|
|
840
1033
|
] }),
|
|
@@ -844,7 +1037,7 @@ var Details2 = ({ node, edges, byId, adminURLs, onSelect }) => {
|
|
|
844
1037
|
};
|
|
845
1038
|
var ContentNodeView = ({
|
|
846
1039
|
data: { node, state, selfLink, onSelect, onTrace }
|
|
847
|
-
}) => /* @__PURE__ */
|
|
1040
|
+
}) => /* @__PURE__ */ jsxs3(
|
|
848
1041
|
"div",
|
|
849
1042
|
{
|
|
850
1043
|
className: `rs-lineage__node rs-lineage__node--${node.kind} rs-lineage__node--${state}`,
|
|
@@ -859,7 +1052,7 @@ var ContentNodeView = ({
|
|
|
859
1052
|
isConnectable: false
|
|
860
1053
|
}
|
|
861
1054
|
),
|
|
862
|
-
/* @__PURE__ */
|
|
1055
|
+
/* @__PURE__ */ jsxs3(
|
|
863
1056
|
"button",
|
|
864
1057
|
{
|
|
865
1058
|
type: "button",
|
|
@@ -869,11 +1062,11 @@ var ContentNodeView = ({
|
|
|
869
1062
|
onFocus: () => onTrace(node.id),
|
|
870
1063
|
onBlur: () => onTrace(null),
|
|
871
1064
|
children: [
|
|
872
|
-
/* @__PURE__ */
|
|
1065
|
+
/* @__PURE__ */ jsxs3("span", { className: "rs-lineage__label", children: [
|
|
873
1066
|
node.label,
|
|
874
1067
|
selfLink && /* @__PURE__ */ jsx2("span", { className: "rs-lineage__self", title: "Links to itself", children: "\u21BB" })
|
|
875
1068
|
] }),
|
|
876
|
-
/* @__PURE__ */
|
|
1069
|
+
/* @__PURE__ */ jsxs3("span", { className: "rs-lineage__meta", children: [
|
|
877
1070
|
node.kind,
|
|
878
1071
|
" \xB7 ",
|
|
879
1072
|
node.slug
|
|
@@ -899,13 +1092,13 @@ var nodeTypes2 = { content: ContentNodeView };
|
|
|
899
1092
|
import { Link, useConfig } from "@payloadcms/ui";
|
|
900
1093
|
import { usePathname } from "next/navigation";
|
|
901
1094
|
import { formatAdminURL } from "payload/shared";
|
|
902
|
-
import { Fragment, jsx as jsx3, jsxs as
|
|
1095
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
903
1096
|
var LineageNavLinkClient = () => {
|
|
904
1097
|
const { config } = useConfig();
|
|
905
1098
|
const pathname = usePathname();
|
|
906
1099
|
const href = formatAdminURL({ adminRoute: config.routes.admin, path: "/lineage" });
|
|
907
1100
|
const active = pathname === href || pathname.startsWith(`${href}/`);
|
|
908
|
-
const label = /* @__PURE__ */
|
|
1101
|
+
const label = /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
909
1102
|
active && /* @__PURE__ */ jsx3("div", { className: "nav__link-indicator" }),
|
|
910
1103
|
/* @__PURE__ */ jsx3("span", { className: "nav__link-label", children: "Lineage" })
|
|
911
1104
|
] });
|
package/dist/index.d.ts
CHANGED
|
@@ -151,6 +151,16 @@ type Schema = {
|
|
|
151
151
|
/** Builds the data model of a Payload config: each collection and global with its fields, and the relations between them. */
|
|
152
152
|
declare function buildSchema(config: PayloadConfigLike, { exclude }?: ContentGraphOptions): Schema;
|
|
153
153
|
|
|
154
|
+
/** The lineage graph as a left-to-right flowchart, each link labelled with its fields. */
|
|
155
|
+
declare function contentGraphToMermaid(graph: ContentGraph): string;
|
|
156
|
+
/**
|
|
157
|
+
* The data model as an erDiagram: a table per collection and global with its fields, and a line
|
|
158
|
+
* per row and target, in crow's-foot notation (`}o--o|` is many-to-one, `}o--o{` many-to-many).
|
|
159
|
+
* Fields keep their full path in their comment, and lines are labelled with the full paths of the
|
|
160
|
+
* references they stand for.
|
|
161
|
+
*/
|
|
162
|
+
declare function schemaToMermaid(schema: Schema): string;
|
|
163
|
+
|
|
154
164
|
/**
|
|
155
165
|
* Adds content lineage to a Payload config: the Lineage and Data model pages in the admin, at
|
|
156
166
|
* `/admin/lineage` and `/admin/lineage/schema`, and with `outputFile`, the lineage graph as JSON on
|
|
@@ -158,4 +168,4 @@ declare function buildSchema(config: PayloadConfigLike, { exclude }?: ContentGra
|
|
|
158
168
|
*/
|
|
159
169
|
declare function lineagePlugin({ enabled, adminView, access, outputFile, ...graphOptions }?: LineagePluginOptions): Plugin;
|
|
160
170
|
|
|
161
|
-
export { type ContentEdge, type ContentEdgeKind, type ContentGraph, type ContentGraphOptions, type ContentKind, type ContentNode, type LineageAccess, type LineageCustomConfig, type LineagePluginOptions, type PayloadConfigLike, type Schema, type SchemaEntity, type SchemaField, type SchemaRelation, buildContentGraph, buildSchema, lineagePlugin };
|
|
171
|
+
export { type ContentEdge, type ContentEdgeKind, type ContentGraph, type ContentGraphOptions, type ContentKind, type ContentNode, type LineageAccess, type LineageCustomConfig, type LineagePluginOptions, type PayloadConfigLike, type Schema, type SchemaEntity, type SchemaField, type SchemaRelation, buildContentGraph, buildSchema, contentGraphToMermaid, lineagePlugin, schemaToMermaid };
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,130 @@ import {
|
|
|
3
3
|
buildSchema
|
|
4
4
|
} from "./chunk-GL5UMO2C.js";
|
|
5
5
|
|
|
6
|
+
// ../lineage-core/src/layout.ts
|
|
7
|
+
import ELK from "elkjs/lib/elk.bundled.js";
|
|
8
|
+
|
|
9
|
+
// ../lineage-core/src/mermaid.ts
|
|
10
|
+
var ARROW = { solid: "-->", dashed: "-.->", thick: "==>" };
|
|
11
|
+
var MERMAID_COLOR = /^(#[0-9a-f]{3,8}|[a-z]+)$/i;
|
|
12
|
+
function toMermaidFlowchart({
|
|
13
|
+
nodes,
|
|
14
|
+
edges,
|
|
15
|
+
groups = [],
|
|
16
|
+
direction = "LR"
|
|
17
|
+
}) {
|
|
18
|
+
const ids = new Map(nodes.map((n, i) => [n.id, `n${i}`]));
|
|
19
|
+
const lines = [`flowchart ${direction}`];
|
|
20
|
+
for (const n of nodes) lines.push(` ${ids.get(n.id)}["${escapeLabel(n.label)}"]`);
|
|
21
|
+
for (const e of edges) {
|
|
22
|
+
const from = ids.get(e.source);
|
|
23
|
+
const to = ids.get(e.target);
|
|
24
|
+
if (!from || !to) continue;
|
|
25
|
+
const arrow = ARROW[e.style ?? "solid"];
|
|
26
|
+
const label = e.label ? `|"${escapeLabel(e.label)}"|` : "";
|
|
27
|
+
lines.push(` ${from} ${arrow}${label} ${to}`);
|
|
28
|
+
}
|
|
29
|
+
const used = new Set(nodes.map((n) => n.group).filter(Boolean));
|
|
30
|
+
groups.forEach((g, i) => {
|
|
31
|
+
if (!used.has(g.id) || !MERMAID_COLOR.test(g.color)) return;
|
|
32
|
+
const members = nodes.filter((n) => n.group === g.id).map((n) => ids.get(n.id));
|
|
33
|
+
lines.push(` classDef g${i} fill:${g.color},stroke:${g.color},color:#fff`);
|
|
34
|
+
lines.push(` class ${members.join(",")} g${i}`);
|
|
35
|
+
});
|
|
36
|
+
return `${lines.join("\n")}
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
function escapeLabel(text) {
|
|
40
|
+
return text.replace(/#/g, "#35;").replace(/"/g, "#quot;").replace(/</g, "#lt;").replace(/>/g, "#gt;");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/mermaid.ts
|
|
44
|
+
var KIND_COLORS = {
|
|
45
|
+
collection: "#0f7c74",
|
|
46
|
+
block: "#b4630c",
|
|
47
|
+
global: "#6b4bb8"
|
|
48
|
+
};
|
|
49
|
+
var EDGE_STYLE = { relationship: "solid", upload: "dashed", blocks: "thick" };
|
|
50
|
+
function contentGraphToMermaid(graph) {
|
|
51
|
+
return toMermaidFlowchart({
|
|
52
|
+
nodes: graph.nodes.map((n) => ({ id: n.id, label: n.label, group: n.kind })),
|
|
53
|
+
edges: graph.edges.map((e) => ({
|
|
54
|
+
source: e.source,
|
|
55
|
+
target: e.target,
|
|
56
|
+
label: e.fields.join(", "),
|
|
57
|
+
style: EDGE_STYLE[e.kind]
|
|
58
|
+
})),
|
|
59
|
+
groups: Object.entries(KIND_COLORS).map(([id, color]) => ({ id, color }))
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function schemaToMermaid(schema) {
|
|
63
|
+
const names = entityNames(schema.entities);
|
|
64
|
+
const byId = new Map(schema.entities.map((e) => [e.id, e]));
|
|
65
|
+
const lines = ["erDiagram"];
|
|
66
|
+
for (const entity of schema.entities) {
|
|
67
|
+
const keys = new Set(schema.relations.filter((r) => r.from === entity.id).map((r) => r.field));
|
|
68
|
+
lines.push(` ${names.get(entity.id)}["${escapeLabel(entity.label)}"] {`);
|
|
69
|
+
for (const field of entity.fields) lines.push(` ${attribute(field, keys.has(field.name))}`);
|
|
70
|
+
lines.push(" }");
|
|
71
|
+
}
|
|
72
|
+
const lineGroups = /* @__PURE__ */ new Map();
|
|
73
|
+
for (const r of schema.relations) {
|
|
74
|
+
if (!names.has(r.from) || !names.has(r.to)) continue;
|
|
75
|
+
const key = `${r.from}
|
|
76
|
+
${r.field}
|
|
77
|
+
${r.to}`;
|
|
78
|
+
lineGroups.set(key, [...lineGroups.get(key) ?? [], r]);
|
|
79
|
+
}
|
|
80
|
+
for (const group of lineGroups.values()) {
|
|
81
|
+
const { from, to, field } = group[0];
|
|
82
|
+
const row = byId.get(from)?.fields.find((f) => f.name === field);
|
|
83
|
+
const label = [...new Set(group.map((r) => r.path))].join(", ");
|
|
84
|
+
lines.push(
|
|
85
|
+
` ${names.get(from)} ${crowsFoot(group, from, !!row?.required)} ${names.get(to)} : "${escapeLabel(label)}"`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return `${lines.join("\n")}
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
function entityNames(entities) {
|
|
92
|
+
const names = /* @__PURE__ */ new Map();
|
|
93
|
+
const taken = /* @__PURE__ */ new Set();
|
|
94
|
+
for (const e of entities) {
|
|
95
|
+
const base = `${e.kind === "global" ? "global_" : ""}${e.slug}`.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
96
|
+
let name = /^[A-Za-z]/.test(base) ? base : `e_${base}`;
|
|
97
|
+
for (let i = 2; taken.has(name); i++) name = `${base}_${i}`;
|
|
98
|
+
taken.add(name);
|
|
99
|
+
names.set(e.id, name);
|
|
100
|
+
}
|
|
101
|
+
return names;
|
|
102
|
+
}
|
|
103
|
+
function attribute(field, isReference) {
|
|
104
|
+
const type = `${safeWord(field.type)}${field.hasMany ? "[]" : ""}`;
|
|
105
|
+
const name = safeWord(field.name);
|
|
106
|
+
const key = field.type === "id" || field.name === "id" ? " PK" : isReference ? " FK" : "";
|
|
107
|
+
const notes = [
|
|
108
|
+
...name !== field.name ? [field.name] : [],
|
|
109
|
+
...field.required ? ["required"] : [],
|
|
110
|
+
...field.unique ? ["unique"] : [],
|
|
111
|
+
...field.relationTo ? [`\u2192 ${field.relationTo.join(" | ")}`] : [],
|
|
112
|
+
...field.blocks ? [`blocks: ${field.blocks.join(", ")}`] : []
|
|
113
|
+
];
|
|
114
|
+
const comment = notes.length ? ` "${escapeLabel(notes.join(", "))}"` : "";
|
|
115
|
+
return `${type} ${name}${key}${comment}`;
|
|
116
|
+
}
|
|
117
|
+
function safeWord(text) {
|
|
118
|
+
const word = text.replace(/[^A-Za-z0-9_\-[\]()]/g, "_");
|
|
119
|
+
return /^[A-Za-z]/.test(word) ? word : `f_${word}`;
|
|
120
|
+
}
|
|
121
|
+
function crowsFoot(relations, from, required) {
|
|
122
|
+
const many = relations.some((r) => r.many);
|
|
123
|
+
const atMostOne = from.startsWith("global:") || relations.every((r) => r.unique) && !many;
|
|
124
|
+
const fromSide = atMostOne ? "|o" : "}o";
|
|
125
|
+
const atLeastOne = required && relations.every((r) => !r.polymorphic && r.field === r.path);
|
|
126
|
+
const toSide = many ? atLeastOne ? "|{" : "o{" : atLeastOne ? "||" : "o|";
|
|
127
|
+
return `${fromSide}--${toSide}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
6
130
|
// src/plugin.ts
|
|
7
131
|
import { mkdir, writeFile } from "fs/promises";
|
|
8
132
|
import path from "path";
|
|
@@ -77,5 +201,7 @@ function withOutputFile(config, outputFile, graphOptions) {
|
|
|
77
201
|
export {
|
|
78
202
|
buildContentGraph,
|
|
79
203
|
buildSchema,
|
|
80
|
-
|
|
204
|
+
contentGraphToMermaid,
|
|
205
|
+
lineagePlugin,
|
|
206
|
+
schemaToMermaid
|
|
81
207
|
};
|
package/dist/lineage.css
CHANGED
|
@@ -203,6 +203,24 @@ html[data-theme='dark'] .rs-lineage {
|
|
|
203
203
|
.rs-lineage__toggle input {
|
|
204
204
|
margin: 0;
|
|
205
205
|
}
|
|
206
|
+
.rs-lineage__copy {
|
|
207
|
+
appearance: none;
|
|
208
|
+
margin: 0;
|
|
209
|
+
padding: 3px 10px;
|
|
210
|
+
border: 1px solid var(--theme-elevation-250);
|
|
211
|
+
border-radius: var(--style-radius-s, 4px);
|
|
212
|
+
background: var(--theme-elevation-50);
|
|
213
|
+
color: var(--theme-text);
|
|
214
|
+
font: inherit;
|
|
215
|
+
cursor: pointer;
|
|
216
|
+
}
|
|
217
|
+
.rs-lineage__copy:hover {
|
|
218
|
+
background: var(--theme-elevation-100);
|
|
219
|
+
}
|
|
220
|
+
.rs-lineage__copy:focus-visible {
|
|
221
|
+
outline: 2px solid var(--theme-elevation-900);
|
|
222
|
+
outline-offset: 2px;
|
|
223
|
+
}
|
|
206
224
|
.rs-lineage__count {
|
|
207
225
|
color: var(--theme-elevation-600);
|
|
208
226
|
font-variant-numeric: tabular-nums;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rocksoliddigital/payload-lineage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Lineage and data model (ERD) pages for the Payload admin: the collections, globals and blocks, and the fields that link them.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"payload",
|
|
@@ -52,13 +52,13 @@
|
|
|
52
52
|
"@types/react": "19.2.14",
|
|
53
53
|
"@types/react-dom": "19.2.3",
|
|
54
54
|
"graphql": "16.14.2",
|
|
55
|
-
"next": "16.3.
|
|
55
|
+
"next": "16.3.8",
|
|
56
56
|
"payload": "3.90.2",
|
|
57
57
|
"react": "19.2.6",
|
|
58
58
|
"react-dom": "19.2.6",
|
|
59
59
|
"tsup": "8.5.1",
|
|
60
60
|
"typescript": "5.7.3",
|
|
61
|
-
"vitest": "4.
|
|
61
|
+
"vitest": "4.1.11"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"@payloadcms/next": "^3.0.0",
|