payload-mcp-toolkit 0.3.3 → 0.7.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 (144) hide show
  1. package/README.md +232 -150
  2. package/dist/__tests__/api-keys.test.js +292 -0
  3. package/dist/__tests__/api-keys.test.js.map +1 -0
  4. package/dist/__tests__/auth-strategy.test.js +681 -0
  5. package/dist/__tests__/auth-strategy.test.js.map +1 -0
  6. package/dist/__tests__/conflict-detection.test.js +69 -0
  7. package/dist/__tests__/conflict-detection.test.js.map +1 -0
  8. package/dist/__tests__/delete-document.test.js +70 -0
  9. package/dist/__tests__/delete-document.test.js.map +1 -0
  10. package/dist/__tests__/endpoint.test.js +143 -0
  11. package/dist/__tests__/endpoint.test.js.map +1 -0
  12. package/dist/__tests__/find-document.test.js +178 -0
  13. package/dist/__tests__/find-document.test.js.map +1 -0
  14. package/dist/__tests__/find-global.test.js +173 -0
  15. package/dist/__tests__/find-global.test.js.map +1 -0
  16. package/dist/__tests__/global-versions.test.js +183 -0
  17. package/dist/__tests__/global-versions.test.js.map +1 -0
  18. package/dist/__tests__/hash.test.js +58 -0
  19. package/dist/__tests__/hash.test.js.map +1 -0
  20. package/dist/__tests__/index-integration.test.js +191 -0
  21. package/dist/__tests__/index-integration.test.js.map +1 -0
  22. package/dist/__tests__/introspection.test.js +201 -1
  23. package/dist/__tests__/introspection.test.js.map +1 -1
  24. package/dist/__tests__/patch-global-layout.test.js +474 -0
  25. package/dist/__tests__/patch-global-layout.test.js.map +1 -0
  26. package/dist/__tests__/patch-layout.test.js +171 -0
  27. package/dist/__tests__/patch-layout.test.js.map +1 -0
  28. package/dist/__tests__/registry.test.js +795 -0
  29. package/dist/__tests__/registry.test.js.map +1 -0
  30. package/dist/__tests__/resources.test.js +139 -0
  31. package/dist/__tests__/resources.test.js.map +1 -0
  32. package/dist/__tests__/update-global.test.js +157 -0
  33. package/dist/__tests__/update-global.test.js.map +1 -0
  34. package/dist/api-keys.d.ts +46 -0
  35. package/dist/api-keys.js +272 -0
  36. package/dist/api-keys.js.map +1 -0
  37. package/dist/auth-strategy.d.ts +85 -0
  38. package/dist/auth-strategy.js +219 -0
  39. package/dist/auth-strategy.js.map +1 -0
  40. package/dist/components/CollectionScopesMatrix.d.ts +8 -0
  41. package/dist/components/CollectionScopesMatrix.js +32 -0
  42. package/dist/components/CollectionScopesMatrix.js.map +1 -0
  43. package/dist/components/GlobalScopesMatrix.d.ts +8 -0
  44. package/dist/components/GlobalScopesMatrix.js +28 -0
  45. package/dist/components/GlobalScopesMatrix.js.map +1 -0
  46. package/dist/components/ScopesTable.d.ts +19 -0
  47. package/dist/components/ScopesTable.js +285 -0
  48. package/dist/components/ScopesTable.js.map +1 -0
  49. package/dist/components/index.d.ts +2 -0
  50. package/dist/components/index.js +4 -0
  51. package/dist/components/index.js.map +1 -0
  52. package/dist/conflict-detection.d.ts +13 -0
  53. package/dist/conflict-detection.js +41 -0
  54. package/dist/conflict-detection.js.map +1 -0
  55. package/dist/draft-workflow.d.ts +46 -47
  56. package/dist/draft-workflow.js +53 -130
  57. package/dist/draft-workflow.js.map +1 -1
  58. package/dist/endpoint.d.ts +35 -0
  59. package/dist/endpoint.js +105 -0
  60. package/dist/endpoint.js.map +1 -0
  61. package/dist/hash.d.ts +21 -0
  62. package/dist/hash.js +36 -0
  63. package/dist/hash.js.map +1 -0
  64. package/dist/index.d.ts +9 -9
  65. package/dist/index.js +168 -68
  66. package/dist/index.js.map +1 -1
  67. package/dist/introspection.d.ts +17 -3
  68. package/dist/introspection.js +95 -36
  69. package/dist/introspection.js.map +1 -1
  70. package/dist/prompts.js +5 -5
  71. package/dist/prompts.js.map +1 -1
  72. package/dist/registry.d.ts +50 -0
  73. package/dist/registry.js +169 -0
  74. package/dist/registry.js.map +1 -0
  75. package/dist/resources.d.ts +5 -3
  76. package/dist/resources.js +23 -11
  77. package/dist/resources.js.map +1 -1
  78. package/dist/scope/audit-log.d.ts +18 -0
  79. package/dist/scope/audit-log.js +50 -0
  80. package/dist/scope/audit-log.js.map +1 -0
  81. package/dist/scope/policy.d.ts +73 -0
  82. package/dist/scope/policy.js +218 -0
  83. package/dist/scope/policy.js.map +1 -0
  84. package/dist/tools/_helpers.d.ts +28 -1
  85. package/dist/tools/_helpers.js +83 -0
  86. package/dist/tools/_helpers.js.map +1 -1
  87. package/dist/tools/_layout-helpers.d.ts +43 -0
  88. package/dist/tools/_layout-helpers.js +159 -0
  89. package/dist/tools/_layout-helpers.js.map +1 -0
  90. package/dist/tools/create-document.d.ts +36 -0
  91. package/dist/tools/create-document.js +83 -0
  92. package/dist/tools/create-document.js.map +1 -0
  93. package/dist/tools/delete-document.d.ts +25 -0
  94. package/dist/tools/delete-document.js +49 -0
  95. package/dist/tools/delete-document.js.map +1 -0
  96. package/dist/tools/find-document.d.ts +33 -0
  97. package/dist/tools/find-document.js +97 -0
  98. package/dist/tools/find-document.js.map +1 -0
  99. package/dist/tools/find-global.d.ts +26 -0
  100. package/dist/tools/find-global.js +122 -0
  101. package/dist/tools/find-global.js.map +1 -0
  102. package/dist/tools/global-versions.d.ts +39 -0
  103. package/dist/tools/global-versions.js +132 -0
  104. package/dist/tools/global-versions.js.map +1 -0
  105. package/dist/tools/patch-global-layout.d.ts +31 -0
  106. package/dist/tools/patch-global-layout.js +127 -0
  107. package/dist/tools/patch-global-layout.js.map +1 -0
  108. package/dist/tools/patch-layout.d.ts +5 -8
  109. package/dist/tools/patch-layout.js +18 -100
  110. package/dist/tools/patch-layout.js.map +1 -1
  111. package/dist/tools/publish-draft.d.ts +5 -4
  112. package/dist/tools/publish-draft.js +6 -1
  113. package/dist/tools/publish-draft.js.map +1 -1
  114. package/dist/tools/publish-global-draft.d.ts +20 -0
  115. package/dist/tools/publish-global-draft.js +50 -0
  116. package/dist/tools/publish-global-draft.js.map +1 -0
  117. package/dist/tools/resolve-reference.d.ts +5 -4
  118. package/dist/tools/resolve-reference.js +4 -0
  119. package/dist/tools/resolve-reference.js.map +1 -1
  120. package/dist/tools/safe-delete.d.ts +5 -5
  121. package/dist/tools/safe-delete.js +20 -15
  122. package/dist/tools/safe-delete.js.map +1 -1
  123. package/dist/tools/schedule-publish.d.ts +5 -5
  124. package/dist/tools/schedule-publish.js +23 -19
  125. package/dist/tools/schedule-publish.js.map +1 -1
  126. package/dist/tools/search-content.d.ts +5 -9
  127. package/dist/tools/search-content.js +16 -12
  128. package/dist/tools/search-content.js.map +1 -1
  129. package/dist/tools/update-document.d.ts +5 -5
  130. package/dist/tools/update-document.js +10 -5
  131. package/dist/tools/update-document.js.map +1 -1
  132. package/dist/tools/update-global.d.ts +27 -0
  133. package/dist/tools/update-global.js +72 -0
  134. package/dist/tools/update-global.js.map +1 -0
  135. package/dist/tools/upload-media.d.ts +5 -4
  136. package/dist/tools/upload-media.js +6 -1
  137. package/dist/tools/upload-media.js.map +1 -1
  138. package/dist/tools/versions.d.ts +10 -9
  139. package/dist/tools/versions.js +15 -7
  140. package/dist/tools/versions.js.map +1 -1
  141. package/dist/types.d.ts +56 -3
  142. package/dist/types.js +13 -6
  143. package/dist/types.js.map +1 -1
  144. package/package.json +11 -4
@@ -0,0 +1,474 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { introspectBlocks, buildBlockNestingMap } from '../introspection';
3
+ import { createPatchGlobalLayoutTool } from '../tools/patch-global-layout';
4
+ const Heading = {
5
+ slug: 'heading',
6
+ fields: [
7
+ {
8
+ name: 'text',
9
+ type: 'text'
10
+ }
11
+ ]
12
+ };
13
+ const CtaBanner = {
14
+ slug: 'ctaBanner',
15
+ fields: [
16
+ {
17
+ name: 'label',
18
+ type: 'text'
19
+ }
20
+ ]
21
+ };
22
+ const Container = {
23
+ slug: 'container',
24
+ fields: [
25
+ {
26
+ name: 'sections',
27
+ type: 'blocks',
28
+ blocks: [
29
+ Heading,
30
+ CtaBanner
31
+ ]
32
+ }
33
+ ]
34
+ };
35
+ const FooterGlobal = {
36
+ slug: 'footer',
37
+ versions: {
38
+ drafts: true
39
+ },
40
+ fields: [
41
+ {
42
+ name: 'sections',
43
+ type: 'blocks',
44
+ blocks: [
45
+ Heading,
46
+ CtaBanner,
47
+ Container
48
+ ]
49
+ }
50
+ ]
51
+ };
52
+ const PlainSettings = {
53
+ slug: 'plain',
54
+ fields: [
55
+ {
56
+ name: 'siteName',
57
+ type: 'text'
58
+ }
59
+ ]
60
+ };
61
+ function buildReq() {
62
+ return {
63
+ payload: {
64
+ findGlobal: vi.fn(),
65
+ updateGlobal: vi.fn(),
66
+ logger: {
67
+ info: vi.fn(),
68
+ warn: vi.fn(),
69
+ error: vi.fn()
70
+ }
71
+ },
72
+ context: {},
73
+ user: null
74
+ };
75
+ }
76
+ const allBlocks = [
77
+ Heading,
78
+ CtaBanner,
79
+ Container
80
+ ];
81
+ const catalog = introspectBlocks(allBlocks);
82
+ const nesting = buildBlockNestingMap([], [
83
+ FooterGlobal
84
+ ], allBlocks);
85
+ const drafts = new Set([
86
+ 'footer'
87
+ ]);
88
+ describe('patchGlobalLayout', ()=>{
89
+ it('returns null when no global has a blocks field (factory short-circuit)', ()=>{
90
+ const emptyNesting = buildBlockNestingMap([], [
91
+ PlainSettings
92
+ ], allBlocks);
93
+ const tool = createPatchGlobalLayoutTool(catalog, emptyNesting, new Set());
94
+ expect(tool).toBeNull();
95
+ });
96
+ it('registers when at least one global has a blocks field', ()=>{
97
+ const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts);
98
+ expect(tool).not.toBeNull();
99
+ expect(tool.name).toBe('patchGlobalLayout');
100
+ });
101
+ it('append: writes existing + new blocks back via updateGlobal', async ()=>{
102
+ const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts);
103
+ const req = buildReq();
104
+ req.payload.findGlobal.mockResolvedValue({
105
+ sections: [
106
+ {
107
+ blockType: 'heading',
108
+ text: 'a'
109
+ }
110
+ ]
111
+ });
112
+ req.payload.updateGlobal.mockResolvedValue({});
113
+ await tool.handler({
114
+ slug: 'footer',
115
+ layoutField: 'sections',
116
+ blocks: [
117
+ {
118
+ blockType: 'ctaBanner',
119
+ label: 'Buy'
120
+ }
121
+ ],
122
+ operation: 'append'
123
+ }, req, {});
124
+ expect(req.payload.updateGlobal).toHaveBeenCalledWith(expect.objectContaining({
125
+ slug: 'footer',
126
+ draft: true,
127
+ data: {
128
+ sections: [
129
+ {
130
+ blockType: 'heading',
131
+ text: 'a'
132
+ },
133
+ {
134
+ blockType: 'ctaBanner',
135
+ label: 'Buy'
136
+ }
137
+ ]
138
+ }
139
+ }));
140
+ });
141
+ it('replaceAt index 0 swaps the first block', async ()=>{
142
+ const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts);
143
+ const req = buildReq();
144
+ req.payload.findGlobal.mockResolvedValue({
145
+ sections: [
146
+ {
147
+ blockType: 'heading',
148
+ text: 'a'
149
+ },
150
+ {
151
+ blockType: 'ctaBanner',
152
+ label: 'old'
153
+ }
154
+ ]
155
+ });
156
+ req.payload.updateGlobal.mockResolvedValue({});
157
+ await tool.handler({
158
+ slug: 'footer',
159
+ layoutField: 'sections',
160
+ blocks: [
161
+ {
162
+ blockType: 'heading',
163
+ text: 'new-first'
164
+ }
165
+ ],
166
+ operation: 'replaceAt',
167
+ insertIndex: 0
168
+ }, req, {});
169
+ expect(req.payload.updateGlobal).toHaveBeenCalledWith(expect.objectContaining({
170
+ data: {
171
+ sections: [
172
+ {
173
+ blockType: 'heading',
174
+ text: 'new-first'
175
+ },
176
+ {
177
+ blockType: 'ctaBanner',
178
+ label: 'old'
179
+ }
180
+ ]
181
+ }
182
+ }));
183
+ });
184
+ it('rejects blocks whose slug is not in the field allow-list', async ()=>{
185
+ const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts);
186
+ const req = buildReq();
187
+ const result = await tool.handler({
188
+ slug: 'footer',
189
+ layoutField: 'sections',
190
+ // ImageBlock is not in the FooterGlobal allow list.
191
+ blocks: [
192
+ {
193
+ blockType: 'image',
194
+ src: 'x'
195
+ }
196
+ ],
197
+ operation: 'full'
198
+ }, req, {});
199
+ const text = result.content[0].text;
200
+ expect(text).toMatch(/unknown blockType/i);
201
+ expect(text).toContain('image');
202
+ expect(req.payload.updateGlobal).not.toHaveBeenCalled();
203
+ });
204
+ it('validates nested blocks fields with breadcrumb path on failure', async ()=>{
205
+ const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts);
206
+ const req = buildReq();
207
+ const result = await tool.handler({
208
+ slug: 'footer',
209
+ layoutField: 'sections',
210
+ // Container.sections allows only heading + ctaBanner, but here we shove
211
+ // a non-existent slug to ensure the nested breadcrumb is reported.
212
+ blocks: [
213
+ {
214
+ blockType: 'container',
215
+ sections: [
216
+ {
217
+ blockType: 'mystery',
218
+ foo: 1
219
+ }
220
+ ]
221
+ }
222
+ ],
223
+ operation: 'full'
224
+ }, req, {});
225
+ const text = result.content[0].text;
226
+ expect(text).toMatch(/sections\[0\]\.sections\[0\]/);
227
+ expect(text).toMatch(/mystery/);
228
+ expect(req.payload.updateGlobal).not.toHaveBeenCalled();
229
+ });
230
+ it('dotted layoutField preserves sibling fields in the parent group', async ()=>{
231
+ // Reg test for the sibling-wipe bug: a dotted layoutField like
232
+ // "sections.layout" used to write {sections: {layout: [...]}} which
233
+ // Payload merges at the top level only — silently wiping
234
+ // sections.copyright. writePath now reads the existing parent group
235
+ // off the fetched global and merges siblings in.
236
+ const NestedFooter = {
237
+ slug: 'nestedFooter',
238
+ versions: {
239
+ drafts: true
240
+ },
241
+ fields: [
242
+ {
243
+ name: 'sections',
244
+ type: 'group',
245
+ fields: [
246
+ {
247
+ name: 'copyright',
248
+ type: 'text'
249
+ },
250
+ {
251
+ name: 'layout',
252
+ type: 'blocks',
253
+ blocks: [
254
+ Heading,
255
+ CtaBanner
256
+ ]
257
+ }
258
+ ]
259
+ }
260
+ ]
261
+ };
262
+ const nestedNesting = buildBlockNestingMap([], [
263
+ NestedFooter
264
+ ], allBlocks);
265
+ const tool = createPatchGlobalLayoutTool(catalog, nestedNesting, new Set([
266
+ 'nestedFooter'
267
+ ]));
268
+ const req = buildReq();
269
+ req.payload.findGlobal.mockResolvedValue({
270
+ sections: {
271
+ copyright: 'do-not-wipe',
272
+ layout: [
273
+ {
274
+ blockType: 'heading',
275
+ text: 'a'
276
+ }
277
+ ]
278
+ }
279
+ });
280
+ req.payload.updateGlobal.mockResolvedValue({});
281
+ await tool.handler({
282
+ slug: 'nestedFooter',
283
+ layoutField: 'sections.layout',
284
+ blocks: [
285
+ {
286
+ blockType: 'ctaBanner',
287
+ label: 'Buy'
288
+ }
289
+ ],
290
+ operation: 'append'
291
+ }, req, {});
292
+ expect(req.payload.updateGlobal).toHaveBeenCalledWith(expect.objectContaining({
293
+ data: {
294
+ sections: {
295
+ copyright: 'do-not-wipe',
296
+ layout: [
297
+ {
298
+ blockType: 'heading',
299
+ text: 'a'
300
+ },
301
+ {
302
+ blockType: 'ctaBanner',
303
+ label: 'Buy'
304
+ }
305
+ ]
306
+ }
307
+ }
308
+ }));
309
+ });
310
+ it('multi-level dotted layoutField preserves siblings at every depth', async ()=>{
311
+ const DeepFooter = {
312
+ slug: 'deepFooter',
313
+ versions: {
314
+ drafts: true
315
+ },
316
+ fields: [
317
+ {
318
+ name: 'a',
319
+ type: 'group',
320
+ fields: [
321
+ {
322
+ name: 'keepA',
323
+ type: 'text'
324
+ },
325
+ {
326
+ name: 'b',
327
+ type: 'group',
328
+ fields: [
329
+ {
330
+ name: 'keepB',
331
+ type: 'text'
332
+ },
333
+ {
334
+ name: 'layout',
335
+ type: 'blocks',
336
+ blocks: [
337
+ Heading
338
+ ]
339
+ }
340
+ ]
341
+ }
342
+ ]
343
+ }
344
+ ]
345
+ };
346
+ const deepNesting = buildBlockNestingMap([], [
347
+ DeepFooter
348
+ ], allBlocks);
349
+ const tool = createPatchGlobalLayoutTool(catalog, deepNesting, new Set([
350
+ 'deepFooter'
351
+ ]));
352
+ const req = buildReq();
353
+ req.payload.findGlobal.mockResolvedValue({
354
+ a: {
355
+ keepA: 'A-survives',
356
+ b: {
357
+ keepB: 'B-survives',
358
+ layout: []
359
+ }
360
+ }
361
+ });
362
+ req.payload.updateGlobal.mockResolvedValue({});
363
+ await tool.handler({
364
+ slug: 'deepFooter',
365
+ layoutField: 'a.b.layout',
366
+ blocks: [
367
+ {
368
+ blockType: 'heading',
369
+ text: 'new'
370
+ }
371
+ ],
372
+ operation: 'full'
373
+ }, req, {});
374
+ expect(req.payload.updateGlobal).toHaveBeenCalledWith(expect.objectContaining({
375
+ data: {
376
+ a: {
377
+ keepA: 'A-survives',
378
+ b: {
379
+ keepB: 'B-survives',
380
+ layout: [
381
+ {
382
+ blockType: 'heading',
383
+ text: 'new'
384
+ }
385
+ ]
386
+ }
387
+ }
388
+ }
389
+ }));
390
+ });
391
+ it('expectedUpdatedAt mismatch rejects the patch without writing', async ()=>{
392
+ const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts);
393
+ const req = buildReq();
394
+ req.payload.findGlobal.mockResolvedValue({
395
+ sections: [],
396
+ updatedAt: '2026-01-02T00:00:00.000Z'
397
+ });
398
+ const result = await tool.handler({
399
+ slug: 'footer',
400
+ layoutField: 'sections',
401
+ blocks: [
402
+ {
403
+ blockType: 'heading',
404
+ text: 'a'
405
+ }
406
+ ],
407
+ operation: 'append',
408
+ expectedUpdatedAt: '2026-01-01T00:00:00.000Z'
409
+ }, req, {});
410
+ expect(result.content[0].text).toMatch(/conflict/i);
411
+ expect(req.payload.updateGlobal).not.toHaveBeenCalled();
412
+ });
413
+ it('expectedUpdatedAt match proceeds with the patch', async ()=>{
414
+ const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts);
415
+ const req = buildReq();
416
+ req.payload.findGlobal.mockResolvedValue({
417
+ sections: [],
418
+ updatedAt: '2026-01-02T00:00:00.000Z'
419
+ });
420
+ req.payload.updateGlobal.mockResolvedValue({});
421
+ await tool.handler({
422
+ slug: 'footer',
423
+ layoutField: 'sections',
424
+ blocks: [
425
+ {
426
+ blockType: 'heading',
427
+ text: 'a'
428
+ }
429
+ ],
430
+ operation: 'append',
431
+ expectedUpdatedAt: '2026-01-02T00:00:00.000Z'
432
+ }, req, {});
433
+ expect(req.payload.updateGlobal).toHaveBeenCalled();
434
+ });
435
+ it('locale arg is forwarded to findGlobal and updateGlobal', async ()=>{
436
+ const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts);
437
+ const req = buildReq();
438
+ req.payload.findGlobal.mockResolvedValue({
439
+ sections: []
440
+ });
441
+ req.payload.updateGlobal.mockResolvedValue({});
442
+ await tool.handler({
443
+ slug: 'footer',
444
+ layoutField: 'sections',
445
+ blocks: [
446
+ {
447
+ blockType: 'heading',
448
+ text: 'a'
449
+ }
450
+ ],
451
+ operation: 'append',
452
+ locale: 'fr'
453
+ }, req, {});
454
+ expect(req.payload.findGlobal).toHaveBeenCalledWith(expect.objectContaining({
455
+ locale: 'fr'
456
+ }));
457
+ expect(req.payload.updateGlobal).toHaveBeenCalledWith(expect.objectContaining({
458
+ locale: 'fr'
459
+ }));
460
+ });
461
+ it('returns an error when layoutField is not a blocks field on the global', async ()=>{
462
+ const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts);
463
+ const req = buildReq();
464
+ const result = await tool.handler({
465
+ slug: 'footer',
466
+ layoutField: 'definitely-not-a-field',
467
+ blocks: [],
468
+ operation: 'full'
469
+ }, req, {});
470
+ expect(result.content[0].text).toMatch(/not a blocks-typed field/i);
471
+ });
472
+ });
473
+
474
+ //# sourceMappingURL=patch-global-layout.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/__tests__/patch-global-layout.test.ts"],"sourcesContent":["import { describe, it, expect, vi } from 'vitest'\r\nimport type { Block, GlobalConfig } from 'payload'\r\nimport { introspectBlocks, buildBlockNestingMap } from '../introspection'\r\nimport { createPatchGlobalLayoutTool } from '../tools/patch-global-layout'\r\n\r\nconst Heading: Block = { slug: 'heading', fields: [{ name: 'text', type: 'text' }] }\r\nconst CtaBanner: Block = { slug: 'ctaBanner', fields: [{ name: 'label', type: 'text' }] }\r\nconst Container: Block = {\r\n slug: 'container',\r\n fields: [\r\n {\r\n name: 'sections',\r\n type: 'blocks',\r\n blocks: [Heading, CtaBanner],\r\n },\r\n ],\r\n}\r\n\r\nconst FooterGlobal: GlobalConfig = {\r\n slug: 'footer',\r\n versions: { drafts: true },\r\n fields: [\r\n {\r\n name: 'sections',\r\n type: 'blocks',\r\n blocks: [Heading, CtaBanner, Container],\r\n },\r\n ],\r\n}\r\n\r\nconst PlainSettings: GlobalConfig = {\r\n slug: 'plain',\r\n fields: [{ name: 'siteName', type: 'text' }],\r\n}\r\n\r\nfunction buildReq() {\r\n return {\r\n payload: {\r\n findGlobal: vi.fn(),\r\n updateGlobal: vi.fn(),\r\n logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },\r\n },\r\n context: {},\r\n user: null,\r\n }\r\n}\r\n\r\nconst allBlocks = [Heading, CtaBanner, Container]\r\nconst catalog = introspectBlocks(allBlocks)\r\nconst nesting = buildBlockNestingMap([], [FooterGlobal], allBlocks)\r\nconst drafts = new Set(['footer'])\r\n\r\ndescribe('patchGlobalLayout', () => {\r\n it('returns null when no global has a blocks field (factory short-circuit)', () => {\r\n const emptyNesting = buildBlockNestingMap([], [PlainSettings], allBlocks)\r\n const tool = createPatchGlobalLayoutTool(catalog, emptyNesting, new Set())\r\n expect(tool).toBeNull()\r\n })\r\n\r\n it('registers when at least one global has a blocks field', () => {\r\n const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts)\r\n expect(tool).not.toBeNull()\r\n expect(tool!.name).toBe('patchGlobalLayout')\r\n })\r\n\r\n it('append: writes existing + new blocks back via updateGlobal', async () => {\r\n const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts)!\r\n const req = buildReq()\r\n req.payload.findGlobal.mockResolvedValue({\r\n sections: [{ blockType: 'heading', text: 'a' }],\r\n })\r\n req.payload.updateGlobal.mockResolvedValue({})\r\n\r\n await tool.handler(\r\n {\r\n slug: 'footer',\r\n layoutField: 'sections',\r\n blocks: [{ blockType: 'ctaBanner', label: 'Buy' }],\r\n operation: 'append',\r\n },\r\n req as never,\r\n {},\r\n )\r\n\r\n expect(req.payload.updateGlobal).toHaveBeenCalledWith(\r\n expect.objectContaining({\r\n slug: 'footer',\r\n draft: true,\r\n data: {\r\n sections: [\r\n { blockType: 'heading', text: 'a' },\r\n { blockType: 'ctaBanner', label: 'Buy' },\r\n ],\r\n },\r\n }),\r\n )\r\n })\r\n\r\n it('replaceAt index 0 swaps the first block', async () => {\r\n const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts)!\r\n const req = buildReq()\r\n req.payload.findGlobal.mockResolvedValue({\r\n sections: [\r\n { blockType: 'heading', text: 'a' },\r\n { blockType: 'ctaBanner', label: 'old' },\r\n ],\r\n })\r\n req.payload.updateGlobal.mockResolvedValue({})\r\n\r\n await tool.handler(\r\n {\r\n slug: 'footer',\r\n layoutField: 'sections',\r\n blocks: [{ blockType: 'heading', text: 'new-first' }],\r\n operation: 'replaceAt',\r\n insertIndex: 0,\r\n },\r\n req as never,\r\n {},\r\n )\r\n\r\n expect(req.payload.updateGlobal).toHaveBeenCalledWith(\r\n expect.objectContaining({\r\n data: {\r\n sections: [\r\n { blockType: 'heading', text: 'new-first' },\r\n { blockType: 'ctaBanner', label: 'old' },\r\n ],\r\n },\r\n }),\r\n )\r\n })\r\n\r\n it('rejects blocks whose slug is not in the field allow-list', async () => {\r\n const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts)!\r\n const req = buildReq()\r\n const result = await tool.handler(\r\n {\r\n slug: 'footer',\r\n layoutField: 'sections',\r\n // ImageBlock is not in the FooterGlobal allow list.\r\n blocks: [{ blockType: 'image', src: 'x' }],\r\n operation: 'full',\r\n },\r\n req as never,\r\n {},\r\n )\r\n const text = result.content[0]!.text\r\n expect(text).toMatch(/unknown blockType/i)\r\n expect(text).toContain('image')\r\n expect(req.payload.updateGlobal).not.toHaveBeenCalled()\r\n })\r\n\r\n it('validates nested blocks fields with breadcrumb path on failure', async () => {\r\n const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts)!\r\n const req = buildReq()\r\n const result = await tool.handler(\r\n {\r\n slug: 'footer',\r\n layoutField: 'sections',\r\n // Container.sections allows only heading + ctaBanner, but here we shove\r\n // a non-existent slug to ensure the nested breadcrumb is reported.\r\n blocks: [\r\n {\r\n blockType: 'container',\r\n sections: [{ blockType: 'mystery', foo: 1 }],\r\n },\r\n ],\r\n operation: 'full',\r\n },\r\n req as never,\r\n {},\r\n )\r\n const text = result.content[0]!.text\r\n expect(text).toMatch(/sections\\[0\\]\\.sections\\[0\\]/)\r\n expect(text).toMatch(/mystery/)\r\n expect(req.payload.updateGlobal).not.toHaveBeenCalled()\r\n })\r\n\r\n it('dotted layoutField preserves sibling fields in the parent group', async () => {\r\n // Reg test for the sibling-wipe bug: a dotted layoutField like\r\n // \"sections.layout\" used to write {sections: {layout: [...]}} which\r\n // Payload merges at the top level only — silently wiping\r\n // sections.copyright. writePath now reads the existing parent group\r\n // off the fetched global and merges siblings in.\r\n const NestedFooter: GlobalConfig = {\r\n slug: 'nestedFooter',\r\n versions: { drafts: true },\r\n fields: [\r\n {\r\n name: 'sections',\r\n type: 'group',\r\n fields: [\r\n { name: 'copyright', type: 'text' },\r\n { name: 'layout', type: 'blocks', blocks: [Heading, CtaBanner] },\r\n ],\r\n },\r\n ],\r\n }\r\n const nestedNesting = buildBlockNestingMap([], [NestedFooter], allBlocks)\r\n const tool = createPatchGlobalLayoutTool(catalog, nestedNesting, new Set(['nestedFooter']))!\r\n const req = buildReq()\r\n req.payload.findGlobal.mockResolvedValue({\r\n sections: {\r\n copyright: 'do-not-wipe',\r\n layout: [{ blockType: 'heading', text: 'a' }],\r\n },\r\n })\r\n req.payload.updateGlobal.mockResolvedValue({})\r\n\r\n await tool.handler(\r\n {\r\n slug: 'nestedFooter',\r\n layoutField: 'sections.layout',\r\n blocks: [{ blockType: 'ctaBanner', label: 'Buy' }],\r\n operation: 'append',\r\n },\r\n req as never,\r\n {},\r\n )\r\n\r\n expect(req.payload.updateGlobal).toHaveBeenCalledWith(\r\n expect.objectContaining({\r\n data: {\r\n sections: {\r\n copyright: 'do-not-wipe',\r\n layout: [\r\n { blockType: 'heading', text: 'a' },\r\n { blockType: 'ctaBanner', label: 'Buy' },\r\n ],\r\n },\r\n },\r\n }),\r\n )\r\n })\r\n\r\n it('multi-level dotted layoutField preserves siblings at every depth', async () => {\r\n const DeepFooter: GlobalConfig = {\r\n slug: 'deepFooter',\r\n versions: { drafts: true },\r\n fields: [\r\n {\r\n name: 'a',\r\n type: 'group',\r\n fields: [\r\n { name: 'keepA', type: 'text' },\r\n {\r\n name: 'b',\r\n type: 'group',\r\n fields: [\r\n { name: 'keepB', type: 'text' },\r\n { name: 'layout', type: 'blocks', blocks: [Heading] },\r\n ],\r\n },\r\n ],\r\n },\r\n ],\r\n }\r\n const deepNesting = buildBlockNestingMap([], [DeepFooter], allBlocks)\r\n const tool = createPatchGlobalLayoutTool(catalog, deepNesting, new Set(['deepFooter']))!\r\n const req = buildReq()\r\n req.payload.findGlobal.mockResolvedValue({\r\n a: {\r\n keepA: 'A-survives',\r\n b: {\r\n keepB: 'B-survives',\r\n layout: [],\r\n },\r\n },\r\n })\r\n req.payload.updateGlobal.mockResolvedValue({})\r\n\r\n await tool.handler(\r\n {\r\n slug: 'deepFooter',\r\n layoutField: 'a.b.layout',\r\n blocks: [{ blockType: 'heading', text: 'new' }],\r\n operation: 'full',\r\n },\r\n req as never,\r\n {},\r\n )\r\n\r\n expect(req.payload.updateGlobal).toHaveBeenCalledWith(\r\n expect.objectContaining({\r\n data: {\r\n a: {\r\n keepA: 'A-survives',\r\n b: {\r\n keepB: 'B-survives',\r\n layout: [{ blockType: 'heading', text: 'new' }],\r\n },\r\n },\r\n },\r\n }),\r\n )\r\n })\r\n\r\n it('expectedUpdatedAt mismatch rejects the patch without writing', async () => {\r\n const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts)!\r\n const req = buildReq()\r\n req.payload.findGlobal.mockResolvedValue({\r\n sections: [],\r\n updatedAt: '2026-01-02T00:00:00.000Z',\r\n })\r\n\r\n const result = await tool.handler(\r\n {\r\n slug: 'footer',\r\n layoutField: 'sections',\r\n blocks: [{ blockType: 'heading', text: 'a' }],\r\n operation: 'append',\r\n expectedUpdatedAt: '2026-01-01T00:00:00.000Z',\r\n },\r\n req as never,\r\n {},\r\n )\r\n\r\n expect(result.content[0]!.text).toMatch(/conflict/i)\r\n expect(req.payload.updateGlobal).not.toHaveBeenCalled()\r\n })\r\n\r\n it('expectedUpdatedAt match proceeds with the patch', async () => {\r\n const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts)!\r\n const req = buildReq()\r\n req.payload.findGlobal.mockResolvedValue({\r\n sections: [],\r\n updatedAt: '2026-01-02T00:00:00.000Z',\r\n })\r\n req.payload.updateGlobal.mockResolvedValue({})\r\n\r\n await tool.handler(\r\n {\r\n slug: 'footer',\r\n layoutField: 'sections',\r\n blocks: [{ blockType: 'heading', text: 'a' }],\r\n operation: 'append',\r\n expectedUpdatedAt: '2026-01-02T00:00:00.000Z',\r\n },\r\n req as never,\r\n {},\r\n )\r\n\r\n expect(req.payload.updateGlobal).toHaveBeenCalled()\r\n })\r\n\r\n it('locale arg is forwarded to findGlobal and updateGlobal', async () => {\r\n const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts)!\r\n const req = buildReq()\r\n req.payload.findGlobal.mockResolvedValue({ sections: [] })\r\n req.payload.updateGlobal.mockResolvedValue({})\r\n\r\n await tool.handler(\r\n {\r\n slug: 'footer',\r\n layoutField: 'sections',\r\n blocks: [{ blockType: 'heading', text: 'a' }],\r\n operation: 'append',\r\n locale: 'fr',\r\n },\r\n req as never,\r\n {},\r\n )\r\n\r\n expect(req.payload.findGlobal).toHaveBeenCalledWith(expect.objectContaining({ locale: 'fr' }))\r\n expect(req.payload.updateGlobal).toHaveBeenCalledWith(expect.objectContaining({ locale: 'fr' }))\r\n })\r\n\r\n it('returns an error when layoutField is not a blocks field on the global', async () => {\r\n const tool = createPatchGlobalLayoutTool(catalog, nesting, drafts)!\r\n const req = buildReq()\r\n const result = await tool.handler(\r\n {\r\n slug: 'footer',\r\n layoutField: 'definitely-not-a-field',\r\n blocks: [],\r\n operation: 'full',\r\n },\r\n req as never,\r\n {},\r\n )\r\n expect(result.content[0]!.text).toMatch(/not a blocks-typed field/i)\r\n })\r\n})\r\n"],"names":["describe","it","expect","vi","introspectBlocks","buildBlockNestingMap","createPatchGlobalLayoutTool","Heading","slug","fields","name","type","CtaBanner","Container","blocks","FooterGlobal","versions","drafts","PlainSettings","buildReq","payload","findGlobal","fn","updateGlobal","logger","info","warn","error","context","user","allBlocks","catalog","nesting","Set","emptyNesting","tool","toBeNull","not","toBe","req","mockResolvedValue","sections","blockType","text","handler","layoutField","label","operation","toHaveBeenCalledWith","objectContaining","draft","data","insertIndex","result","src","content","toMatch","toContain","toHaveBeenCalled","foo","NestedFooter","nestedNesting","copyright","layout","DeepFooter","deepNesting","a","keepA","b","keepB","updatedAt","expectedUpdatedAt","locale"],"mappings":"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"}
@@ -0,0 +1,171 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { introspectBlocks, buildBlockNestingMap } from '../introspection';
3
+ import { createPatchLayoutTool } from '../tools/patch-layout';
4
+ const Heading = {
5
+ slug: 'heading',
6
+ fields: [
7
+ {
8
+ name: 'text',
9
+ type: 'text'
10
+ }
11
+ ]
12
+ };
13
+ const CtaBanner = {
14
+ slug: 'ctaBanner',
15
+ fields: [
16
+ {
17
+ name: 'label',
18
+ type: 'text'
19
+ }
20
+ ]
21
+ };
22
+ const Pages = {
23
+ slug: 'pages',
24
+ versions: {
25
+ drafts: true
26
+ },
27
+ fields: [
28
+ {
29
+ name: 'layout',
30
+ type: 'blocks',
31
+ blocks: [
32
+ Heading,
33
+ CtaBanner
34
+ ]
35
+ }
36
+ ]
37
+ };
38
+ function buildReq() {
39
+ return {
40
+ payload: {
41
+ findByID: vi.fn(),
42
+ update: vi.fn(),
43
+ logger: {
44
+ info: vi.fn(),
45
+ warn: vi.fn(),
46
+ error: vi.fn()
47
+ }
48
+ },
49
+ context: {},
50
+ user: null
51
+ };
52
+ }
53
+ const allBlocks = [
54
+ Heading,
55
+ CtaBanner
56
+ ];
57
+ const catalog = introspectBlocks(allBlocks);
58
+ const nesting = buildBlockNestingMap([
59
+ Pages
60
+ ], [], allBlocks);
61
+ const drafts = new Set([
62
+ 'pages'
63
+ ]);
64
+ describe('patchLayout', ()=>{
65
+ it('flat layoutField writes {[layoutField]: finalLayout} — no regression from shared writePath', async ()=>{
66
+ // After extracting writePath into _layout-helpers, the collection tool
67
+ // doesn't itself use writePath, but this is the canonical regression
68
+ // check that the flat-path shape stays a single top-level key so
69
+ // Payload's `update` merges normally.
70
+ const tool = createPatchLayoutTool(catalog, nesting, drafts);
71
+ const req = buildReq();
72
+ req.payload.findByID.mockResolvedValue({
73
+ id: '1',
74
+ layout: [
75
+ {
76
+ blockType: 'heading',
77
+ text: 'a'
78
+ }
79
+ ]
80
+ });
81
+ req.payload.update.mockResolvedValue({
82
+ id: '1',
83
+ title: 'p'
84
+ });
85
+ await tool.handler({
86
+ collection: 'pages',
87
+ documentId: '1',
88
+ layoutField: 'layout',
89
+ blocks: [
90
+ {
91
+ blockType: 'ctaBanner',
92
+ label: 'Buy'
93
+ }
94
+ ],
95
+ operation: 'append'
96
+ }, req, {});
97
+ expect(req.payload.update).toHaveBeenCalledWith(expect.objectContaining({
98
+ collection: 'pages',
99
+ id: '1',
100
+ data: {
101
+ layout: [
102
+ {
103
+ blockType: 'heading',
104
+ text: 'a'
105
+ },
106
+ {
107
+ blockType: 'ctaBanner',
108
+ label: 'Buy'
109
+ }
110
+ ]
111
+ }
112
+ }));
113
+ });
114
+ it('full operation replaces the array wholesale', async ()=>{
115
+ const tool = createPatchLayoutTool(catalog, nesting, drafts);
116
+ const req = buildReq();
117
+ req.payload.findByID.mockResolvedValue({
118
+ id: '1',
119
+ layout: [
120
+ {
121
+ blockType: 'heading',
122
+ text: 'old'
123
+ }
124
+ ]
125
+ });
126
+ req.payload.update.mockResolvedValue({
127
+ id: '1'
128
+ });
129
+ await tool.handler({
130
+ collection: 'pages',
131
+ documentId: '1',
132
+ layoutField: 'layout',
133
+ blocks: [
134
+ {
135
+ blockType: 'heading',
136
+ text: 'new'
137
+ }
138
+ ],
139
+ operation: 'full'
140
+ }, req, {});
141
+ expect(req.payload.update).toHaveBeenCalledWith(expect.objectContaining({
142
+ data: {
143
+ layout: [
144
+ {
145
+ blockType: 'heading',
146
+ text: 'new'
147
+ }
148
+ ]
149
+ }
150
+ }));
151
+ });
152
+ it('rejects unknown blockType with validation error', async ()=>{
153
+ const tool = createPatchLayoutTool(catalog, nesting, drafts);
154
+ const req = buildReq();
155
+ const result = await tool.handler({
156
+ collection: 'pages',
157
+ documentId: '1',
158
+ layoutField: 'layout',
159
+ blocks: [
160
+ {
161
+ blockType: 'mystery'
162
+ }
163
+ ],
164
+ operation: 'full'
165
+ }, req, {});
166
+ expect(result.content[0].text).toMatch(/unknown blockType/i);
167
+ expect(req.payload.update).not.toHaveBeenCalled();
168
+ });
169
+ });
170
+
171
+ //# sourceMappingURL=patch-layout.test.js.map