payload-mcp-toolkit 0.2.0 → 0.3.1
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 +150 -133
- package/dist/__tests__/introspection.test.js +141 -46
- package/dist/__tests__/introspection.test.js.map +1 -1
- package/dist/draft-workflow.d.ts +24 -19
- package/dist/draft-workflow.js +89 -42
- package/dist/draft-workflow.js.map +1 -1
- package/dist/index.d.ts +10 -15
- package/dist/index.js +36 -76
- package/dist/index.js.map +1 -1
- package/dist/introspection.d.ts +18 -7
- package/dist/introspection.js +113 -84
- package/dist/introspection.js.map +1 -1
- package/dist/prompts.d.ts +4 -4
- package/dist/prompts.js +47 -38
- package/dist/prompts.js.map +1 -1
- package/dist/resources.d.ts +9 -4
- package/dist/resources.js +43 -58
- package/dist/resources.js.map +1 -1
- package/dist/tools/_helpers.d.ts +14 -0
- package/dist/tools/_helpers.js +35 -0
- package/dist/tools/_helpers.js.map +1 -0
- package/dist/tools/patch-layout.d.ts +15 -85
- package/dist/tools/patch-layout.js +142 -69
- package/dist/tools/patch-layout.js.map +1 -1
- package/dist/tools/publish-draft.d.ts +1 -11
- package/dist/tools/publish-draft.js +8 -39
- package/dist/tools/publish-draft.js.map +1 -1
- package/dist/tools/resolve-reference.d.ts +1 -12
- package/dist/tools/resolve-reference.js +45 -85
- package/dist/tools/resolve-reference.js.map +1 -1
- package/dist/tools/safe-delete.d.ts +8 -13
- package/dist/tools/safe-delete.js +68 -100
- package/dist/tools/safe-delete.js.map +1 -1
- package/dist/tools/schedule-publish.d.ts +11 -21
- package/dist/tools/schedule-publish.js +18 -61
- package/dist/tools/schedule-publish.js.map +1 -1
- package/dist/tools/search-content.d.ts +1 -6
- package/dist/tools/search-content.js +52 -64
- package/dist/tools/search-content.js.map +1 -1
- package/dist/tools/update-document.d.ts +4 -14
- package/dist/tools/update-document.js +23 -72
- package/dist/tools/update-document.js.map +1 -1
- package/dist/tools/upload-media.d.ts +1 -10
- package/dist/tools/upload-media.js +11 -54
- package/dist/tools/upload-media.js.map +1 -1
- package/dist/tools/versions.d.ts +7 -20
- package/dist/tools/versions.js +25 -82
- package/dist/tools/versions.js.map +1 -1
- package/dist/types.d.ts +82 -53
- package/dist/types.js +6 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/dist/rate-limiter.d.ts +0 -25
- package/dist/rate-limiter.js +0 -51
- package/dist/rate-limiter.js.map +0 -1
- package/dist/tools/compose-helpers.d.ts +0 -117
- package/dist/tools/compose-helpers.js +0 -236
- package/dist/tools/compose-helpers.js.map +0 -1
- package/dist/tools/compose-layout.d.ts +0 -139
- package/dist/tools/compose-layout.js +0 -61
- package/dist/tools/compose-layout.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,133 +1,150 @@
|
|
|
1
|
-
# payload-mcp-toolkit
|
|
2
|
-
|
|
3
|
-
> Schema-aware MCP toolkit for Payload CMS — wraps the official [`@payloadcms/plugin-mcp`](https://github.com/payloadcms/payload/tree/main/packages/plugin-mcp) with introspected prompts, resources, draft workflow, and AI-friendly tools so non-technical editors can manage content via AI chat.
|
|
4
|
-
|
|
5
|
-
## What it does
|
|
6
|
-
|
|
7
|
-
The official Payload MCP plugin gives every collection a generic CRUD surface. That works, but an LLM driving it has no idea:
|
|
8
|
-
|
|
9
|
-
- which collections support drafts vs publish-immediately,
|
|
10
|
-
- which block types are valid inside which sections,
|
|
11
|
-
- which fields are searchable for resolving relationships,
|
|
12
|
-
- how to compose a page layout without trial and error.
|
|
13
|
-
|
|
14
|
-
`payload-mcp-toolkit` introspects the Payload config at boot, then layers schema-aware **prompts**, **resources**, and **tools** on top of the official plugin so an AI client (Claude Desktop, Claude API, any MCP-compatible chat) can drive your CMS confidently.
|
|
15
|
-
|
|
16
|
-
## What it adds
|
|
17
|
-
|
|
18
|
-
**Auto-generated prompts** (no setup required):
|
|
19
|
-
- `contentModelOverview` — every collection, fields, and relationships.
|
|
20
|
-
- `blockCompositionGuide` — section/leaf hierarchy and nesting rules.
|
|
21
|
-
- `draftWorkflowGuide` — which collections need `publishDraft` to go live.
|
|
22
|
-
|
|
23
|
-
**Auto-generated resources** (machine-readable JSON for the LLM):
|
|
24
|
-
- `blocks://catalog`, `collections://schema`, `collections://relationships`.
|
|
25
|
-
|
|
26
|
-
**Custom tools
|
|
27
|
-
|
|
28
|
-
*Authoring*
|
|
29
|
-
- `
|
|
30
|
-
- `
|
|
31
|
-
- `
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- `
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
- `
|
|
40
|
-
- `
|
|
41
|
-
- `
|
|
42
|
-
- `
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
1
|
+
# payload-mcp-toolkit
|
|
2
|
+
|
|
3
|
+
> Schema-aware MCP toolkit for Payload CMS — wraps the official [`@payloadcms/plugin-mcp`](https://github.com/payloadcms/payload/tree/main/packages/plugin-mcp) with introspected prompts, resources, draft workflow, and AI-friendly tools so non-technical editors can manage content via AI chat.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
The official Payload MCP plugin gives every collection a generic CRUD surface. That works, but an LLM driving it has no idea:
|
|
8
|
+
|
|
9
|
+
- which collections support drafts vs publish-immediately,
|
|
10
|
+
- which block types are valid inside which sections,
|
|
11
|
+
- which fields are searchable for resolving relationships,
|
|
12
|
+
- how to compose a page layout without trial and error.
|
|
13
|
+
|
|
14
|
+
`payload-mcp-toolkit` introspects the Payload config at boot, then layers schema-aware **prompts**, **resources**, and **tools** on top of the official plugin so an AI client (Claude Desktop, Claude API, any MCP-compatible chat) can drive your CMS confidently.
|
|
15
|
+
|
|
16
|
+
## What it adds
|
|
17
|
+
|
|
18
|
+
**Auto-generated prompts** (no setup required):
|
|
19
|
+
- `contentModelOverview` — every collection, fields, and relationships.
|
|
20
|
+
- `blockCompositionGuide` — section/leaf hierarchy and nesting rules.
|
|
21
|
+
- `draftWorkflowGuide` — which collections need `publishDraft` to go live.
|
|
22
|
+
|
|
23
|
+
**Auto-generated resources** (machine-readable JSON for the LLM):
|
|
24
|
+
- `blocks://catalog`, `collections://schema`, `collections://relationships`.
|
|
25
|
+
|
|
26
|
+
**Custom tools (10, plus an auto-registered scheduler)**
|
|
27
|
+
|
|
28
|
+
*Authoring*
|
|
29
|
+
- `patchLayout` — surgical append/prepend/insertAt/replaceAt against any blocks-typed field. Validates each block (recursively, at any depth) against the introspected nesting map. Safer than `updateDocument` for incremental layout edits.
|
|
30
|
+
- `updateDocument` — Local-API based update that survives the upload-field bug in the official plugin.
|
|
31
|
+
- `uploadMedia` — fetch a public HTTPS image, validate (SSRF-safe with streaming size cap), create a Media doc.
|
|
32
|
+
|
|
33
|
+
*Discovery*
|
|
34
|
+
- `resolveReference` — search collections by name/title/slug for relationship IDs.
|
|
35
|
+
- `searchContent` — natural-language editor triage. Filter by `status`, `olderThanDays` / `newerThanDays`, `missingFields`, free-text `query`, scoped to one collection or all.
|
|
36
|
+
|
|
37
|
+
*Lifecycle / safety*
|
|
38
|
+
- `publishDraft` — flip `_status` from draft to published.
|
|
39
|
+
- `schedulePublish` — **bring your own scheduler.** Stamps a future `publishedAt` on a draft and leaves `_status: 'draft'`; it does **not** itself flip status at the appointed time. Auto-registered only for collections that have both drafts AND a `publishedAt` date field. Wire up a [Payload Jobs Queue task](https://payloadcms.com/docs/jobs-queue/scheduled-jobs), external cron, or `beforeRead` hook to actually publish on schedule.
|
|
40
|
+
- `listVersions` — recent saved versions of a draft document.
|
|
41
|
+
- `restoreVersion` — roll a document back to a saved version (creates a new version on top, so reversible).
|
|
42
|
+
- `safeDelete` — relationship-aware delete. Walks the relationship graph, refuses with a structured impact summary if other documents reference the target. Fail-closed on permission errors. Override with `confirm: true`.
|
|
43
|
+
|
|
44
|
+
**Draft workflow** wired into the official plugin's `mcpCollections`:
|
|
45
|
+
- For collections with `versions.drafts` enabled, disables raw `update` so clients go through `publishDraft` / `patchLayout` / `updateDocument` (all of which preserve draft semantics).
|
|
46
|
+
- Appends preview URLs to draft responses by calling each collection's own `admin.livePreview.url` or `admin.preview` function — no separate path config needed.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pnpm add payload-mcp-toolkit @payloadcms/plugin-mcp
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Peer dependencies: `payload` ^3, `@payloadcms/plugin-mcp` ^3, `zod` ^3.
|
|
55
|
+
|
|
56
|
+
## Use — zero config
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// payload.config.ts
|
|
60
|
+
import { contentToolkitPlugin } from 'payload-mcp-toolkit'
|
|
61
|
+
|
|
62
|
+
export default buildConfig({
|
|
63
|
+
// ...your collections, blocks, globals
|
|
64
|
+
serverURL: process.env.SITE_URL, // used for absolute preview URLs
|
|
65
|
+
admin: { user: 'users' }, // your auth collection
|
|
66
|
+
plugins: [contentToolkitPlugin()],
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
That's it. The toolkit infers everything from your Payload config:
|
|
71
|
+
- **Draft behavior** — collections with `versions.drafts` get `always-draft` (raw update locked); others publish immediately.
|
|
72
|
+
- **Preview URLs** — pulled from each collection's `admin.livePreview.url` (or `admin.preview` as a fallback). If neither is set, draft responses just get a generic admin-panel hint.
|
|
73
|
+
- **Block nesting** — for every blocks-typed field, anywhere in the schema, the toolkit records which slugs are allowed. The AI composes layouts at any depth from that map.
|
|
74
|
+
- **Auth collection** — comes from `admin.user` (the standard Payload setting). The official plugin handles this directly.
|
|
75
|
+
|
|
76
|
+
## Optional configuration
|
|
77
|
+
|
|
78
|
+
Every option is an escape hatch — pass only what you need:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
contentToolkitPlugin({
|
|
82
|
+
preview: {
|
|
83
|
+
siteUrl: 'https://staging.example.com', // override serverURL
|
|
84
|
+
disabled: false, // set true to suppress preview URLs entirely
|
|
85
|
+
},
|
|
86
|
+
draftBehavior: {
|
|
87
|
+
posts: 'always-publish', // allow raw update on a draftable collection
|
|
88
|
+
},
|
|
89
|
+
userCollection: 'admins', // override admin.user
|
|
90
|
+
exclude: {
|
|
91
|
+
collections: ['internal-bookkeeping'],
|
|
92
|
+
globals: ['secret-config'],
|
|
93
|
+
},
|
|
94
|
+
mediaUpload: {
|
|
95
|
+
maxFileSize: 25 * 1024 * 1024,
|
|
96
|
+
collectionSlug: 'images',
|
|
97
|
+
},
|
|
98
|
+
domainPrompts: [
|
|
99
|
+
{
|
|
100
|
+
name: 'siteVocabulary',
|
|
101
|
+
title: 'Site Vocabulary',
|
|
102
|
+
description: 'Site-specific terms the AI should know.',
|
|
103
|
+
content: '...',
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
| Option | Description |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `preview.siteUrl` | Base URL for preview links. Defaults to `serverURL`, then `NEXT_PUBLIC_SERVER_URL`/`SITE_URL` env vars. |
|
|
112
|
+
| `preview.disabled` | Suppress preview URL injection on draft responses. |
|
|
113
|
+
| `draftBehavior` | Per-collection override of inferred behavior. |
|
|
114
|
+
| `userCollection` | Override `admin.user` for API key linkage. |
|
|
115
|
+
| `exclude.collections` / `exclude.globals` | Hide from MCP exposure. |
|
|
116
|
+
| `domainPrompts` | Site-specific vocabulary prompts. |
|
|
117
|
+
| `mediaUpload.maxFileSize` | Default 10MB. Enforced as a streaming cap, not a post-buffer check. |
|
|
118
|
+
| `mediaUpload.collectionSlug` | Default `'media'`. |
|
|
119
|
+
|
|
120
|
+
## Development
|
|
121
|
+
|
|
122
|
+
This package follows the [official Payload 3 plugin template](https://github.com/payloadcms/payload/tree/main/templates/plugin) layout: source in `src/`, a fully-working Payload + Next.js app in `dev/`, source-export `package.json` so the dev harness consumes the plugin directly without a build step.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pnpm install
|
|
126
|
+
cp dev/.env.example dev/.env
|
|
127
|
+
pnpm dev # boot dev/ Next.js + Payload at http://localhost:3000
|
|
128
|
+
pnpm test # vitest — runs introspection unit tests
|
|
129
|
+
pnpm build # produce dist/ for npm publish
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The dev harness ships with a realistic CMS schema:
|
|
133
|
+
- `Pages` — block-based layout (FullWidth, TwoColumn, CtaBanner, HeadingOnly), drafts enabled.
|
|
134
|
+
- `Posts` — title/slug/excerpt/content/cover/category/authors/tags/SEO, drafts enabled.
|
|
135
|
+
- `Authors`, `Categories`, `Media`, `Users` — taxonomy + auth.
|
|
136
|
+
- `SiteSettings` — global with site name, logo, social, footer.
|
|
137
|
+
- 5 leaf blocks (Heading, RichText, Image, ButtonGroup, Quote) and 4 section blocks.
|
|
138
|
+
|
|
139
|
+
Seed sample content:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Generate the admin import map first time:
|
|
143
|
+
pnpm dev:generate-importmap
|
|
144
|
+
|
|
145
|
+
# Then visit http://localhost:3000/admin and create your first user.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { introspectCollection, introspectCollections, introspectBlocks, buildRelationshipGraph } from '../introspection';
|
|
2
|
+
import { introspectCollection, introspectCollections, introspectBlocks, buildBlockNestingMap, buildRelationshipGraph } from '../introspection';
|
|
3
3
|
// ─── Sample schema (kept inline so the test is self-contained) ─────
|
|
4
4
|
const Media = {
|
|
5
5
|
slug: 'media',
|
|
@@ -47,7 +47,7 @@ const Authors = {
|
|
|
47
47
|
}
|
|
48
48
|
]
|
|
49
49
|
};
|
|
50
|
-
// Leaf blocks
|
|
50
|
+
// Leaf-style blocks
|
|
51
51
|
const Heading = {
|
|
52
52
|
slug: 'heading',
|
|
53
53
|
fields: [
|
|
@@ -102,19 +102,18 @@ const ImageBlock = {
|
|
|
102
102
|
}
|
|
103
103
|
]
|
|
104
104
|
};
|
|
105
|
-
|
|
106
|
-
Heading,
|
|
107
|
-
RichText,
|
|
108
|
-
ImageBlock
|
|
109
|
-
];
|
|
110
|
-
// Section blocks
|
|
105
|
+
// Container-style blocks (have nested blocks fields)
|
|
111
106
|
const FullWidth = {
|
|
112
107
|
slug: 'fullWidth',
|
|
113
108
|
fields: [
|
|
114
109
|
{
|
|
115
110
|
name: 'content',
|
|
116
111
|
type: 'blocks',
|
|
117
|
-
blocks:
|
|
112
|
+
blocks: [
|
|
113
|
+
Heading,
|
|
114
|
+
RichText,
|
|
115
|
+
ImageBlock
|
|
116
|
+
]
|
|
118
117
|
}
|
|
119
118
|
]
|
|
120
119
|
};
|
|
@@ -149,10 +148,39 @@ const CtaBanner = {
|
|
|
149
148
|
}
|
|
150
149
|
]
|
|
151
150
|
};
|
|
152
|
-
|
|
151
|
+
// Deeply-nestable container — exercises the recursive path
|
|
152
|
+
const Accordion = {
|
|
153
|
+
slug: 'accordion',
|
|
154
|
+
fields: [
|
|
155
|
+
{
|
|
156
|
+
name: 'panels',
|
|
157
|
+
type: 'array',
|
|
158
|
+
fields: [
|
|
159
|
+
{
|
|
160
|
+
name: 'title',
|
|
161
|
+
type: 'text'
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'body',
|
|
165
|
+
type: 'blocks',
|
|
166
|
+
blocks: [
|
|
167
|
+
Heading,
|
|
168
|
+
RichText,
|
|
169
|
+
FullWidth
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
};
|
|
176
|
+
const allBlocks = [
|
|
177
|
+
Heading,
|
|
178
|
+
RichText,
|
|
179
|
+
ImageBlock,
|
|
153
180
|
FullWidth,
|
|
154
181
|
HeadingOnly,
|
|
155
|
-
CtaBanner
|
|
182
|
+
CtaBanner,
|
|
183
|
+
Accordion
|
|
156
184
|
];
|
|
157
185
|
const Posts = {
|
|
158
186
|
slug: 'posts',
|
|
@@ -242,7 +270,12 @@ const Pages = {
|
|
|
242
270
|
{
|
|
243
271
|
name: 'layout',
|
|
244
272
|
type: 'blocks',
|
|
245
|
-
blocks:
|
|
273
|
+
blocks: [
|
|
274
|
+
FullWidth,
|
|
275
|
+
HeadingOnly,
|
|
276
|
+
CtaBanner,
|
|
277
|
+
Accordion
|
|
278
|
+
]
|
|
246
279
|
}
|
|
247
280
|
]
|
|
248
281
|
}
|
|
@@ -250,7 +283,7 @@ const Pages = {
|
|
|
250
283
|
}
|
|
251
284
|
]
|
|
252
285
|
};
|
|
253
|
-
// ───
|
|
286
|
+
// ─── introspectCollection ──────────────────────────────────────────
|
|
254
287
|
describe('introspectCollection', ()=>{
|
|
255
288
|
it('extracts Posts collection fields, relationships, and draft status', ()=>{
|
|
256
289
|
const schema = introspectCollection(Posts);
|
|
@@ -292,44 +325,24 @@ describe('introspectCollection', ()=>{
|
|
|
292
325
|
expect(heroSize.options.length).toBe(3);
|
|
293
326
|
});
|
|
294
327
|
});
|
|
328
|
+
// ─── introspectBlocks (flat catalog) ───────────────────────────────
|
|
295
329
|
describe('introspectBlocks', ()=>{
|
|
296
|
-
it('
|
|
297
|
-
const catalog = introspectBlocks(
|
|
298
|
-
const
|
|
299
|
-
expect(
|
|
300
|
-
expect(fullWidth.nestingType).toBe('composable');
|
|
301
|
-
expect(fullWidth.acceptedLeafSlugs.length).toBe(allLeafBlocks.length);
|
|
302
|
-
});
|
|
303
|
-
it('marks ctaBanner as fixed (no nested blocks)', ()=>{
|
|
304
|
-
const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks);
|
|
305
|
-
const ctaBanner = catalog.sections.find((s)=>s.slug === 'ctaBanner');
|
|
306
|
-
expect(ctaBanner).toBeDefined();
|
|
307
|
-
expect(ctaBanner.nestingType).toBe('fixed');
|
|
308
|
-
expect(ctaBanner.acceptedLeafSlugs).toHaveLength(0);
|
|
309
|
-
});
|
|
310
|
-
it('marks headingOnly as constrained (single leaf type, maxRows: 1)', ()=>{
|
|
311
|
-
const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks);
|
|
312
|
-
const headingOnly = catalog.sections.find((s)=>s.slug === 'headingOnly');
|
|
313
|
-
expect(headingOnly).toBeDefined();
|
|
314
|
-
expect(headingOnly.nestingType).toBe('constrained');
|
|
315
|
-
expect(headingOnly.acceptedLeafSlugs).toEqual([
|
|
316
|
-
'heading'
|
|
317
|
-
]);
|
|
318
|
-
expect(headingOnly.maxRows).toBe(1);
|
|
319
|
-
});
|
|
320
|
-
it('extracts all leaf blocks', ()=>{
|
|
321
|
-
const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks);
|
|
322
|
-
expect(catalog.leaves).toHaveLength(3);
|
|
323
|
-
const leafSlugs = catalog.leaves.map((l)=>l.slug);
|
|
324
|
-
expect(leafSlugs).toEqual([
|
|
330
|
+
it('returns a flat catalog of every block with no section/leaf split', ()=>{
|
|
331
|
+
const catalog = introspectBlocks(allBlocks);
|
|
332
|
+
const slugs = catalog.blocks.map((b)=>b.slug);
|
|
333
|
+
expect(slugs).toEqual([
|
|
325
334
|
'heading',
|
|
326
335
|
'richText',
|
|
327
|
-
'image'
|
|
336
|
+
'image',
|
|
337
|
+
'fullWidth',
|
|
338
|
+
'headingOnly',
|
|
339
|
+
'ctaBanner',
|
|
340
|
+
'accordion'
|
|
328
341
|
]);
|
|
329
342
|
});
|
|
330
|
-
it('extracts
|
|
331
|
-
const catalog = introspectBlocks(
|
|
332
|
-
const heading = catalog.
|
|
343
|
+
it('extracts each block\'s fields including select options', ()=>{
|
|
344
|
+
const catalog = introspectBlocks(allBlocks);
|
|
345
|
+
const heading = catalog.blocks.find((b)=>b.slug === 'heading');
|
|
333
346
|
expect(heading).toBeDefined();
|
|
334
347
|
const headingFieldNames = heading.fields.map((f)=>f.name);
|
|
335
348
|
expect(headingFieldNames).toEqual([
|
|
@@ -341,6 +354,88 @@ describe('introspectBlocks', ()=>{
|
|
|
341
354
|
expect(level.options).toBeDefined();
|
|
342
355
|
});
|
|
343
356
|
});
|
|
357
|
+
// ─── buildBlockNestingMap ──────────────────────────────────────────
|
|
358
|
+
describe('buildBlockNestingMap', ()=>{
|
|
359
|
+
it('records the layout field on Pages with the slugs it accepts', ()=>{
|
|
360
|
+
const map = buildBlockNestingMap([
|
|
361
|
+
Pages,
|
|
362
|
+
Posts
|
|
363
|
+
], allBlocks);
|
|
364
|
+
const pageLayout = map.find((e)=>e.ownerType === 'collection' && e.owner === 'pages' && e.fieldPath === 'layout');
|
|
365
|
+
expect(pageLayout).toBeDefined();
|
|
366
|
+
expect(pageLayout.acceptedBlockSlugs).toEqual([
|
|
367
|
+
'fullWidth',
|
|
368
|
+
'headingOnly',
|
|
369
|
+
'ctaBanner',
|
|
370
|
+
'accordion'
|
|
371
|
+
]);
|
|
372
|
+
});
|
|
373
|
+
it('records nested blocks fields inside container blocks', ()=>{
|
|
374
|
+
const map = buildBlockNestingMap([
|
|
375
|
+
Pages
|
|
376
|
+
], allBlocks);
|
|
377
|
+
const fullWidthContent = map.find((e)=>e.ownerType === 'block' && e.owner === 'fullWidth' && e.fieldPath === 'content');
|
|
378
|
+
expect(fullWidthContent).toBeDefined();
|
|
379
|
+
expect(fullWidthContent.acceptedBlockSlugs).toEqual([
|
|
380
|
+
'heading',
|
|
381
|
+
'richText',
|
|
382
|
+
'image'
|
|
383
|
+
]);
|
|
384
|
+
const headingOnly = map.find((e)=>e.ownerType === 'block' && e.owner === 'headingOnly' && e.fieldPath === 'content');
|
|
385
|
+
expect(headingOnly.acceptedBlockSlugs).toEqual([
|
|
386
|
+
'heading'
|
|
387
|
+
]);
|
|
388
|
+
expect(headingOnly.maxRows).toBe(1);
|
|
389
|
+
});
|
|
390
|
+
it('handles arbitrarily-deep nesting via array fields inside blocks', ()=>{
|
|
391
|
+
const map = buildBlockNestingMap([
|
|
392
|
+
Pages
|
|
393
|
+
], allBlocks);
|
|
394
|
+
const accordionPanelBody = map.find((e)=>e.ownerType === 'block' && e.owner === 'accordion' && e.fieldPath === 'panels[].body');
|
|
395
|
+
expect(accordionPanelBody).toBeDefined();
|
|
396
|
+
expect(accordionPanelBody.acceptedBlockSlugs).toEqual([
|
|
397
|
+
'heading',
|
|
398
|
+
'richText',
|
|
399
|
+
'fullWidth'
|
|
400
|
+
]);
|
|
401
|
+
});
|
|
402
|
+
it('omits unknown slugs not present in the block list', ()=>{
|
|
403
|
+
const Stray = {
|
|
404
|
+
slug: 'stray',
|
|
405
|
+
fields: [
|
|
406
|
+
{
|
|
407
|
+
name: 'layout',
|
|
408
|
+
type: 'blocks',
|
|
409
|
+
blocks: [
|
|
410
|
+
Heading,
|
|
411
|
+
{
|
|
412
|
+
slug: 'mystery',
|
|
413
|
+
fields: []
|
|
414
|
+
}
|
|
415
|
+
]
|
|
416
|
+
}
|
|
417
|
+
]
|
|
418
|
+
};
|
|
419
|
+
const map = buildBlockNestingMap([
|
|
420
|
+
Stray
|
|
421
|
+
], [
|
|
422
|
+
Heading
|
|
423
|
+
]) // mystery not in catalog
|
|
424
|
+
;
|
|
425
|
+
const stray = map.find((e)=>e.owner === 'stray' && e.fieldPath === 'layout');
|
|
426
|
+
expect(stray.acceptedBlockSlugs).toEqual([
|
|
427
|
+
'heading'
|
|
428
|
+
]);
|
|
429
|
+
});
|
|
430
|
+
it('omits fixed blocks (no nested blocks fields) from the map', ()=>{
|
|
431
|
+
const map = buildBlockNestingMap([
|
|
432
|
+
Pages
|
|
433
|
+
], allBlocks);
|
|
434
|
+
const ctaEntries = map.filter((e)=>e.owner === 'ctaBanner');
|
|
435
|
+
expect(ctaEntries).toHaveLength(0);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
// ─── buildRelationshipGraph ────────────────────────────────────────
|
|
344
439
|
describe('buildRelationshipGraph', ()=>{
|
|
345
440
|
it('builds correct graph from sample collections', ()=>{
|
|
346
441
|
const schemas = introspectCollections([
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/__tests__/introspection.test.ts"],"sourcesContent":["import { describe, it, expect } from 'vitest'\nimport type { Block, CollectionConfig } from 'payload'\nimport {\n introspectCollection,\n introspectCollections,\n introspectBlocks,\n buildRelationshipGraph,\n} from '../introspection'\n\n// ─── Sample schema (kept inline so the test is self-contained) ─────\n\nconst Media: CollectionConfig = {\n slug: 'media',\n upload: true,\n fields: [{ name: 'alt', type: 'text', required: true }],\n}\n\nconst Categories: CollectionConfig = {\n slug: 'categories',\n fields: [\n { name: 'name', type: 'text', required: true },\n { name: 'slug', type: 'text', required: true },\n ],\n}\n\nconst Authors: CollectionConfig = {\n slug: 'authors',\n fields: [\n { name: 'name', type: 'text', required: true },\n { name: 'slug', type: 'text', required: true },\n { name: 'avatar', type: 'upload', relationTo: 'media' },\n ],\n}\n\n// Leaf blocks\nconst Heading: Block = {\n slug: 'heading',\n fields: [\n { name: 'text', type: 'text', required: true },\n {\n name: 'level',\n type: 'select',\n options: ['h1', 'h2', 'h3'],\n defaultValue: 'h2',\n },\n {\n name: 'align',\n type: 'select',\n options: ['left', 'center', 'right'],\n defaultValue: 'left',\n },\n ],\n}\n\nconst RichText: Block = {\n slug: 'richText',\n fields: [{ name: 'content', type: 'richText' }],\n}\n\nconst ImageBlock: Block = {\n slug: 'image',\n fields: [\n { name: 'image', type: 'upload', relationTo: 'media', required: true },\n { name: 'caption', type: 'text' },\n ],\n}\n\nconst allLeafBlocks: Block[] = [Heading, RichText, ImageBlock]\n\n// Section blocks\nconst FullWidth: Block = {\n slug: 'fullWidth',\n fields: [\n {\n name: 'content',\n type: 'blocks',\n blocks: allLeafBlocks,\n },\n ],\n}\n\nconst HeadingOnly: Block = {\n slug: 'headingOnly',\n fields: [\n {\n name: 'content',\n type: 'blocks',\n maxRows: 1,\n blocks: [Heading],\n },\n ],\n}\n\nconst CtaBanner: Block = {\n slug: 'ctaBanner',\n fields: [\n { name: 'headline', type: 'text', required: true },\n { name: 'buttonLabel', type: 'text' },\n { name: 'buttonHref', type: 'text' },\n ],\n}\n\nconst allSectionBlocks: Block[] = [FullWidth, HeadingOnly, CtaBanner]\n\nconst Posts: CollectionConfig = {\n slug: 'posts',\n versions: { drafts: true },\n fields: [\n { name: 'title', type: 'text', required: true },\n { name: 'slug', type: 'text', required: true },\n { name: 'featured', type: 'checkbox' },\n { name: 'category', type: 'relationship', relationTo: 'categories' },\n {\n name: 'authors',\n type: 'relationship',\n relationTo: 'authors',\n hasMany: true,\n },\n { name: 'coverImage', type: 'upload', relationTo: 'media' },\n {\n name: 'tags',\n type: 'array',\n fields: [{ name: 'tag', type: 'text' }],\n },\n ],\n}\n\nconst Pages: CollectionConfig = {\n slug: 'pages',\n versions: { drafts: true },\n fields: [\n {\n type: 'tabs',\n tabs: [\n {\n name: 'hero',\n label: 'Hero',\n fields: [\n { name: 'heroTitle', type: 'text' },\n {\n name: 'heroSize',\n type: 'select',\n options: ['small', 'medium', 'large'],\n defaultValue: 'medium',\n },\n ],\n },\n {\n label: 'Content',\n fields: [\n { name: 'slug', type: 'text', required: true },\n { name: 'layout', type: 'blocks', blocks: allSectionBlocks },\n ],\n },\n ],\n },\n ],\n}\n\n// ─── Tests ─────────────────────────────────────────────────────────\n\ndescribe('introspectCollection', () => {\n it('extracts Posts collection fields, relationships, and draft status', () => {\n const schema = introspectCollection(Posts)\n\n expect(schema.slug).toBe('posts')\n expect(schema.hasDrafts).toBe(true)\n\n const fieldNames = schema.fields.map((f) => f.name)\n expect(fieldNames).toContain('title')\n expect(fieldNames).toContain('slug')\n expect(fieldNames).toContain('featured')\n expect(fieldNames).toContain('tags')\n\n const relFieldNames = schema.relationships.map((r) => r.fieldName)\n expect(relFieldNames).toContain('category')\n expect(relFieldNames).toContain('authors')\n\n const cover = schema.relationships.find((r) => r.fieldName === 'coverImage')\n expect(cover).toBeDefined()\n expect(cover!.relationTo).toBe('media')\n\n expect(schema.searchableFields).toContain('title')\n expect(schema.searchableFields).toContain('slug')\n })\n\n it('extracts Pages collection with tab-nested fields', () => {\n const schema = introspectCollection(Pages)\n\n expect(schema.slug).toBe('pages')\n expect(schema.hasDrafts).toBe(true)\n\n const fieldNames = schema.fields.map((f) => f.name)\n expect(fieldNames).toContain('heroTitle')\n expect(fieldNames).toContain('slug')\n expect(fieldNames).toContain('layout')\n })\n\n it('detects collections without draft support', () => {\n const schema = introspectCollection(Categories)\n expect(schema.hasDrafts).toBe(false)\n })\n\n it('extracts select field options from Pages heroSize', () => {\n const schema = introspectCollection(Pages)\n const heroSize = schema.fields.find((f) => f.name === 'heroSize')\n expect(heroSize).toBeDefined()\n expect(heroSize!.type).toBe('select')\n expect(heroSize!.options).toBeDefined()\n expect(heroSize!.options!.length).toBe(3)\n })\n})\n\ndescribe('introspectBlocks', () => {\n it('discovers fullWidth accepts all leaf blocks (composable)', () => {\n const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks)\n\n const fullWidth = catalog.sections.find((s) => s.slug === 'fullWidth')\n expect(fullWidth).toBeDefined()\n expect(fullWidth!.nestingType).toBe('composable')\n expect(fullWidth!.acceptedLeafSlugs.length).toBe(allLeafBlocks.length)\n })\n\n it('marks ctaBanner as fixed (no nested blocks)', () => {\n const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks)\n\n const ctaBanner = catalog.sections.find((s) => s.slug === 'ctaBanner')\n expect(ctaBanner).toBeDefined()\n expect(ctaBanner!.nestingType).toBe('fixed')\n expect(ctaBanner!.acceptedLeafSlugs).toHaveLength(0)\n })\n\n it('marks headingOnly as constrained (single leaf type, maxRows: 1)', () => {\n const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks)\n\n const headingOnly = catalog.sections.find((s) => s.slug === 'headingOnly')\n expect(headingOnly).toBeDefined()\n expect(headingOnly!.nestingType).toBe('constrained')\n expect(headingOnly!.acceptedLeafSlugs).toEqual(['heading'])\n expect(headingOnly!.maxRows).toBe(1)\n })\n\n it('extracts all leaf blocks', () => {\n const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks)\n expect(catalog.leaves).toHaveLength(3)\n const leafSlugs = catalog.leaves.map((l) => l.slug)\n expect(leafSlugs).toEqual(['heading', 'richText', 'image'])\n })\n\n it('extracts leaf block fields including select options', () => {\n const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks)\n const heading = catalog.leaves.find((l) => l.slug === 'heading')\n expect(heading).toBeDefined()\n const headingFieldNames = heading!.fields.map((f) => f.name)\n expect(headingFieldNames).toEqual(['text', 'level', 'align'])\n const level = heading!.fields.find((f) => f.name === 'level')\n expect(level!.options).toBeDefined()\n })\n})\n\ndescribe('buildRelationshipGraph', () => {\n it('builds correct graph from sample collections', () => {\n const schemas = introspectCollections([Posts, Pages, Categories, Authors, Media])\n const edges = buildRelationshipGraph(schemas)\n\n const postEdges = edges.filter((e) => e.fromCollection === 'posts')\n const postTargets = postEdges.map((e) => e.toCollection)\n expect(postTargets).toContain('categories')\n expect(postTargets).toContain('authors')\n expect(postTargets).toContain('media')\n\n const authorEdges = edges.filter((e) => e.fromCollection === 'authors')\n expect(authorEdges.map((e) => e.toCollection)).toContain('media')\n })\n})\n"],"names":["describe","it","expect","introspectCollection","introspectCollections","introspectBlocks","buildRelationshipGraph","Media","slug","upload","fields","name","type","required","Categories","Authors","relationTo","Heading","options","defaultValue","RichText","ImageBlock","allLeafBlocks","FullWidth","blocks","HeadingOnly","maxRows","CtaBanner","allSectionBlocks","Posts","versions","drafts","hasMany","Pages","tabs","label","schema","toBe","hasDrafts","fieldNames","map","f","toContain","relFieldNames","relationships","r","fieldName","cover","find","toBeDefined","searchableFields","heroSize","length","catalog","fullWidth","sections","s","nestingType","acceptedLeafSlugs","ctaBanner","toHaveLength","headingOnly","toEqual","leaves","leafSlugs","l","heading","headingFieldNames","level","schemas","edges","postEdges","filter","e","fromCollection","postTargets","toCollection","authorEdges"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/__tests__/introspection.test.ts"],"sourcesContent":["import { describe, it, expect } from 'vitest'\r\nimport type { Block, CollectionConfig } from 'payload'\r\nimport {\r\n introspectCollection,\r\n introspectCollections,\r\n introspectBlocks,\r\n buildBlockNestingMap,\r\n buildRelationshipGraph,\r\n} from '../introspection'\r\n\r\n// ─── Sample schema (kept inline so the test is self-contained) ─────\r\n\r\nconst Media: CollectionConfig = {\r\n slug: 'media',\r\n upload: true,\r\n fields: [{ name: 'alt', type: 'text', required: true }],\r\n}\r\n\r\nconst Categories: CollectionConfig = {\r\n slug: 'categories',\r\n fields: [\r\n { name: 'name', type: 'text', required: true },\r\n { name: 'slug', type: 'text', required: true },\r\n ],\r\n}\r\n\r\nconst Authors: CollectionConfig = {\r\n slug: 'authors',\r\n fields: [\r\n { name: 'name', type: 'text', required: true },\r\n { name: 'slug', type: 'text', required: true },\r\n { name: 'avatar', type: 'upload', relationTo: 'media' },\r\n ],\r\n}\r\n\r\n// Leaf-style blocks\r\nconst Heading: Block = {\r\n slug: 'heading',\r\n fields: [\r\n { name: 'text', type: 'text', required: true },\r\n {\r\n name: 'level',\r\n type: 'select',\r\n options: ['h1', 'h2', 'h3'],\r\n defaultValue: 'h2',\r\n },\r\n {\r\n name: 'align',\r\n type: 'select',\r\n options: ['left', 'center', 'right'],\r\n defaultValue: 'left',\r\n },\r\n ],\r\n}\r\n\r\nconst RichText: Block = {\r\n slug: 'richText',\r\n fields: [{ name: 'content', type: 'richText' }],\r\n}\r\n\r\nconst ImageBlock: Block = {\r\n slug: 'image',\r\n fields: [\r\n { name: 'image', type: 'upload', relationTo: 'media', required: true },\r\n { name: 'caption', type: 'text' },\r\n ],\r\n}\r\n\r\n// Container-style blocks (have nested blocks fields)\r\nconst FullWidth: Block = {\r\n slug: 'fullWidth',\r\n fields: [\r\n {\r\n name: 'content',\r\n type: 'blocks',\r\n blocks: [Heading, RichText, ImageBlock],\r\n },\r\n ],\r\n}\r\n\r\nconst HeadingOnly: Block = {\r\n slug: 'headingOnly',\r\n fields: [\r\n {\r\n name: 'content',\r\n type: 'blocks',\r\n maxRows: 1,\r\n blocks: [Heading],\r\n },\r\n ],\r\n}\r\n\r\nconst CtaBanner: Block = {\r\n slug: 'ctaBanner',\r\n fields: [\r\n { name: 'headline', type: 'text', required: true },\r\n { name: 'buttonLabel', type: 'text' },\r\n { name: 'buttonHref', type: 'text' },\r\n ],\r\n}\r\n\r\n// Deeply-nestable container — exercises the recursive path\r\nconst Accordion: Block = {\r\n slug: 'accordion',\r\n fields: [\r\n {\r\n name: 'panels',\r\n type: 'array',\r\n fields: [\r\n { name: 'title', type: 'text' },\r\n {\r\n name: 'body',\r\n type: 'blocks',\r\n blocks: [Heading, RichText, FullWidth],\r\n },\r\n ],\r\n },\r\n ],\r\n}\r\n\r\nconst allBlocks: Block[] = [Heading, RichText, ImageBlock, FullWidth, HeadingOnly, CtaBanner, Accordion]\r\n\r\nconst Posts: CollectionConfig = {\r\n slug: 'posts',\r\n versions: { drafts: true },\r\n fields: [\r\n { name: 'title', type: 'text', required: true },\r\n { name: 'slug', type: 'text', required: true },\r\n { name: 'featured', type: 'checkbox' },\r\n { name: 'category', type: 'relationship', relationTo: 'categories' },\r\n {\r\n name: 'authors',\r\n type: 'relationship',\r\n relationTo: 'authors',\r\n hasMany: true,\r\n },\r\n { name: 'coverImage', type: 'upload', relationTo: 'media' },\r\n {\r\n name: 'tags',\r\n type: 'array',\r\n fields: [{ name: 'tag', type: 'text' }],\r\n },\r\n ],\r\n}\r\n\r\nconst Pages: CollectionConfig = {\r\n slug: 'pages',\r\n versions: { drafts: true },\r\n fields: [\r\n {\r\n type: 'tabs',\r\n tabs: [\r\n {\r\n name: 'hero',\r\n label: 'Hero',\r\n fields: [\r\n { name: 'heroTitle', type: 'text' },\r\n {\r\n name: 'heroSize',\r\n type: 'select',\r\n options: ['small', 'medium', 'large'],\r\n defaultValue: 'medium',\r\n },\r\n ],\r\n },\r\n {\r\n label: 'Content',\r\n fields: [\r\n { name: 'slug', type: 'text', required: true },\r\n { name: 'layout', type: 'blocks', blocks: [FullWidth, HeadingOnly, CtaBanner, Accordion] },\r\n ],\r\n },\r\n ],\r\n },\r\n ],\r\n}\r\n\r\n// ─── introspectCollection ──────────────────────────────────────────\r\n\r\ndescribe('introspectCollection', () => {\r\n it('extracts Posts collection fields, relationships, and draft status', () => {\r\n const schema = introspectCollection(Posts)\r\n\r\n expect(schema.slug).toBe('posts')\r\n expect(schema.hasDrafts).toBe(true)\r\n\r\n const fieldNames = schema.fields.map((f) => f.name)\r\n expect(fieldNames).toContain('title')\r\n expect(fieldNames).toContain('slug')\r\n expect(fieldNames).toContain('featured')\r\n expect(fieldNames).toContain('tags')\r\n\r\n const relFieldNames = schema.relationships.map((r) => r.fieldName)\r\n expect(relFieldNames).toContain('category')\r\n expect(relFieldNames).toContain('authors')\r\n\r\n const cover = schema.relationships.find((r) => r.fieldName === 'coverImage')\r\n expect(cover).toBeDefined()\r\n expect(cover!.relationTo).toBe('media')\r\n\r\n expect(schema.searchableFields).toContain('title')\r\n expect(schema.searchableFields).toContain('slug')\r\n })\r\n\r\n it('extracts Pages collection with tab-nested fields', () => {\r\n const schema = introspectCollection(Pages)\r\n\r\n expect(schema.slug).toBe('pages')\r\n expect(schema.hasDrafts).toBe(true)\r\n\r\n const fieldNames = schema.fields.map((f) => f.name)\r\n expect(fieldNames).toContain('heroTitle')\r\n expect(fieldNames).toContain('slug')\r\n expect(fieldNames).toContain('layout')\r\n })\r\n\r\n it('detects collections without draft support', () => {\r\n const schema = introspectCollection(Categories)\r\n expect(schema.hasDrafts).toBe(false)\r\n })\r\n\r\n it('extracts select field options from Pages heroSize', () => {\r\n const schema = introspectCollection(Pages)\r\n const heroSize = schema.fields.find((f) => f.name === 'heroSize')\r\n expect(heroSize).toBeDefined()\r\n expect(heroSize!.type).toBe('select')\r\n expect(heroSize!.options).toBeDefined()\r\n expect(heroSize!.options!.length).toBe(3)\r\n })\r\n})\r\n\r\n// ─── introspectBlocks (flat catalog) ───────────────────────────────\r\n\r\ndescribe('introspectBlocks', () => {\r\n it('returns a flat catalog of every block with no section/leaf split', () => {\r\n const catalog = introspectBlocks(allBlocks)\r\n const slugs = catalog.blocks.map((b) => b.slug)\r\n expect(slugs).toEqual([\r\n 'heading',\r\n 'richText',\r\n 'image',\r\n 'fullWidth',\r\n 'headingOnly',\r\n 'ctaBanner',\r\n 'accordion',\r\n ])\r\n })\r\n\r\n it('extracts each block\\'s fields including select options', () => {\r\n const catalog = introspectBlocks(allBlocks)\r\n const heading = catalog.blocks.find((b) => b.slug === 'heading')\r\n expect(heading).toBeDefined()\r\n const headingFieldNames = heading!.fields.map((f) => f.name)\r\n expect(headingFieldNames).toEqual(['text', 'level', 'align'])\r\n const level = heading!.fields.find((f) => f.name === 'level')\r\n expect(level!.options).toBeDefined()\r\n })\r\n})\r\n\r\n// ─── buildBlockNestingMap ──────────────────────────────────────────\r\n\r\ndescribe('buildBlockNestingMap', () => {\r\n it('records the layout field on Pages with the slugs it accepts', () => {\r\n const map = buildBlockNestingMap([Pages, Posts], allBlocks)\r\n const pageLayout = map.find(\r\n (e) => e.ownerType === 'collection' && e.owner === 'pages' && e.fieldPath === 'layout',\r\n )\r\n expect(pageLayout).toBeDefined()\r\n expect(pageLayout!.acceptedBlockSlugs).toEqual(['fullWidth', 'headingOnly', 'ctaBanner', 'accordion'])\r\n })\r\n\r\n it('records nested blocks fields inside container blocks', () => {\r\n const map = buildBlockNestingMap([Pages], allBlocks)\r\n\r\n const fullWidthContent = map.find(\r\n (e) => e.ownerType === 'block' && e.owner === 'fullWidth' && e.fieldPath === 'content',\r\n )\r\n expect(fullWidthContent).toBeDefined()\r\n expect(fullWidthContent!.acceptedBlockSlugs).toEqual(['heading', 'richText', 'image'])\r\n\r\n const headingOnly = map.find(\r\n (e) => e.ownerType === 'block' && e.owner === 'headingOnly' && e.fieldPath === 'content',\r\n )\r\n expect(headingOnly!.acceptedBlockSlugs).toEqual(['heading'])\r\n expect(headingOnly!.maxRows).toBe(1)\r\n })\r\n\r\n it('handles arbitrarily-deep nesting via array fields inside blocks', () => {\r\n const map = buildBlockNestingMap([Pages], allBlocks)\r\n\r\n const accordionPanelBody = map.find(\r\n (e) =>\r\n e.ownerType === 'block' && e.owner === 'accordion' && e.fieldPath === 'panels[].body',\r\n )\r\n expect(accordionPanelBody).toBeDefined()\r\n expect(accordionPanelBody!.acceptedBlockSlugs).toEqual(['heading', 'richText', 'fullWidth'])\r\n })\r\n\r\n it('omits unknown slugs not present in the block list', () => {\r\n const Stray: CollectionConfig = {\r\n slug: 'stray',\r\n fields: [\r\n {\r\n name: 'layout',\r\n type: 'blocks',\r\n blocks: [Heading, { slug: 'mystery', fields: [] } as Block],\r\n },\r\n ],\r\n }\r\n const map = buildBlockNestingMap([Stray], [Heading]) // mystery not in catalog\r\n const stray = map.find((e) => e.owner === 'stray' && e.fieldPath === 'layout')\r\n expect(stray!.acceptedBlockSlugs).toEqual(['heading'])\r\n })\r\n\r\n it('omits fixed blocks (no nested blocks fields) from the map', () => {\r\n const map = buildBlockNestingMap([Pages], allBlocks)\r\n const ctaEntries = map.filter((e) => e.owner === 'ctaBanner')\r\n expect(ctaEntries).toHaveLength(0)\r\n })\r\n})\r\n\r\n// ─── buildRelationshipGraph ────────────────────────────────────────\r\n\r\ndescribe('buildRelationshipGraph', () => {\r\n it('builds correct graph from sample collections', () => {\r\n const schemas = introspectCollections([Posts, Pages, Categories, Authors, Media])\r\n const edges = buildRelationshipGraph(schemas)\r\n\r\n const postEdges = edges.filter((e) => e.fromCollection === 'posts')\r\n const postTargets = postEdges.map((e) => e.toCollection)\r\n expect(postTargets).toContain('categories')\r\n expect(postTargets).toContain('authors')\r\n expect(postTargets).toContain('media')\r\n\r\n const authorEdges = edges.filter((e) => e.fromCollection === 'authors')\r\n expect(authorEdges.map((e) => e.toCollection)).toContain('media')\r\n })\r\n})\r\n"],"names":["describe","it","expect","introspectCollection","introspectCollections","introspectBlocks","buildBlockNestingMap","buildRelationshipGraph","Media","slug","upload","fields","name","type","required","Categories","Authors","relationTo","Heading","options","defaultValue","RichText","ImageBlock","FullWidth","blocks","HeadingOnly","maxRows","CtaBanner","Accordion","allBlocks","Posts","versions","drafts","hasMany","Pages","tabs","label","schema","toBe","hasDrafts","fieldNames","map","f","toContain","relFieldNames","relationships","r","fieldName","cover","find","toBeDefined","searchableFields","heroSize","length","catalog","slugs","b","toEqual","heading","headingFieldNames","level","pageLayout","e","ownerType","owner","fieldPath","acceptedBlockSlugs","fullWidthContent","headingOnly","accordionPanelBody","Stray","stray","ctaEntries","filter","toHaveLength","schemas","edges","postEdges","fromCollection","postTargets","toCollection","authorEdges"],"mappings":"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"}
|