payload-mcp-toolkit 0.7.0 → 0.7.5
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 +30 -9
- package/dist/api-keys.js +57 -21
- package/dist/api-keys.js.map +1 -1
- package/dist/auth-strategy.d.ts +18 -7
- package/dist/auth-strategy.js +54 -12
- package/dist/auth-strategy.js.map +1 -1
- package/dist/tools/_helpers.d.ts +34 -0
- package/dist/tools/_helpers.js +98 -0
- package/dist/tools/_helpers.js.map +1 -1
- package/dist/tools/create-document.js +8 -0
- package/dist/tools/create-document.js.map +1 -1
- package/dist/tools/delete-document.d.ts +1 -1
- package/dist/tools/delete-document.js +6 -6
- package/dist/tools/delete-document.js.map +1 -1
- package/dist/tools/find-document.d.ts +3 -3
- package/dist/tools/find-document.js +8 -8
- package/dist/tools/find-document.js.map +1 -1
- package/dist/tools/publish-draft.js +33 -1
- package/dist/tools/publish-draft.js.map +1 -1
- package/dist/tools/publish-global-draft.js +30 -1
- package/dist/tools/publish-global-draft.js.map +1 -1
- package/package.json +29 -15
- package/dist/__tests__/api-keys.test.js +0 -292
- package/dist/__tests__/api-keys.test.js.map +0 -1
- package/dist/__tests__/auth-strategy.test.js +0 -681
- package/dist/__tests__/auth-strategy.test.js.map +0 -1
- package/dist/__tests__/conflict-detection.test.js +0 -69
- package/dist/__tests__/conflict-detection.test.js.map +0 -1
- package/dist/__tests__/delete-document.test.js +0 -70
- package/dist/__tests__/delete-document.test.js.map +0 -1
- package/dist/__tests__/endpoint.test.js +0 -143
- package/dist/__tests__/endpoint.test.js.map +0 -1
- package/dist/__tests__/find-document.test.js +0 -178
- package/dist/__tests__/find-document.test.js.map +0 -1
- package/dist/__tests__/find-global.test.js +0 -173
- package/dist/__tests__/find-global.test.js.map +0 -1
- package/dist/__tests__/global-versions.test.js +0 -183
- package/dist/__tests__/global-versions.test.js.map +0 -1
- package/dist/__tests__/hash.test.js +0 -58
- package/dist/__tests__/hash.test.js.map +0 -1
- package/dist/__tests__/index-integration.test.js +0 -191
- package/dist/__tests__/index-integration.test.js.map +0 -1
- package/dist/__tests__/introspection.test.js +0 -659
- package/dist/__tests__/introspection.test.js.map +0 -1
- package/dist/__tests__/patch-global-layout.test.js +0 -474
- package/dist/__tests__/patch-global-layout.test.js.map +0 -1
- package/dist/__tests__/patch-layout.test.js +0 -171
- package/dist/__tests__/patch-layout.test.js.map +0 -1
- package/dist/__tests__/registry.test.js +0 -795
- package/dist/__tests__/registry.test.js.map +0 -1
- package/dist/__tests__/resources.test.js +0 -139
- package/dist/__tests__/resources.test.js.map +0 -1
- package/dist/__tests__/update-global.test.js +0 -157
- package/dist/__tests__/update-global.test.js.map +0 -1
- package/dist/__tests__/url-validator.test.js +0 -326
- package/dist/__tests__/url-validator.test.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/__tests__/index-integration.test.ts"],"sourcesContent":["import { describe, it, expect } from 'vitest'\r\nimport type { Config } from 'payload'\r\nimport { mcpToolkitPlugin } from '../index'\r\n\r\nfunction baseConfig(): Config {\r\n return {\r\n serverURL: 'https://app.example.com',\r\n secret: 'test-secret',\r\n admin: { user: 'users' },\r\n collections: [\r\n {\r\n slug: 'users',\r\n auth: true,\r\n fields: [{ name: 'email', type: 'email', required: true }],\r\n },\r\n {\r\n slug: 'posts',\r\n fields: [\r\n { name: 'title', type: 'text' },\r\n { name: 'slug', type: 'text' },\r\n ],\r\n },\r\n ],\r\n endpoints: [],\r\n } as never\r\n}\r\n\r\ndescribe('mcpToolkitPlugin integration', () => {\r\n it('appends the api-keys collection and the MCP endpoints additively', () => {\r\n const cfg = mcpToolkitPlugin()(baseConfig())\r\n const slugs = (cfg.collections ?? []).map((c) => c.slug)\r\n expect(slugs).toContain('payload-mcp-api-keys')\r\n expect(slugs).toContain('users')\r\n expect(slugs).toContain('posts')\r\n const endpointPaths = (cfg.endpoints ?? []).map((e) => `${e.method.toUpperCase()} ${e.path}`)\r\n expect(endpointPaths).toEqual(expect.arrayContaining(['POST /mcp', 'GET /mcp']))\r\n })\r\n\r\n it('attaches the bearer strategy to the user collection without losing existing auth config', () => {\r\n const cfg = mcpToolkitPlugin()(baseConfig())\r\n const users = (cfg.collections ?? []).find((c) => c.slug === 'users') as {\r\n auth: { strategies?: Array<{ name: string }> }\r\n }\r\n expect(users).toBeDefined()\r\n const names = users.auth.strategies?.map((s) => s.name) ?? []\r\n expect(names).toContain('mcp-toolkit-bearer')\r\n })\r\n\r\n it('preserves existing auth.strategies on the user collection', () => {\r\n const cfg = baseConfig() as Config & {\r\n collections: Array<{ slug: string; auth?: unknown; fields: unknown[] }>\r\n }\r\n const otherStrategy = { name: 'tenant-shared-strategy', authenticate: async () => ({ user: null }) }\r\n cfg.collections[0]!.auth = { strategies: [otherStrategy] } as never\r\n const out = mcpToolkitPlugin()(cfg as never)\r\n const users = (out.collections ?? []).find((c) => c.slug === 'users') as {\r\n auth: { strategies: Array<{ name: string }> }\r\n }\r\n const names = users.auth.strategies.map((s) => s.name)\r\n expect(names).toEqual(['tenant-shared-strategy', 'mcp-toolkit-bearer'])\r\n })\r\n\r\n it('respects a custom user collection slug from incomingConfig.admin.user', () => {\r\n const cfg = baseConfig() as Config & {\r\n collections: Array<{ slug: string; auth?: unknown; fields: unknown[] }>\r\n admin?: { user?: string }\r\n }\r\n cfg.admin = { user: 'admins' }\r\n cfg.collections[0]!.slug = 'admins'\r\n const out = mcpToolkitPlugin()(cfg as never)\r\n const admins = (out.collections ?? []).find((c) => c.slug === 'admins') as {\r\n auth: { strategies?: Array<{ name: string }> }\r\n }\r\n expect(admins.auth.strategies?.some((s) => s.name === 'mcp-toolkit-bearer')).toBe(true)\r\n })\r\n\r\n it('throws when @payloadcms/plugin-mcp appears to also be registered', () => {\r\n const cfg = baseConfig() as Config\r\n function mcpPlugin() {}\r\n ;(cfg as { plugins: unknown[] }).plugins = [mcpPlugin as never]\r\n expect(() => mcpToolkitPlugin()(cfg)).toThrow(/standalone successor/)\r\n })\r\n\r\n it('throws when an existing collection takes the api-keys slug', () => {\r\n const cfg = baseConfig() as Config & {\r\n collections: Array<{ slug: string; fields: unknown[] }>\r\n }\r\n cfg.collections.push({ slug: 'payload-mcp-api-keys', fields: [] })\r\n expect(() => mcpToolkitPlugin()(cfg as never)).toThrow(/payload-mcp-api-keys/)\r\n })\r\n\r\n it('honours a custom apiKeyCollection.slug', () => {\r\n const cfg = mcpToolkitPlugin({ apiKeyCollection: { slug: 'my-keys' } })(baseConfig())\r\n const slugs = (cfg.collections ?? []).map((c) => c.slug)\r\n expect(slugs).toContain('my-keys')\r\n expect(slugs).not.toContain('payload-mcp-api-keys')\r\n })\r\n\r\n it('a host config with no globals still registers the globalScopes field (matrix shows empty state)', () => {\r\n const cfg = mcpToolkitPlugin()(baseConfig())\r\n // The field always renders under Custom; the matrix component reports\r\n // the absence via its own empty-state copy when availableGlobals is [].\r\n const apiKeys = (cfg.collections ?? []).find((c) => c.slug === 'payload-mcp-api-keys') as {\r\n fields: Array<{\r\n name?: string\r\n admin?: {\r\n condition?: (data: unknown) => boolean\r\n components?: { Field?: { clientProps?: { availableGlobals?: string[] } } }\r\n }\r\n }>\r\n }\r\n const globalScopes = apiKeys.fields.find((f) => f.name === 'globalScopes')!\r\n expect(globalScopes.admin?.condition?.({ preset: 'custom' })).toBe(true)\r\n expect(globalScopes.admin?.condition?.({ preset: 'editor' })).toBe(false)\r\n expect(globalScopes.admin?.components?.Field?.clientProps?.availableGlobals).toEqual([])\r\n })\r\n\r\n it('a host config with one plain global makes globalScopes UI render under Custom', () => {\r\n const cfg = baseConfig() as Config & { globals?: unknown[] }\r\n cfg.globals = [\r\n { slug: 'site-settings', fields: [{ name: 'siteName', type: 'text' }] },\r\n ] as never\r\n const out = mcpToolkitPlugin()(cfg)\r\n const apiKeys = (out.collections ?? []).find((c) => c.slug === 'payload-mcp-api-keys') as {\r\n fields: Array<{\r\n name?: string\r\n admin?: {\r\n condition?: (data: unknown) => boolean\r\n components?: { Field?: { clientProps?: { availableGlobals?: string[] } } }\r\n }\r\n }>\r\n }\r\n const globalScopes = apiKeys.fields.find((f) => f.name === 'globalScopes')!\r\n expect(globalScopes.admin?.condition?.({ preset: 'custom' })).toBe(true)\r\n expect(globalScopes.admin?.components?.Field?.clientProps?.availableGlobals).toEqual([\r\n 'site-settings',\r\n ])\r\n })\r\n\r\n it('excluded globals are filtered out of availableGlobals at registration time', () => {\r\n const cfg = baseConfig() as Config & { globals?: unknown[] }\r\n cfg.globals = [\r\n { slug: 'site-settings', fields: [{ name: 'siteName', type: 'text' }] },\r\n { slug: 'secret-config', fields: [{ name: 'token', type: 'text' }] },\r\n ] as never\r\n const out = mcpToolkitPlugin({ exclude: { globals: ['secret-config'] } })(cfg)\r\n const apiKeys = (out.collections ?? []).find((c) => c.slug === 'payload-mcp-api-keys') as {\r\n fields: Array<{\r\n name?: string\r\n admin?: { components?: { Field?: { clientProps?: { availableGlobals?: string[] } } } }\r\n }>\r\n }\r\n const globalScopes = apiKeys.fields.find((f) => f.name === 'globalScopes')!\r\n expect(globalScopes.admin?.components?.Field?.clientProps?.availableGlobals).toEqual([\r\n 'site-settings',\r\n ])\r\n })\r\n})\r\n"],"names":["describe","it","expect","mcpToolkitPlugin","baseConfig","serverURL","secret","admin","user","collections","slug","auth","fields","name","type","required","endpoints","cfg","slugs","map","c","toContain","endpointPaths","e","method","toUpperCase","path","toEqual","arrayContaining","users","find","toBeDefined","names","strategies","s","otherStrategy","authenticate","out","admins","some","toBe","mcpPlugin","plugins","toThrow","push","apiKeyCollection","not","apiKeys","globalScopes","f","condition","preset","components","Field","clientProps","availableGlobals","globals","exclude"],"mappings":"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"}
|
|
@@ -1,659 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { introspectCollection, introspectCollections, introspectBlocks, buildBlockNestingMap, buildRelationshipGraph, hasGlobalDrafts, introspectGlobal, introspectGlobals } 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-style 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
|
-
// Container-style blocks (have nested blocks fields)
|
|
106
|
-
const FullWidth = {
|
|
107
|
-
slug: 'fullWidth',
|
|
108
|
-
fields: [
|
|
109
|
-
{
|
|
110
|
-
name: 'content',
|
|
111
|
-
type: 'blocks',
|
|
112
|
-
blocks: [
|
|
113
|
-
Heading,
|
|
114
|
-
RichText,
|
|
115
|
-
ImageBlock
|
|
116
|
-
]
|
|
117
|
-
}
|
|
118
|
-
]
|
|
119
|
-
};
|
|
120
|
-
const HeadingOnly = {
|
|
121
|
-
slug: 'headingOnly',
|
|
122
|
-
fields: [
|
|
123
|
-
{
|
|
124
|
-
name: 'content',
|
|
125
|
-
type: 'blocks',
|
|
126
|
-
maxRows: 1,
|
|
127
|
-
blocks: [
|
|
128
|
-
Heading
|
|
129
|
-
]
|
|
130
|
-
}
|
|
131
|
-
]
|
|
132
|
-
};
|
|
133
|
-
const CtaBanner = {
|
|
134
|
-
slug: 'ctaBanner',
|
|
135
|
-
fields: [
|
|
136
|
-
{
|
|
137
|
-
name: 'headline',
|
|
138
|
-
type: 'text',
|
|
139
|
-
required: true
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
name: 'buttonLabel',
|
|
143
|
-
type: 'text'
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
name: 'buttonHref',
|
|
147
|
-
type: 'text'
|
|
148
|
-
}
|
|
149
|
-
]
|
|
150
|
-
};
|
|
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,
|
|
180
|
-
FullWidth,
|
|
181
|
-
HeadingOnly,
|
|
182
|
-
CtaBanner,
|
|
183
|
-
Accordion
|
|
184
|
-
];
|
|
185
|
-
const Posts = {
|
|
186
|
-
slug: 'posts',
|
|
187
|
-
versions: {
|
|
188
|
-
drafts: true
|
|
189
|
-
},
|
|
190
|
-
fields: [
|
|
191
|
-
{
|
|
192
|
-
name: 'title',
|
|
193
|
-
type: 'text',
|
|
194
|
-
required: true
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
name: 'slug',
|
|
198
|
-
type: 'text',
|
|
199
|
-
required: true
|
|
200
|
-
},
|
|
201
|
-
{
|
|
202
|
-
name: 'featured',
|
|
203
|
-
type: 'checkbox'
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
name: 'category',
|
|
207
|
-
type: 'relationship',
|
|
208
|
-
relationTo: 'categories'
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
name: 'authors',
|
|
212
|
-
type: 'relationship',
|
|
213
|
-
relationTo: 'authors',
|
|
214
|
-
hasMany: true
|
|
215
|
-
},
|
|
216
|
-
{
|
|
217
|
-
name: 'coverImage',
|
|
218
|
-
type: 'upload',
|
|
219
|
-
relationTo: 'media'
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
name: 'tags',
|
|
223
|
-
type: 'array',
|
|
224
|
-
fields: [
|
|
225
|
-
{
|
|
226
|
-
name: 'tag',
|
|
227
|
-
type: 'text'
|
|
228
|
-
}
|
|
229
|
-
]
|
|
230
|
-
}
|
|
231
|
-
]
|
|
232
|
-
};
|
|
233
|
-
const Pages = {
|
|
234
|
-
slug: 'pages',
|
|
235
|
-
versions: {
|
|
236
|
-
drafts: true
|
|
237
|
-
},
|
|
238
|
-
fields: [
|
|
239
|
-
{
|
|
240
|
-
type: 'tabs',
|
|
241
|
-
tabs: [
|
|
242
|
-
{
|
|
243
|
-
name: 'hero',
|
|
244
|
-
label: 'Hero',
|
|
245
|
-
fields: [
|
|
246
|
-
{
|
|
247
|
-
name: 'heroTitle',
|
|
248
|
-
type: 'text'
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
name: 'heroSize',
|
|
252
|
-
type: 'select',
|
|
253
|
-
options: [
|
|
254
|
-
'small',
|
|
255
|
-
'medium',
|
|
256
|
-
'large'
|
|
257
|
-
],
|
|
258
|
-
defaultValue: 'medium'
|
|
259
|
-
}
|
|
260
|
-
]
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
label: 'Content',
|
|
264
|
-
fields: [
|
|
265
|
-
{
|
|
266
|
-
name: 'slug',
|
|
267
|
-
type: 'text',
|
|
268
|
-
required: true
|
|
269
|
-
},
|
|
270
|
-
{
|
|
271
|
-
name: 'layout',
|
|
272
|
-
type: 'blocks',
|
|
273
|
-
blocks: [
|
|
274
|
-
FullWidth,
|
|
275
|
-
HeadingOnly,
|
|
276
|
-
CtaBanner,
|
|
277
|
-
Accordion
|
|
278
|
-
]
|
|
279
|
-
}
|
|
280
|
-
]
|
|
281
|
-
}
|
|
282
|
-
]
|
|
283
|
-
}
|
|
284
|
-
]
|
|
285
|
-
};
|
|
286
|
-
// ─── introspectCollection ──────────────────────────────────────────
|
|
287
|
-
describe('introspectCollection', ()=>{
|
|
288
|
-
it('extracts Posts collection fields, relationships, and draft status', ()=>{
|
|
289
|
-
const schema = introspectCollection(Posts);
|
|
290
|
-
expect(schema.slug).toBe('posts');
|
|
291
|
-
expect(schema.hasDrafts).toBe(true);
|
|
292
|
-
const fieldNames = schema.fields.map((f)=>f.name);
|
|
293
|
-
expect(fieldNames).toContain('title');
|
|
294
|
-
expect(fieldNames).toContain('slug');
|
|
295
|
-
expect(fieldNames).toContain('featured');
|
|
296
|
-
expect(fieldNames).toContain('tags');
|
|
297
|
-
const relFieldNames = schema.relationships.map((r)=>r.fieldName);
|
|
298
|
-
expect(relFieldNames).toContain('category');
|
|
299
|
-
expect(relFieldNames).toContain('authors');
|
|
300
|
-
const cover = schema.relationships.find((r)=>r.fieldName === 'coverImage');
|
|
301
|
-
expect(cover).toBeDefined();
|
|
302
|
-
expect(cover.relationTo).toBe('media');
|
|
303
|
-
expect(schema.searchableFields).toContain('title');
|
|
304
|
-
expect(schema.searchableFields).toContain('slug');
|
|
305
|
-
});
|
|
306
|
-
it('extracts Pages collection with tab-nested fields', ()=>{
|
|
307
|
-
const schema = introspectCollection(Pages);
|
|
308
|
-
expect(schema.slug).toBe('pages');
|
|
309
|
-
expect(schema.hasDrafts).toBe(true);
|
|
310
|
-
const fieldNames = schema.fields.map((f)=>f.name);
|
|
311
|
-
expect(fieldNames).toContain('heroTitle');
|
|
312
|
-
expect(fieldNames).toContain('slug');
|
|
313
|
-
expect(fieldNames).toContain('layout');
|
|
314
|
-
});
|
|
315
|
-
it('detects collections without draft support', ()=>{
|
|
316
|
-
const schema = introspectCollection(Categories);
|
|
317
|
-
expect(schema.hasDrafts).toBe(false);
|
|
318
|
-
});
|
|
319
|
-
it('extracts select field options from Pages heroSize', ()=>{
|
|
320
|
-
const schema = introspectCollection(Pages);
|
|
321
|
-
const heroSize = schema.fields.find((f)=>f.name === 'heroSize');
|
|
322
|
-
expect(heroSize).toBeDefined();
|
|
323
|
-
expect(heroSize.type).toBe('select');
|
|
324
|
-
expect(heroSize.options).toBeDefined();
|
|
325
|
-
expect(heroSize.options.length).toBe(3);
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
// ─── introspectBlocks (flat catalog) ───────────────────────────────
|
|
329
|
-
describe('introspectBlocks', ()=>{
|
|
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([
|
|
334
|
-
'heading',
|
|
335
|
-
'richText',
|
|
336
|
-
'image',
|
|
337
|
-
'fullWidth',
|
|
338
|
-
'headingOnly',
|
|
339
|
-
'ctaBanner',
|
|
340
|
-
'accordion'
|
|
341
|
-
]);
|
|
342
|
-
});
|
|
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');
|
|
346
|
-
expect(heading).toBeDefined();
|
|
347
|
-
const headingFieldNames = heading.fields.map((f)=>f.name);
|
|
348
|
-
expect(headingFieldNames).toEqual([
|
|
349
|
-
'text',
|
|
350
|
-
'level',
|
|
351
|
-
'align'
|
|
352
|
-
]);
|
|
353
|
-
const level = heading.fields.find((f)=>f.name === 'level');
|
|
354
|
-
expect(level.options).toBeDefined();
|
|
355
|
-
});
|
|
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 ────────────────────────────────────────
|
|
439
|
-
// ─── Global introspection ──────────────────────────────────────────
|
|
440
|
-
const SiteSettings = {
|
|
441
|
-
slug: 'site-settings',
|
|
442
|
-
fields: [
|
|
443
|
-
{
|
|
444
|
-
name: 'siteName',
|
|
445
|
-
type: 'text',
|
|
446
|
-
required: true
|
|
447
|
-
},
|
|
448
|
-
{
|
|
449
|
-
name: 'tagline',
|
|
450
|
-
type: 'text'
|
|
451
|
-
},
|
|
452
|
-
{
|
|
453
|
-
name: 'social',
|
|
454
|
-
type: 'group',
|
|
455
|
-
fields: [
|
|
456
|
-
{
|
|
457
|
-
name: 'twitter',
|
|
458
|
-
type: 'text'
|
|
459
|
-
},
|
|
460
|
-
{
|
|
461
|
-
name: 'instagram',
|
|
462
|
-
type: 'text'
|
|
463
|
-
}
|
|
464
|
-
]
|
|
465
|
-
}
|
|
466
|
-
]
|
|
467
|
-
};
|
|
468
|
-
const FooterGlobal = {
|
|
469
|
-
slug: 'footer',
|
|
470
|
-
versions: {
|
|
471
|
-
drafts: true
|
|
472
|
-
},
|
|
473
|
-
fields: [
|
|
474
|
-
{
|
|
475
|
-
name: 'layout',
|
|
476
|
-
type: 'blocks',
|
|
477
|
-
blocks: [
|
|
478
|
-
Heading,
|
|
479
|
-
CtaBanner
|
|
480
|
-
]
|
|
481
|
-
}
|
|
482
|
-
]
|
|
483
|
-
};
|
|
484
|
-
const HeaderGlobal = {
|
|
485
|
-
slug: 'header',
|
|
486
|
-
fields: [
|
|
487
|
-
{
|
|
488
|
-
name: 'menu',
|
|
489
|
-
type: 'group',
|
|
490
|
-
fields: [
|
|
491
|
-
{
|
|
492
|
-
name: 'label',
|
|
493
|
-
type: 'text'
|
|
494
|
-
},
|
|
495
|
-
{
|
|
496
|
-
name: 'links',
|
|
497
|
-
type: 'blocks',
|
|
498
|
-
blocks: [
|
|
499
|
-
Heading
|
|
500
|
-
]
|
|
501
|
-
}
|
|
502
|
-
]
|
|
503
|
-
}
|
|
504
|
-
]
|
|
505
|
-
};
|
|
506
|
-
describe('hasGlobalDrafts', ()=>{
|
|
507
|
-
it('returns true for { versions: { drafts: true } }', ()=>{
|
|
508
|
-
expect(hasGlobalDrafts({
|
|
509
|
-
slug: 'g',
|
|
510
|
-
versions: {
|
|
511
|
-
drafts: true
|
|
512
|
-
},
|
|
513
|
-
fields: []
|
|
514
|
-
})).toBe(true);
|
|
515
|
-
});
|
|
516
|
-
it('returns false for { versions: { drafts: false } }', ()=>{
|
|
517
|
-
expect(hasGlobalDrafts({
|
|
518
|
-
slug: 'g',
|
|
519
|
-
versions: {
|
|
520
|
-
drafts: false
|
|
521
|
-
},
|
|
522
|
-
fields: []
|
|
523
|
-
})).toBe(false);
|
|
524
|
-
});
|
|
525
|
-
it('returns false when versions is undefined', ()=>{
|
|
526
|
-
expect(hasGlobalDrafts({
|
|
527
|
-
slug: 'g',
|
|
528
|
-
fields: []
|
|
529
|
-
})).toBe(false);
|
|
530
|
-
});
|
|
531
|
-
it('returns false for versions without drafts key', ()=>{
|
|
532
|
-
expect(hasGlobalDrafts({
|
|
533
|
-
slug: 'g',
|
|
534
|
-
versions: {
|
|
535
|
-
maxPerDoc: 10
|
|
536
|
-
},
|
|
537
|
-
fields: []
|
|
538
|
-
})).toBe(false);
|
|
539
|
-
});
|
|
540
|
-
});
|
|
541
|
-
describe('introspectGlobal', ()=>{
|
|
542
|
-
it('extracts SiteSettings fields and draft/live-preview flags', ()=>{
|
|
543
|
-
const schema = introspectGlobal(SiteSettings);
|
|
544
|
-
expect(schema.slug).toBe('site-settings');
|
|
545
|
-
expect(schema.hasDrafts).toBe(false);
|
|
546
|
-
expect(schema.hasLivePreview).toBe(false);
|
|
547
|
-
const names = schema.fields.map((f)=>f.name);
|
|
548
|
-
expect(names).toContain('siteName');
|
|
549
|
-
expect(names).toContain('tagline');
|
|
550
|
-
const social = schema.fields.find((f)=>f.name === 'social');
|
|
551
|
-
expect(social?.type).toBe('group');
|
|
552
|
-
expect(social?.fields?.map((f)=>f.name)).toEqual([
|
|
553
|
-
'twitter',
|
|
554
|
-
'instagram'
|
|
555
|
-
]);
|
|
556
|
-
});
|
|
557
|
-
it('reports hasDrafts: true when versions.drafts is set', ()=>{
|
|
558
|
-
expect(introspectGlobal(FooterGlobal).hasDrafts).toBe(true);
|
|
559
|
-
});
|
|
560
|
-
});
|
|
561
|
-
describe('introspectGlobals', ()=>{
|
|
562
|
-
it('returns an empty Map for []', ()=>{
|
|
563
|
-
expect(introspectGlobals([]).size).toBe(0);
|
|
564
|
-
});
|
|
565
|
-
it('keys the map by slug', ()=>{
|
|
566
|
-
const map = introspectGlobals([
|
|
567
|
-
SiteSettings,
|
|
568
|
-
FooterGlobal
|
|
569
|
-
]);
|
|
570
|
-
expect(map.has('site-settings')).toBe(true);
|
|
571
|
-
expect(map.has('footer')).toBe(true);
|
|
572
|
-
});
|
|
573
|
-
});
|
|
574
|
-
describe('buildBlockNestingMap with globals', ()=>{
|
|
575
|
-
it('emits an edge with ownerType "global" for a top-level blocks field', ()=>{
|
|
576
|
-
const map = buildBlockNestingMap([], [
|
|
577
|
-
FooterGlobal
|
|
578
|
-
], allBlocks);
|
|
579
|
-
const edge = map.find((e)=>e.ownerType === 'global' && e.owner === 'footer' && e.fieldPath === 'layout');
|
|
580
|
-
expect(edge).toBeDefined();
|
|
581
|
-
expect(edge.acceptedBlockSlugs).toEqual([
|
|
582
|
-
'heading',
|
|
583
|
-
'ctaBanner'
|
|
584
|
-
]);
|
|
585
|
-
});
|
|
586
|
-
it('walks group-nested blocks fields under a global with the dotted path', ()=>{
|
|
587
|
-
const map = buildBlockNestingMap([], [
|
|
588
|
-
HeaderGlobal
|
|
589
|
-
], allBlocks);
|
|
590
|
-
const edge = map.find((e)=>e.ownerType === 'global' && e.owner === 'header' && e.fieldPath === 'menu.links');
|
|
591
|
-
expect(edge).toBeDefined();
|
|
592
|
-
expect(edge.acceptedBlockSlugs).toEqual([
|
|
593
|
-
'heading'
|
|
594
|
-
]);
|
|
595
|
-
});
|
|
596
|
-
it('two-arg call (no globals) produces the same edges as before — regression guard', ()=>{
|
|
597
|
-
const before = buildBlockNestingMap([
|
|
598
|
-
Pages
|
|
599
|
-
], allBlocks);
|
|
600
|
-
const after = buildBlockNestingMap([
|
|
601
|
-
Pages
|
|
602
|
-
], [], allBlocks);
|
|
603
|
-
expect(after).toEqual(before);
|
|
604
|
-
});
|
|
605
|
-
it('invariant: throws when (owner, fieldPath) appears with different ownerTypes', ()=>{
|
|
606
|
-
const ClashCollection = {
|
|
607
|
-
slug: 'site-settings',
|
|
608
|
-
fields: [
|
|
609
|
-
{
|
|
610
|
-
name: 'layout',
|
|
611
|
-
type: 'blocks',
|
|
612
|
-
blocks: [
|
|
613
|
-
Heading
|
|
614
|
-
]
|
|
615
|
-
}
|
|
616
|
-
]
|
|
617
|
-
};
|
|
618
|
-
const ClashGlobal = {
|
|
619
|
-
slug: 'site-settings',
|
|
620
|
-
fields: [
|
|
621
|
-
{
|
|
622
|
-
name: 'layout',
|
|
623
|
-
type: 'blocks',
|
|
624
|
-
blocks: [
|
|
625
|
-
Heading
|
|
626
|
-
]
|
|
627
|
-
}
|
|
628
|
-
]
|
|
629
|
-
};
|
|
630
|
-
expect(()=>buildBlockNestingMap([
|
|
631
|
-
ClashCollection
|
|
632
|
-
], [
|
|
633
|
-
ClashGlobal
|
|
634
|
-
], [
|
|
635
|
-
Heading
|
|
636
|
-
])).toThrow(/invariant violated/i);
|
|
637
|
-
});
|
|
638
|
-
});
|
|
639
|
-
describe('buildRelationshipGraph', ()=>{
|
|
640
|
-
it('builds correct graph from sample collections', ()=>{
|
|
641
|
-
const schemas = introspectCollections([
|
|
642
|
-
Posts,
|
|
643
|
-
Pages,
|
|
644
|
-
Categories,
|
|
645
|
-
Authors,
|
|
646
|
-
Media
|
|
647
|
-
]);
|
|
648
|
-
const edges = buildRelationshipGraph(schemas);
|
|
649
|
-
const postEdges = edges.filter((e)=>e.fromCollection === 'posts');
|
|
650
|
-
const postTargets = postEdges.map((e)=>e.toCollection);
|
|
651
|
-
expect(postTargets).toContain('categories');
|
|
652
|
-
expect(postTargets).toContain('authors');
|
|
653
|
-
expect(postTargets).toContain('media');
|
|
654
|
-
const authorEdges = edges.filter((e)=>e.fromCollection === 'authors');
|
|
655
|
-
expect(authorEdges.map((e)=>e.toCollection)).toContain('media');
|
|
656
|
-
});
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
//# sourceMappingURL=introspection.test.js.map
|