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.
- package/README.md +232 -150
- package/dist/__tests__/api-keys.test.js +292 -0
- package/dist/__tests__/api-keys.test.js.map +1 -0
- package/dist/__tests__/auth-strategy.test.js +681 -0
- package/dist/__tests__/auth-strategy.test.js.map +1 -0
- package/dist/__tests__/conflict-detection.test.js +69 -0
- package/dist/__tests__/conflict-detection.test.js.map +1 -0
- package/dist/__tests__/delete-document.test.js +70 -0
- package/dist/__tests__/delete-document.test.js.map +1 -0
- package/dist/__tests__/endpoint.test.js +143 -0
- package/dist/__tests__/endpoint.test.js.map +1 -0
- package/dist/__tests__/find-document.test.js +178 -0
- package/dist/__tests__/find-document.test.js.map +1 -0
- package/dist/__tests__/find-global.test.js +173 -0
- package/dist/__tests__/find-global.test.js.map +1 -0
- package/dist/__tests__/global-versions.test.js +183 -0
- package/dist/__tests__/global-versions.test.js.map +1 -0
- package/dist/__tests__/hash.test.js +58 -0
- package/dist/__tests__/hash.test.js.map +1 -0
- package/dist/__tests__/index-integration.test.js +191 -0
- package/dist/__tests__/index-integration.test.js.map +1 -0
- package/dist/__tests__/introspection.test.js +201 -1
- package/dist/__tests__/introspection.test.js.map +1 -1
- package/dist/__tests__/patch-global-layout.test.js +474 -0
- package/dist/__tests__/patch-global-layout.test.js.map +1 -0
- package/dist/__tests__/patch-layout.test.js +171 -0
- package/dist/__tests__/patch-layout.test.js.map +1 -0
- package/dist/__tests__/registry.test.js +795 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/resources.test.js +139 -0
- package/dist/__tests__/resources.test.js.map +1 -0
- package/dist/__tests__/update-global.test.js +157 -0
- package/dist/__tests__/update-global.test.js.map +1 -0
- package/dist/api-keys.d.ts +46 -0
- package/dist/api-keys.js +272 -0
- package/dist/api-keys.js.map +1 -0
- package/dist/auth-strategy.d.ts +85 -0
- package/dist/auth-strategy.js +219 -0
- package/dist/auth-strategy.js.map +1 -0
- package/dist/components/CollectionScopesMatrix.d.ts +8 -0
- package/dist/components/CollectionScopesMatrix.js +32 -0
- package/dist/components/CollectionScopesMatrix.js.map +1 -0
- package/dist/components/GlobalScopesMatrix.d.ts +8 -0
- package/dist/components/GlobalScopesMatrix.js +28 -0
- package/dist/components/GlobalScopesMatrix.js.map +1 -0
- package/dist/components/ScopesTable.d.ts +19 -0
- package/dist/components/ScopesTable.js +285 -0
- package/dist/components/ScopesTable.js.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +4 -0
- package/dist/components/index.js.map +1 -0
- package/dist/conflict-detection.d.ts +13 -0
- package/dist/conflict-detection.js +41 -0
- package/dist/conflict-detection.js.map +1 -0
- package/dist/draft-workflow.d.ts +46 -47
- package/dist/draft-workflow.js +53 -130
- package/dist/draft-workflow.js.map +1 -1
- package/dist/endpoint.d.ts +35 -0
- package/dist/endpoint.js +105 -0
- package/dist/endpoint.js.map +1 -0
- package/dist/hash.d.ts +21 -0
- package/dist/hash.js +36 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +9 -9
- package/dist/index.js +168 -68
- package/dist/index.js.map +1 -1
- package/dist/introspection.d.ts +17 -3
- package/dist/introspection.js +95 -36
- package/dist/introspection.js.map +1 -1
- package/dist/prompts.js +5 -5
- package/dist/prompts.js.map +1 -1
- package/dist/registry.d.ts +50 -0
- package/dist/registry.js +169 -0
- package/dist/registry.js.map +1 -0
- package/dist/resources.d.ts +5 -3
- package/dist/resources.js +23 -11
- package/dist/resources.js.map +1 -1
- package/dist/scope/audit-log.d.ts +18 -0
- package/dist/scope/audit-log.js +50 -0
- package/dist/scope/audit-log.js.map +1 -0
- package/dist/scope/policy.d.ts +73 -0
- package/dist/scope/policy.js +218 -0
- package/dist/scope/policy.js.map +1 -0
- package/dist/tools/_helpers.d.ts +28 -1
- package/dist/tools/_helpers.js +83 -0
- package/dist/tools/_helpers.js.map +1 -1
- package/dist/tools/_layout-helpers.d.ts +43 -0
- package/dist/tools/_layout-helpers.js +159 -0
- package/dist/tools/_layout-helpers.js.map +1 -0
- package/dist/tools/create-document.d.ts +36 -0
- package/dist/tools/create-document.js +83 -0
- package/dist/tools/create-document.js.map +1 -0
- package/dist/tools/delete-document.d.ts +25 -0
- package/dist/tools/delete-document.js +49 -0
- package/dist/tools/delete-document.js.map +1 -0
- package/dist/tools/find-document.d.ts +33 -0
- package/dist/tools/find-document.js +97 -0
- package/dist/tools/find-document.js.map +1 -0
- package/dist/tools/find-global.d.ts +26 -0
- package/dist/tools/find-global.js +122 -0
- package/dist/tools/find-global.js.map +1 -0
- package/dist/tools/global-versions.d.ts +39 -0
- package/dist/tools/global-versions.js +132 -0
- package/dist/tools/global-versions.js.map +1 -0
- package/dist/tools/patch-global-layout.d.ts +31 -0
- package/dist/tools/patch-global-layout.js +127 -0
- package/dist/tools/patch-global-layout.js.map +1 -0
- package/dist/tools/patch-layout.d.ts +5 -8
- package/dist/tools/patch-layout.js +18 -100
- package/dist/tools/patch-layout.js.map +1 -1
- package/dist/tools/publish-draft.d.ts +5 -4
- package/dist/tools/publish-draft.js +6 -1
- package/dist/tools/publish-draft.js.map +1 -1
- package/dist/tools/publish-global-draft.d.ts +20 -0
- package/dist/tools/publish-global-draft.js +50 -0
- package/dist/tools/publish-global-draft.js.map +1 -0
- package/dist/tools/resolve-reference.d.ts +5 -4
- package/dist/tools/resolve-reference.js +4 -0
- package/dist/tools/resolve-reference.js.map +1 -1
- package/dist/tools/safe-delete.d.ts +5 -5
- package/dist/tools/safe-delete.js +20 -15
- package/dist/tools/safe-delete.js.map +1 -1
- package/dist/tools/schedule-publish.d.ts +5 -5
- package/dist/tools/schedule-publish.js +23 -19
- package/dist/tools/schedule-publish.js.map +1 -1
- package/dist/tools/search-content.d.ts +5 -9
- package/dist/tools/search-content.js +16 -12
- package/dist/tools/search-content.js.map +1 -1
- package/dist/tools/update-document.d.ts +5 -5
- package/dist/tools/update-document.js +10 -5
- package/dist/tools/update-document.js.map +1 -1
- package/dist/tools/update-global.d.ts +27 -0
- package/dist/tools/update-global.js +72 -0
- package/dist/tools/update-global.js.map +1 -0
- package/dist/tools/upload-media.d.ts +5 -4
- package/dist/tools/upload-media.js +6 -1
- package/dist/tools/upload-media.js.map +1 -1
- package/dist/tools/versions.d.ts +10 -9
- package/dist/tools/versions.js +15 -7
- package/dist/tools/versions.js.map +1 -1
- package/dist/types.d.ts +56 -3
- package/dist/types.js +13 -6
- package/dist/types.js.map +1 -1
- 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
|