payload-mcp-toolkit 0.2.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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +133 -0
  3. package/dist/__tests__/introspection.test.js +364 -0
  4. package/dist/__tests__/introspection.test.js.map +1 -0
  5. package/dist/__tests__/url-validator.test.js +326 -0
  6. package/dist/__tests__/url-validator.test.js.map +1 -0
  7. package/dist/draft-workflow.d.ts +60 -0
  8. package/dist/draft-workflow.js +93 -0
  9. package/dist/draft-workflow.js.map +1 -0
  10. package/dist/index.d.ts +24 -0
  11. package/dist/index.js +142 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/introspection.d.ts +23 -0
  14. package/dist/introspection.js +238 -0
  15. package/dist/introspection.js.map +1 -0
  16. package/dist/prompts.d.ts +21 -0
  17. package/dist/prompts.js +215 -0
  18. package/dist/prompts.js.map +1 -0
  19. package/dist/rate-limiter.d.ts +25 -0
  20. package/dist/rate-limiter.js +51 -0
  21. package/dist/rate-limiter.js.map +1 -0
  22. package/dist/resources.d.ts +18 -0
  23. package/dist/resources.js +77 -0
  24. package/dist/resources.js.map +1 -0
  25. package/dist/tools/compose-helpers.d.ts +117 -0
  26. package/dist/tools/compose-helpers.js +236 -0
  27. package/dist/tools/compose-helpers.js.map +1 -0
  28. package/dist/tools/compose-layout.d.ts +139 -0
  29. package/dist/tools/compose-layout.js +61 -0
  30. package/dist/tools/compose-layout.js.map +1 -0
  31. package/dist/tools/patch-layout.d.ts +107 -0
  32. package/dist/tools/patch-layout.js +123 -0
  33. package/dist/tools/patch-layout.js.map +1 -0
  34. package/dist/tools/publish-draft.d.ts +24 -0
  35. package/dist/tools/publish-draft.js +69 -0
  36. package/dist/tools/publish-draft.js.map +1 -0
  37. package/dist/tools/resolve-reference.d.ts +31 -0
  38. package/dist/tools/resolve-reference.js +169 -0
  39. package/dist/tools/resolve-reference.js.map +1 -0
  40. package/dist/tools/safe-delete.d.ts +37 -0
  41. package/dist/tools/safe-delete.js +161 -0
  42. package/dist/tools/safe-delete.js.map +1 -0
  43. package/dist/tools/schedule-publish.d.ts +49 -0
  44. package/dist/tools/schedule-publish.js +120 -0
  45. package/dist/tools/schedule-publish.js.map +1 -0
  46. package/dist/tools/search-content.d.ts +43 -0
  47. package/dist/tools/search-content.js +210 -0
  48. package/dist/tools/search-content.js.map +1 -0
  49. package/dist/tools/update-document.d.ts +32 -0
  50. package/dist/tools/update-document.js +114 -0
  51. package/dist/tools/update-document.js.map +1 -0
  52. package/dist/tools/upload-media.d.ts +26 -0
  53. package/dist/tools/upload-media.js +115 -0
  54. package/dist/tools/upload-media.js.map +1 -0
  55. package/dist/tools/versions.d.ts +50 -0
  56. package/dist/tools/versions.js +159 -0
  57. package/dist/tools/versions.js.map +1 -0
  58. package/dist/types.d.ts +118 -0
  59. package/dist/types.js +3 -0
  60. package/dist/types.js.map +1 -0
  61. package/dist/url-validator.d.ts +36 -0
  62. package/dist/url-validator.js +222 -0
  63. package/dist/url-validator.js.map +1 -0
  64. package/package.json +85 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 payload-mcp-toolkit contributors
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 ADDED
@@ -0,0 +1,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** (11 total):
27
+
28
+ *Authoring*
29
+ - `composePageLayout` — build a validated page layout from sections + leaves.
30
+ - `patchLayout` — surgically append/prepend/insertAt/replaceAt sections on a doc's block-array field without round-tripping the whole array. Safer than `updateDocument` for incremental layout edits.
31
+ - `updateDocument` — Local-API based update that survives the upload-field bug in the official plugin.
32
+ - `uploadMedia` — fetch a public HTTPS image, validate (SSRF-safe), create a Media doc.
33
+
34
+ *Discovery*
35
+ - `resolveReference` — search collections by name/title/slug for relationship IDs.
36
+ - `searchContent` — natural-language editor triage. Filter by `status`, `olderThanDays` / `newerThanDays`, `missingFields`, free-text `query`, scoped to one collection or all.
37
+
38
+ *Lifecycle / safety*
39
+ - `publishDraft` — flip `_status` from draft to published.
40
+ - `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. To actually publish on schedule, you must wire up one of: a [Payload Jobs Queue scheduled task](https://payloadcms.com/docs/jobs-queue/scheduled-jobs), an external cron worker, or a `beforeRead` hook that resolves status on the fly. Without one of those, scheduled drafts stay drafts forever — the tool says so in its response, but it's still a footgun if you skim past it.
41
+ - `listVersions` — recent saved versions of a draft document with id/status/timestamp/displayName.
42
+ - `restoreVersion` — roll a document back to a saved version (creates a new version on top, so itself reversible).
43
+ - `safeDelete` — relationship-aware delete. Walks the introspected relationship graph, refuses with a structured impact summary if other documents reference the target. Override with `confirm: true` after reviewing.
44
+
45
+ **Draft workflow** wired into the official plugin's `mcpCollections`:
46
+ - Disables raw `update` for `always-draft` collections so clients go through `publishDraft` (or `patchLayout` / `updateDocument`, both of which preserve draft semantics).
47
+ - Appends preview URLs to draft responses (path prefixes are configurable via `previewPaths`).
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pnpm add payload-mcp-toolkit @payloadcms/plugin-mcp
53
+ ```
54
+
55
+ Peer dependencies: `payload` ^3, `@payloadcms/plugin-mcp` ^3, `zod` ^3.
56
+
57
+ ## Use
58
+
59
+ ```ts
60
+ // payload.config.ts
61
+ import { contentToolkitPlugin } from 'payload-mcp-toolkit'
62
+
63
+ export default buildConfig({
64
+ // ...your collections, blocks, globals
65
+ plugins: [
66
+ contentToolkitPlugin({
67
+ siteUrl: process.env.SITE_URL!,
68
+ previewSecret: process.env.PREVIEW_SECRET!,
69
+ previewPaths: {
70
+ pages: '', // pages live at /
71
+ posts: '/blog', // posts live at /blog/:slug
72
+ },
73
+ draftBehavior: {
74
+ pages: 'always-draft',
75
+ posts: 'always-draft',
76
+ },
77
+ domainPrompts: [
78
+ // Optional site-specific vocabulary — see examples/
79
+ ],
80
+ }),
81
+ ],
82
+ })
83
+ ```
84
+
85
+ That's it. The toolkit reads your Payload config and registers everything against the official MCP plugin. Connect any MCP client to your Payload server and the LLM will see the prompts, resources, and tools.
86
+
87
+ See `examples/angels-config.example.ts` for a fully-worked domain-prompt setup from a real-world site.
88
+
89
+ ## Configuration reference
90
+
91
+ | Option | Type | Description |
92
+ |---|---|---|
93
+ | `siteUrl` | `string` | Base URL used to construct preview URLs. |
94
+ | `previewSecret` | `string` | Secret embedded in the preview URL query. |
95
+ | `previewPaths` | `Record<string, string>` | Per-collection URL path prefix. Defaults to `/{slug}` if omitted. Use `''` for collections at the site root. |
96
+ | `draftBehavior` | `Record<string, 'always-draft' \| 'always-publish'>` | Override the default draft behavior per collection. |
97
+ | `domainPrompts` | `DomainPrompt[]` | Custom prompts that teach the AI site-specific vocabulary. |
98
+ | `mediaUpload.maxFileSize` | `number` | Max bytes for `uploadMedia` (default 10MB). |
99
+ | `mediaUpload.collectionSlug` | `string` | Media collection slug (default `media`). |
100
+ | `excludeCollections` | `string[]` | Collection slugs to hide from MCP. |
101
+ | `excludeGlobals` | `string[]` | Global slugs to hide from MCP. |
102
+
103
+ ## Development
104
+
105
+ 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.
106
+
107
+ ```bash
108
+ pnpm install
109
+ cp dev/.env.example dev/.env
110
+ pnpm dev # boot dev/ Next.js + Payload at http://localhost:3000
111
+ pnpm test # vitest — runs introspection unit tests
112
+ pnpm build # produce dist/ for npm publish
113
+ ```
114
+
115
+ The dev harness ships with a realistic CMS schema:
116
+ - `Pages` — block-based layout (FullWidth, TwoColumn, CtaBanner, HeadingOnly), drafts enabled.
117
+ - `Posts` — title/slug/excerpt/content/cover/category/authors/tags/SEO, drafts enabled.
118
+ - `Authors`, `Categories`, `Media`, `Users` — taxonomy + auth.
119
+ - `SiteSettings` — global with site name, logo, social, footer.
120
+ - 5 leaf blocks (Heading, RichText, Image, ButtonGroup, Quote) and 4 section blocks.
121
+
122
+ Seed sample content:
123
+
124
+ ```bash
125
+ # Generate the admin import map first time:
126
+ pnpm dev:generate-importmap
127
+
128
+ # Then visit http://localhost:3000/admin and create your first user.
129
+ ```
130
+
131
+ ## License
132
+
133
+ MIT
@@ -0,0 +1,364 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { introspectCollection, introspectCollections, introspectBlocks, buildRelationshipGraph } from '../introspection';
3
+ // ─── Sample schema (kept inline so the test is self-contained) ─────
4
+ const Media = {
5
+ slug: 'media',
6
+ upload: true,
7
+ fields: [
8
+ {
9
+ name: 'alt',
10
+ type: 'text',
11
+ required: true
12
+ }
13
+ ]
14
+ };
15
+ const Categories = {
16
+ slug: 'categories',
17
+ fields: [
18
+ {
19
+ name: 'name',
20
+ type: 'text',
21
+ required: true
22
+ },
23
+ {
24
+ name: 'slug',
25
+ type: 'text',
26
+ required: true
27
+ }
28
+ ]
29
+ };
30
+ const Authors = {
31
+ slug: 'authors',
32
+ fields: [
33
+ {
34
+ name: 'name',
35
+ type: 'text',
36
+ required: true
37
+ },
38
+ {
39
+ name: 'slug',
40
+ type: 'text',
41
+ required: true
42
+ },
43
+ {
44
+ name: 'avatar',
45
+ type: 'upload',
46
+ relationTo: 'media'
47
+ }
48
+ ]
49
+ };
50
+ // Leaf blocks
51
+ const Heading = {
52
+ slug: 'heading',
53
+ fields: [
54
+ {
55
+ name: 'text',
56
+ type: 'text',
57
+ required: true
58
+ },
59
+ {
60
+ name: 'level',
61
+ type: 'select',
62
+ options: [
63
+ 'h1',
64
+ 'h2',
65
+ 'h3'
66
+ ],
67
+ defaultValue: 'h2'
68
+ },
69
+ {
70
+ name: 'align',
71
+ type: 'select',
72
+ options: [
73
+ 'left',
74
+ 'center',
75
+ 'right'
76
+ ],
77
+ defaultValue: 'left'
78
+ }
79
+ ]
80
+ };
81
+ const RichText = {
82
+ slug: 'richText',
83
+ fields: [
84
+ {
85
+ name: 'content',
86
+ type: 'richText'
87
+ }
88
+ ]
89
+ };
90
+ const ImageBlock = {
91
+ slug: 'image',
92
+ fields: [
93
+ {
94
+ name: 'image',
95
+ type: 'upload',
96
+ relationTo: 'media',
97
+ required: true
98
+ },
99
+ {
100
+ name: 'caption',
101
+ type: 'text'
102
+ }
103
+ ]
104
+ };
105
+ const allLeafBlocks = [
106
+ Heading,
107
+ RichText,
108
+ ImageBlock
109
+ ];
110
+ // Section blocks
111
+ const FullWidth = {
112
+ slug: 'fullWidth',
113
+ fields: [
114
+ {
115
+ name: 'content',
116
+ type: 'blocks',
117
+ blocks: allLeafBlocks
118
+ }
119
+ ]
120
+ };
121
+ const HeadingOnly = {
122
+ slug: 'headingOnly',
123
+ fields: [
124
+ {
125
+ name: 'content',
126
+ type: 'blocks',
127
+ maxRows: 1,
128
+ blocks: [
129
+ Heading
130
+ ]
131
+ }
132
+ ]
133
+ };
134
+ const CtaBanner = {
135
+ slug: 'ctaBanner',
136
+ fields: [
137
+ {
138
+ name: 'headline',
139
+ type: 'text',
140
+ required: true
141
+ },
142
+ {
143
+ name: 'buttonLabel',
144
+ type: 'text'
145
+ },
146
+ {
147
+ name: 'buttonHref',
148
+ type: 'text'
149
+ }
150
+ ]
151
+ };
152
+ const allSectionBlocks = [
153
+ FullWidth,
154
+ HeadingOnly,
155
+ CtaBanner
156
+ ];
157
+ const Posts = {
158
+ slug: 'posts',
159
+ versions: {
160
+ drafts: true
161
+ },
162
+ fields: [
163
+ {
164
+ name: 'title',
165
+ type: 'text',
166
+ required: true
167
+ },
168
+ {
169
+ name: 'slug',
170
+ type: 'text',
171
+ required: true
172
+ },
173
+ {
174
+ name: 'featured',
175
+ type: 'checkbox'
176
+ },
177
+ {
178
+ name: 'category',
179
+ type: 'relationship',
180
+ relationTo: 'categories'
181
+ },
182
+ {
183
+ name: 'authors',
184
+ type: 'relationship',
185
+ relationTo: 'authors',
186
+ hasMany: true
187
+ },
188
+ {
189
+ name: 'coverImage',
190
+ type: 'upload',
191
+ relationTo: 'media'
192
+ },
193
+ {
194
+ name: 'tags',
195
+ type: 'array',
196
+ fields: [
197
+ {
198
+ name: 'tag',
199
+ type: 'text'
200
+ }
201
+ ]
202
+ }
203
+ ]
204
+ };
205
+ const Pages = {
206
+ slug: 'pages',
207
+ versions: {
208
+ drafts: true
209
+ },
210
+ fields: [
211
+ {
212
+ type: 'tabs',
213
+ tabs: [
214
+ {
215
+ name: 'hero',
216
+ label: 'Hero',
217
+ fields: [
218
+ {
219
+ name: 'heroTitle',
220
+ type: 'text'
221
+ },
222
+ {
223
+ name: 'heroSize',
224
+ type: 'select',
225
+ options: [
226
+ 'small',
227
+ 'medium',
228
+ 'large'
229
+ ],
230
+ defaultValue: 'medium'
231
+ }
232
+ ]
233
+ },
234
+ {
235
+ label: 'Content',
236
+ fields: [
237
+ {
238
+ name: 'slug',
239
+ type: 'text',
240
+ required: true
241
+ },
242
+ {
243
+ name: 'layout',
244
+ type: 'blocks',
245
+ blocks: allSectionBlocks
246
+ }
247
+ ]
248
+ }
249
+ ]
250
+ }
251
+ ]
252
+ };
253
+ // ─── Tests ─────────────────────────────────────────────────────────
254
+ describe('introspectCollection', ()=>{
255
+ it('extracts Posts collection fields, relationships, and draft status', ()=>{
256
+ const schema = introspectCollection(Posts);
257
+ expect(schema.slug).toBe('posts');
258
+ expect(schema.hasDrafts).toBe(true);
259
+ const fieldNames = schema.fields.map((f)=>f.name);
260
+ expect(fieldNames).toContain('title');
261
+ expect(fieldNames).toContain('slug');
262
+ expect(fieldNames).toContain('featured');
263
+ expect(fieldNames).toContain('tags');
264
+ const relFieldNames = schema.relationships.map((r)=>r.fieldName);
265
+ expect(relFieldNames).toContain('category');
266
+ expect(relFieldNames).toContain('authors');
267
+ const cover = schema.relationships.find((r)=>r.fieldName === 'coverImage');
268
+ expect(cover).toBeDefined();
269
+ expect(cover.relationTo).toBe('media');
270
+ expect(schema.searchableFields).toContain('title');
271
+ expect(schema.searchableFields).toContain('slug');
272
+ });
273
+ it('extracts Pages collection with tab-nested fields', ()=>{
274
+ const schema = introspectCollection(Pages);
275
+ expect(schema.slug).toBe('pages');
276
+ expect(schema.hasDrafts).toBe(true);
277
+ const fieldNames = schema.fields.map((f)=>f.name);
278
+ expect(fieldNames).toContain('heroTitle');
279
+ expect(fieldNames).toContain('slug');
280
+ expect(fieldNames).toContain('layout');
281
+ });
282
+ it('detects collections without draft support', ()=>{
283
+ const schema = introspectCollection(Categories);
284
+ expect(schema.hasDrafts).toBe(false);
285
+ });
286
+ it('extracts select field options from Pages heroSize', ()=>{
287
+ const schema = introspectCollection(Pages);
288
+ const heroSize = schema.fields.find((f)=>f.name === 'heroSize');
289
+ expect(heroSize).toBeDefined();
290
+ expect(heroSize.type).toBe('select');
291
+ expect(heroSize.options).toBeDefined();
292
+ expect(heroSize.options.length).toBe(3);
293
+ });
294
+ });
295
+ describe('introspectBlocks', ()=>{
296
+ it('discovers fullWidth accepts all leaf blocks (composable)', ()=>{
297
+ const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks);
298
+ const fullWidth = catalog.sections.find((s)=>s.slug === 'fullWidth');
299
+ expect(fullWidth).toBeDefined();
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([
325
+ 'heading',
326
+ 'richText',
327
+ 'image'
328
+ ]);
329
+ });
330
+ it('extracts leaf block fields including select options', ()=>{
331
+ const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks);
332
+ const heading = catalog.leaves.find((l)=>l.slug === 'heading');
333
+ expect(heading).toBeDefined();
334
+ const headingFieldNames = heading.fields.map((f)=>f.name);
335
+ expect(headingFieldNames).toEqual([
336
+ 'text',
337
+ 'level',
338
+ 'align'
339
+ ]);
340
+ const level = heading.fields.find((f)=>f.name === 'level');
341
+ expect(level.options).toBeDefined();
342
+ });
343
+ });
344
+ describe('buildRelationshipGraph', ()=>{
345
+ it('builds correct graph from sample collections', ()=>{
346
+ const schemas = introspectCollections([
347
+ Posts,
348
+ Pages,
349
+ Categories,
350
+ Authors,
351
+ Media
352
+ ]);
353
+ const edges = buildRelationshipGraph(schemas);
354
+ const postEdges = edges.filter((e)=>e.fromCollection === 'posts');
355
+ const postTargets = postEdges.map((e)=>e.toCollection);
356
+ expect(postTargets).toContain('categories');
357
+ expect(postTargets).toContain('authors');
358
+ expect(postTargets).toContain('media');
359
+ const authorEdges = edges.filter((e)=>e.fromCollection === 'authors');
360
+ expect(authorEdges.map((e)=>e.toCollection)).toContain('media');
361
+ });
362
+ });
363
+
364
+ //# sourceMappingURL=introspection.test.js.map
@@ -0,0 +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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"}