@rocksoliddigital/payload-lineage 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +150 -2
- package/dist/chunk-GL5UMO2C.js +203 -0
- package/dist/client.d.ts +104 -0
- package/dist/client.js +1111 -0
- package/dist/index.d.ts +171 -0
- package/dist/index.js +207 -0
- package/dist/lineage.css +538 -0
- package/dist/rsc.d.ts +22 -0
- package/dist/rsc.js +176 -0
- package/package.json +77 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rock Solid Digital
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,151 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🧬 @rocksoliddigital/payload-lineage
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The content side of lineage, from a Payload config, as two pages in the admin:
|
|
4
|
+
|
|
5
|
+
- **Lineage** (`/admin/lineage`): collections, globals and blocks, and the fields that link them,
|
|
6
|
+
read left to right from what each is built from to where it is used: `media` →
|
|
7
|
+
`block:mediaBlock` → `pages`. Hover a node to trace everything upstream and downstream; click it
|
|
8
|
+
for the fields behind each link.
|
|
9
|
+
- **Data model** (`/admin/lineage/schema`): an ERD, with a table per collection and global listing
|
|
10
|
+
its fields, and a line from each field that references another collection, labelled with its
|
|
11
|
+
cardinality:
|
|
12
|
+
|
|
13
|
+
| Label | Means |
|
|
14
|
+
| -------------- | ------------------------------------------------------------------------- |
|
|
15
|
+
| `many-to-one` | One reference per document, which many documents can share |
|
|
16
|
+
| `many-to-many` | A list of references: `hasMany`, or inside an array or blocks field |
|
|
17
|
+
| `one-to-one` | A `unique` reference, or a global's single one (a global is one document) |
|
|
18
|
+
| `one-to-many` | A global's list of references |
|
|
19
|
+
|
|
20
|
+
Both are built from the running config on each visit (well under a millisecond), so they are never
|
|
21
|
+
out of date. A "Lineage" link in the nav opens them. To try them, run the
|
|
22
|
+
[example](https://github.com/rock-solid/lineage-kit/tree/main/examples/payload).
|
|
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
|
+
|
|
28
|
+
Works with Payload 3 on Next.js 15 or 16, with React 19.
|
|
29
|
+
|
|
30
|
+
> **Status:** early. Both pages come from the config only. Which pages use which blocks (from the
|
|
31
|
+
> documents), shared field configs (by import scan) and merging into the Storybook page are next;
|
|
32
|
+
> see the [roadmap](https://github.com/rock-solid/lineage-kit/blob/main/ROADMAP.md#-payload-plugin).
|
|
33
|
+
|
|
34
|
+
## 📥 Install
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pnpm add @rocksoliddigital/payload-lineage
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## ⚙️ Set up
|
|
41
|
+
|
|
42
|
+
Add the plugin to `payload.config.ts`:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { lineagePlugin } from '@rocksoliddigital/payload-lineage';
|
|
46
|
+
|
|
47
|
+
export default buildConfig({
|
|
48
|
+
// ...
|
|
49
|
+
plugins: [lineagePlugin()],
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Then update the import map, so the admin can find the pages. `next dev` does this on its own; for a
|
|
54
|
+
build, run:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
pnpm payload generate:importmap
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
| Option | Default | |
|
|
61
|
+
| ------------ | ------- | --------------------------------------------------------------------------------- |
|
|
62
|
+
| `adminView` | `true` | The two admin pages and the nav link |
|
|
63
|
+
| `access` | none | `({ user, payload }) => boolean`, on top of admin access; see [Access](#-access) |
|
|
64
|
+
| `outputFile` | none | Where to write the lineage graph as JSON on each start, outside production only |
|
|
65
|
+
| `exclude` | `[]` | Slugs to leave out of both pages and the file; `block:text` names only that block |
|
|
66
|
+
| `enabled` | `true` | Set to `false` to turn the plugin off |
|
|
67
|
+
|
|
68
|
+
Payload's own collections (`payload-preferences`, `payload-migrations`, ...) are always left out. If
|
|
69
|
+
the file can't be written (e.g. a read-only filesystem), Payload logs a warning and starts anyway.
|
|
70
|
+
|
|
71
|
+
To leave out form-builder's field blocks, which crowd the lineage graph:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
lineagePlugin({
|
|
75
|
+
exclude: [
|
|
76
|
+
'checkbox',
|
|
77
|
+
'country',
|
|
78
|
+
'email',
|
|
79
|
+
'message',
|
|
80
|
+
'number',
|
|
81
|
+
'select',
|
|
82
|
+
'state',
|
|
83
|
+
'text',
|
|
84
|
+
'textarea',
|
|
85
|
+
].map((s) => `block:${s}`),
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Or build either yourself, e.g. in a script, and keep a diagram in your docs up to date:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import {
|
|
93
|
+
buildContentGraph,
|
|
94
|
+
buildSchema,
|
|
95
|
+
contentGraphToMermaid,
|
|
96
|
+
schemaToMermaid,
|
|
97
|
+
} from '@rocksoliddigital/payload-lineage';
|
|
98
|
+
import config from '@payload-config';
|
|
99
|
+
|
|
100
|
+
const graph = buildContentGraph(await config);
|
|
101
|
+
const schema = buildSchema(await config);
|
|
102
|
+
const diagram = schemaToMermaid(schema); // or contentGraphToMermaid(graph)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 🔒 Access
|
|
106
|
+
|
|
107
|
+
The pages and their nav link are for users who can use the admin (Payload's `access.admin` on the
|
|
108
|
+
users collection, which Payload itself doesn't check on custom pages), and who pass the plugin's
|
|
109
|
+
`access`, if set:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
lineagePlugin({ access: ({ user }) => user.role === 'admin' });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Anyone else is sent where Payload sends them for its own pages: the login page when logged out, the
|
|
116
|
+
unauthorized page when logged in, each with a `redirect` back. The pages show only the collections
|
|
117
|
+
and globals the user can read, along with every link to or from them; blocks have no access control
|
|
118
|
+
of their own, so they're all shown. Everything is checked and built on the server, so nothing a user
|
|
119
|
+
can't see reaches the browser.
|
|
120
|
+
|
|
121
|
+
## 📄 The lineage file
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"nodes": [
|
|
126
|
+
{ "id": "collection:media", "kind": "collection", "slug": "media", "label": "Media" },
|
|
127
|
+
{ "id": "block:mediaBlock", "kind": "block", "slug": "mediaBlock", "label": "Media Block" }
|
|
128
|
+
],
|
|
129
|
+
"edges": [
|
|
130
|
+
{
|
|
131
|
+
"source": "collection:media",
|
|
132
|
+
"target": "block:mediaBlock",
|
|
133
|
+
"kind": "upload",
|
|
134
|
+
"fields": ["media"]
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Node ids are `<kind>:<slug>`, so they can't clash with the file paths the Storybook addon uses as
|
|
141
|
+
ids. `source` is the dependency and `target` what uses it, as in the Storybook addon. Join fields are
|
|
142
|
+
left out, since each is the reverse of a relationship that is already an edge. A collection that
|
|
143
|
+
links to itself, like nested-docs' `parent`, keeps the edge to itself.
|
|
144
|
+
|
|
145
|
+
## 🗂️ The data model
|
|
146
|
+
|
|
147
|
+
`buildSchema` flattens groups, rows, collapsibles and tabs into dotted rows (`hero.media`), and keeps
|
|
148
|
+
each array and blocks field as one row (`gallery[]`, `layout`). A reference inside one of those
|
|
149
|
+
attaches to that row, with its full path: `layout[mediaBlock].media`. Hidden fields (an auth
|
|
150
|
+
collection's `salt` and `hash`) and UI fields are left out. A join field is a row but not a line,
|
|
151
|
+
since it reverses a relationship that already is one.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// src/graph.ts
|
|
2
|
+
var INTERNAL_COLLECTION = /^payload-/;
|
|
3
|
+
function buildContentGraph(config, { exclude = [] } = {}) {
|
|
4
|
+
const excluded = (kind, slug) => kind === "collection" && INTERNAL_COLLECTION.test(slug) || exclude.includes(slug) || exclude.includes(`${kind}:${slug}`);
|
|
5
|
+
const collections = (config.collections ?? []).filter((c) => !excluded("collection", c.slug));
|
|
6
|
+
const globals = (config.globals ?? []).filter((g) => !excluded("global", g.slug));
|
|
7
|
+
const sharedBlocks = new Map((config.blocks ?? []).map((b) => [b.slug, b]));
|
|
8
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
9
|
+
const addNode = (kind, slug, label) => {
|
|
10
|
+
const id = `${kind}:${slug}`;
|
|
11
|
+
if (!nodes.has(id)) nodes.set(id, { id, kind, slug, label });
|
|
12
|
+
return id;
|
|
13
|
+
};
|
|
14
|
+
for (const c of collections) addNode("collection", c.slug, labelOf(c, c.labels?.plural));
|
|
15
|
+
for (const g of globals) addNode("global", g.slug, labelOf(g, g.label));
|
|
16
|
+
const collectionIds = new Set(collections.map((c) => `collection:${c.slug}`));
|
|
17
|
+
const edges = /* @__PURE__ */ new Map();
|
|
18
|
+
const addEdge = (source, target, kind, field) => {
|
|
19
|
+
const key = `${source}\u2192${target}\u2192${kind}`;
|
|
20
|
+
const edge = edges.get(key) ?? { source, target, kind, fields: [] };
|
|
21
|
+
if (!edge.fields.includes(field)) edge.fields.push(field);
|
|
22
|
+
edges.set(key, edge);
|
|
23
|
+
};
|
|
24
|
+
const walkedBlocks = /* @__PURE__ */ new Set();
|
|
25
|
+
const walkBlock = (block) => {
|
|
26
|
+
const id = addNode("block", block.slug, labelOf(block, block.labels?.singular));
|
|
27
|
+
if (walkedBlocks.has(block)) return id;
|
|
28
|
+
walkedBlocks.add(block);
|
|
29
|
+
walkFields(block.fields, id, []);
|
|
30
|
+
return id;
|
|
31
|
+
};
|
|
32
|
+
const walkFields = (fields, owner, path) => {
|
|
33
|
+
for (const field of fields) {
|
|
34
|
+
const fieldPath = field.name ? [...path, field.name] : path;
|
|
35
|
+
const dotted = fieldPath.join(".");
|
|
36
|
+
if (field.type === "relationship" || field.type === "upload") {
|
|
37
|
+
for (const slug of toArray(field.relationTo)) {
|
|
38
|
+
const source = `collection:${slug}`;
|
|
39
|
+
if (collectionIds.has(source)) addEdge(source, owner, field.type, dotted);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (field.type === "blocks") {
|
|
43
|
+
const blocks = [
|
|
44
|
+
...field.blocks ?? [],
|
|
45
|
+
...(field.blockReferences ?? []).map(
|
|
46
|
+
(b) => typeof b === "string" ? sharedBlocks.get(b) : b
|
|
47
|
+
)
|
|
48
|
+
];
|
|
49
|
+
for (const block of blocks) {
|
|
50
|
+
if (block && !excluded("block", block.slug)) {
|
|
51
|
+
addEdge(walkBlock(block), owner, "blocks", dotted);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (field.fields) walkFields(field.fields, owner, fieldPath);
|
|
56
|
+
for (const tab of field.tabs ?? []) {
|
|
57
|
+
walkFields(tab.fields, owner, tab.name ? [...fieldPath, tab.name] : fieldPath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
for (const c of collections) walkFields(c.fields, `collection:${c.slug}`, []);
|
|
62
|
+
for (const g of globals) walkFields(g.fields, `global:${g.slug}`, []);
|
|
63
|
+
return { nodes: [...nodes.values()], edges: [...edges.values()] };
|
|
64
|
+
}
|
|
65
|
+
function labelOf(entity, label) {
|
|
66
|
+
return typeof label === "string" ? label : entity.slug;
|
|
67
|
+
}
|
|
68
|
+
function toArray(value) {
|
|
69
|
+
if (value === void 0) return [];
|
|
70
|
+
return Array.isArray(value) ? value : [value];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/schema.ts
|
|
74
|
+
var INTERNAL_COLLECTION2 = /^payload-/;
|
|
75
|
+
var LAYOUT = /* @__PURE__ */ new Set(["row", "collapsible"]);
|
|
76
|
+
function buildSchema(config, { exclude = [] } = {}) {
|
|
77
|
+
const { collections = [], globals = [], blocks = [] } = config;
|
|
78
|
+
const excluded = (kind, slug) => kind === "collection" && INTERNAL_COLLECTION2.test(slug) || exclude.includes(slug) || exclude.includes(`${kind}:${slug}`);
|
|
79
|
+
const shownCollections = collections.filter((c) => !excluded("collection", c.slug));
|
|
80
|
+
const shownGlobals = globals.filter((g) => !excluded("global", g.slug));
|
|
81
|
+
const targets = new Set(shownCollections.map((c) => c.slug));
|
|
82
|
+
const sharedBlocks = new Map(blocks.map((b) => [b.slug, b]));
|
|
83
|
+
const relations = [];
|
|
84
|
+
const entity = (kind, e) => {
|
|
85
|
+
const id = `${kind}:${e.slug}`;
|
|
86
|
+
const fields = [];
|
|
87
|
+
const onPath = /* @__PURE__ */ new Set();
|
|
88
|
+
const walk = (list, path, row, many) => {
|
|
89
|
+
for (const field of list) {
|
|
90
|
+
if (field.type === "ui" || field.hidden) continue;
|
|
91
|
+
if (LAYOUT.has(field.type) || field.type === "group" && !field.name) {
|
|
92
|
+
walk(field.fields ?? [], path, row, many);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (field.type === "tabs") {
|
|
96
|
+
for (const tab of field.tabs ?? []) {
|
|
97
|
+
walk(tab.fields, tab.name ? join(path, tab.name) : path, row, many);
|
|
98
|
+
}
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const name = join(path, field.name ?? field.type);
|
|
102
|
+
if (field.type === "group") {
|
|
103
|
+
walk(field.fields ?? [], name, row, many);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (field.type === "array") {
|
|
107
|
+
const own2 = row ?? add({ name: `${name}[]`, type: "array", required: field.required });
|
|
108
|
+
walk(field.fields ?? [], `${name}[]`, own2, true);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (field.type === "blocks") {
|
|
112
|
+
const allowed = [
|
|
113
|
+
...field.blocks ?? [],
|
|
114
|
+
...(field.blockReferences ?? []).map(
|
|
115
|
+
(b) => typeof b === "string" ? sharedBlocks.get(b) : b
|
|
116
|
+
)
|
|
117
|
+
].filter((b) => b !== void 0 && !excluded("block", b.slug));
|
|
118
|
+
const own2 = row ?? add({
|
|
119
|
+
name,
|
|
120
|
+
type: "blocks",
|
|
121
|
+
required: field.required,
|
|
122
|
+
blocks: allowed.map((b) => b.slug)
|
|
123
|
+
});
|
|
124
|
+
for (const block of allowed) {
|
|
125
|
+
if (onPath.has(block)) continue;
|
|
126
|
+
onPath.add(block);
|
|
127
|
+
walk(block.fields, `${name}[${block.slug}]`, own2, true);
|
|
128
|
+
onPath.delete(block);
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const own = row ?? add({
|
|
133
|
+
name,
|
|
134
|
+
type: field.type,
|
|
135
|
+
required: field.required,
|
|
136
|
+
hasMany: field.hasMany,
|
|
137
|
+
localized: field.localized,
|
|
138
|
+
unique: field.unique,
|
|
139
|
+
...(field.relationTo || field.collection) && {
|
|
140
|
+
relationTo: toArray2(field.relationTo ?? field.collection)
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
if (field.type === "relationship" || field.type === "upload") {
|
|
144
|
+
const to = toArray2(field.relationTo);
|
|
145
|
+
for (const slug of to) {
|
|
146
|
+
if (!targets.has(slug)) continue;
|
|
147
|
+
relations.push({
|
|
148
|
+
from: id,
|
|
149
|
+
field: own.name,
|
|
150
|
+
path: name,
|
|
151
|
+
to: `collection:${slug}`,
|
|
152
|
+
kind: field.type,
|
|
153
|
+
many: many || !!field.hasMany,
|
|
154
|
+
polymorphic: to.length > 1,
|
|
155
|
+
unique: !!field.unique
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const add = (field) => {
|
|
162
|
+
const clean = Object.fromEntries(
|
|
163
|
+
Object.entries(field).filter(([, v]) => v !== void 0 && v !== false)
|
|
164
|
+
);
|
|
165
|
+
fields.push(clean);
|
|
166
|
+
return clean;
|
|
167
|
+
};
|
|
168
|
+
walk(e.fields, "", null, false);
|
|
169
|
+
if (kind === "collection" && !fields.some((f) => f.name === "id")) {
|
|
170
|
+
fields.unshift({ name: "id", type: "id", required: true });
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
id,
|
|
174
|
+
kind,
|
|
175
|
+
slug: e.slug,
|
|
176
|
+
label: labelOf2(e, kind === "collection" ? e.labels?.plural : e.label),
|
|
177
|
+
singular: labelOf2(e, kind === "collection" ? e.labels?.singular : e.label),
|
|
178
|
+
...kind === "collection" && e.auth ? { auth: true } : {},
|
|
179
|
+
...kind === "collection" && e.upload ? { upload: true } : {},
|
|
180
|
+
fields
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
const entities = [
|
|
184
|
+
...shownCollections.map((c) => entity("collection", c)),
|
|
185
|
+
...shownGlobals.map((g) => entity("global", g))
|
|
186
|
+
];
|
|
187
|
+
return { entities, relations };
|
|
188
|
+
}
|
|
189
|
+
function join(path, name) {
|
|
190
|
+
return path ? `${path}.${name}` : name;
|
|
191
|
+
}
|
|
192
|
+
function labelOf2(entity, label) {
|
|
193
|
+
return typeof label === "string" ? label : entity.slug;
|
|
194
|
+
}
|
|
195
|
+
function toArray2(value) {
|
|
196
|
+
if (value === void 0) return [];
|
|
197
|
+
return Array.isArray(value) ? value : [value];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
buildContentGraph,
|
|
202
|
+
buildSchema
|
|
203
|
+
};
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
type ContentKind = 'collection' | 'global' | 'block';
|
|
4
|
+
type ContentNode = {
|
|
5
|
+
/** `<kind>:<slug>`, e.g. `collection:pages` or `block:hero`. */
|
|
6
|
+
id: string;
|
|
7
|
+
kind: ContentKind;
|
|
8
|
+
slug: string;
|
|
9
|
+
label: string;
|
|
10
|
+
};
|
|
11
|
+
/** What makes one node depend on another. */
|
|
12
|
+
type ContentEdgeKind = 'relationship' | 'upload' | 'blocks';
|
|
13
|
+
/**
|
|
14
|
+
* `source` is the dependency, `target` the collection, global or block whose fields point at it,
|
|
15
|
+
* so the graph reads left to right like the Storybook addon's: `media` → `pages`.
|
|
16
|
+
*/
|
|
17
|
+
type ContentEdge = {
|
|
18
|
+
source: string;
|
|
19
|
+
target: string;
|
|
20
|
+
kind: ContentEdgeKind;
|
|
21
|
+
/** Dotted paths of the fields that make the link, e.g. `layout` or `hero.image`. */
|
|
22
|
+
fields: string[];
|
|
23
|
+
};
|
|
24
|
+
type ContentGraph = {
|
|
25
|
+
nodes: ContentNode[];
|
|
26
|
+
edges: ContentEdge[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type SchemaField = {
|
|
30
|
+
/** Dotted from the top level; arrays end in `[]`, e.g. `hero.media` or `gallery[]`. */
|
|
31
|
+
name: string;
|
|
32
|
+
type: string;
|
|
33
|
+
required?: boolean;
|
|
34
|
+
/** A relationship, upload, select, text or number field holding a list. */
|
|
35
|
+
hasMany?: boolean;
|
|
36
|
+
localized?: boolean;
|
|
37
|
+
unique?: boolean;
|
|
38
|
+
/** Collections a relationship, upload or join field points at. */
|
|
39
|
+
relationTo?: string[];
|
|
40
|
+
/** Block slugs a blocks field allows. */
|
|
41
|
+
blocks?: string[];
|
|
42
|
+
};
|
|
43
|
+
type SchemaEntity = {
|
|
44
|
+
/** `collection:<slug>` or `global:<slug>`, as in the content graph. */
|
|
45
|
+
id: string;
|
|
46
|
+
kind: 'collection' | 'global';
|
|
47
|
+
slug: string;
|
|
48
|
+
/** Plural for a collection (`Pages`), the global's name for a global. */
|
|
49
|
+
label: string;
|
|
50
|
+
/** One document: `Page`. A global's name, since it is one already. */
|
|
51
|
+
singular: string;
|
|
52
|
+
/** An auth collection (users) or an upload collection (media). */
|
|
53
|
+
auth?: boolean;
|
|
54
|
+
upload?: boolean;
|
|
55
|
+
fields: SchemaField[];
|
|
56
|
+
};
|
|
57
|
+
type SchemaRelation = {
|
|
58
|
+
/** The entity holding the reference. */
|
|
59
|
+
from: string;
|
|
60
|
+
/** The row in `from` it belongs to: the field itself, or the array or blocks field around it. */
|
|
61
|
+
field: string;
|
|
62
|
+
/** The full path of the referencing field, e.g. `layout[mediaBlock].media`. */
|
|
63
|
+
path: string;
|
|
64
|
+
/** The collection referenced. */
|
|
65
|
+
to: string;
|
|
66
|
+
kind: 'relationship' | 'upload';
|
|
67
|
+
/** Many references per document: `hasMany`, or inside an array or blocks field. */
|
|
68
|
+
many: boolean;
|
|
69
|
+
/** The field can point at more than one collection. */
|
|
70
|
+
polymorphic: boolean;
|
|
71
|
+
/** No two documents may reference the same one: a `unique` field. */
|
|
72
|
+
unique: boolean;
|
|
73
|
+
};
|
|
74
|
+
type Schema = {
|
|
75
|
+
entities: SchemaEntity[];
|
|
76
|
+
relations: SchemaRelation[];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type ErdCanvasProps = {
|
|
80
|
+
schema: Schema;
|
|
81
|
+
/** Admin URLs of the collections and globals, by entity id, for the "Open" links. */
|
|
82
|
+
adminURLs: Record<string, string>;
|
|
83
|
+
};
|
|
84
|
+
/** The data model as an ERD: a table per collection and global, a line per reference. */
|
|
85
|
+
declare const ErdCanvas: React.FC<ErdCanvasProps>;
|
|
86
|
+
|
|
87
|
+
type LineageCanvasProps = {
|
|
88
|
+
graph: ContentGraph;
|
|
89
|
+
/** Admin URLs of the collections and globals, by node id, for the "Open" links. */
|
|
90
|
+
adminURLs: Record<string, string>;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* The content graph, left to right from what an entity is built from to where it is used. Hover to
|
|
94
|
+
* trace, click for the fields behind each link.
|
|
95
|
+
*/
|
|
96
|
+
declare const LineageCanvas: React.FC<LineageCanvasProps>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* "Lineage" in the admin's nav, after the collections and globals, marked like theirs on either
|
|
100
|
+
* lineage page. Rendered by the server component `LineageNavLink`, which checks access first.
|
|
101
|
+
*/
|
|
102
|
+
declare const LineageNavLinkClient: React.FC;
|
|
103
|
+
|
|
104
|
+
export { ErdCanvas, type ErdCanvasProps, LineageCanvas, type LineageCanvasProps, LineageNavLinkClient };
|