@decocms/blocks-admin 7.34.4 → 7.36.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/package.json +2 -2
- package/src/admin/invoke.test.ts +32 -14
- package/src/admin/invoke.ts +35 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/blocks-admin",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.36.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco admin protocol: /live/_meta, /.decofile, /deco/invoke, /deco/render, plus admin-facing site setup and commerce-app auto-configuration",
|
|
6
6
|
"repository": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"lint:unused": "knip"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@decocms/blocks": "7.
|
|
28
|
+
"@decocms/blocks": "7.36.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"react": "^19.0.0",
|
package/src/admin/invoke.test.ts
CHANGED
|
@@ -58,7 +58,9 @@ describe("handleInvoke — Set-Cookie propagation (single)", () => {
|
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
const request = makeInvokeRequest("vtex/actions/addItemsToCart");
|
|
61
|
-
const response = await RequestContext.run(request, () =>
|
|
61
|
+
const response = await RequestContext.run(request, () =>
|
|
62
|
+
handleInvoke(request),
|
|
63
|
+
);
|
|
62
64
|
|
|
63
65
|
const cookies = response.headers.getSetCookie();
|
|
64
66
|
expect(cookies).toHaveLength(3);
|
|
@@ -77,7 +79,9 @@ describe("handleInvoke — Set-Cookie propagation (single)", () => {
|
|
|
77
79
|
});
|
|
78
80
|
|
|
79
81
|
const request = makeInvokeRequest("vtex/actions/foo");
|
|
80
|
-
const response = await RequestContext.run(request, () =>
|
|
82
|
+
const response = await RequestContext.run(request, () =>
|
|
83
|
+
handleInvoke(request),
|
|
84
|
+
);
|
|
81
85
|
|
|
82
86
|
// The regressed bug appended a single comma-joined string, so
|
|
83
87
|
// `getSetCookie()` returned a 1-element array. The fix appends each
|
|
@@ -94,7 +98,9 @@ describe("handleInvoke — Set-Cookie propagation (single)", () => {
|
|
|
94
98
|
});
|
|
95
99
|
|
|
96
100
|
const request = makeInvokeRequest("vtex/actions/withHeader");
|
|
97
|
-
const response = await RequestContext.run(request, () =>
|
|
101
|
+
const response = await RequestContext.run(request, () =>
|
|
102
|
+
handleInvoke(request),
|
|
103
|
+
);
|
|
98
104
|
expect(response.headers.get("x-vtex-trace-id")).toBe("abc-123");
|
|
99
105
|
});
|
|
100
106
|
|
|
@@ -104,7 +110,9 @@ describe("handleInvoke — Set-Cookie propagation (single)", () => {
|
|
|
104
110
|
});
|
|
105
111
|
|
|
106
112
|
const request = makeInvokeRequest("vtex/loaders/productList");
|
|
107
|
-
const response = await RequestContext.run(request, () =>
|
|
113
|
+
const response = await RequestContext.run(request, () =>
|
|
114
|
+
handleInvoke(request),
|
|
115
|
+
);
|
|
108
116
|
expect(response.headers.getSetCookie()).toEqual([]);
|
|
109
117
|
});
|
|
110
118
|
});
|
|
@@ -131,7 +139,9 @@ describe("handleInvoke — Set-Cookie propagation (batch)", () => {
|
|
|
131
139
|
"vtex/actions/addItemsToCart": { orderFormId: "x" },
|
|
132
140
|
"vtex/loaders/productList": {},
|
|
133
141
|
});
|
|
134
|
-
const response = await RequestContext.run(request, () =>
|
|
142
|
+
const response = await RequestContext.run(request, () =>
|
|
143
|
+
handleInvoke(request),
|
|
144
|
+
);
|
|
135
145
|
|
|
136
146
|
const cookies = response.headers.getSetCookie();
|
|
137
147
|
expect(cookies).toHaveLength(3);
|
|
@@ -155,7 +165,8 @@ describe("handleInvoke — draft binding", () => {
|
|
|
155
165
|
clearDraftCache();
|
|
156
166
|
setBlocks({ "site/x": { value: "published" } });
|
|
157
167
|
registerInvokeHandlers({
|
|
158
|
-
"site/loaders/x.ts": async () =>
|
|
168
|
+
"site/loaders/x.ts": async () =>
|
|
169
|
+
(loadBlocks() as Record<string, any>)["site/x"],
|
|
159
170
|
});
|
|
160
171
|
});
|
|
161
172
|
afterEach(() => {
|
|
@@ -166,11 +177,14 @@ describe("handleInvoke — draft binding", () => {
|
|
|
166
177
|
});
|
|
167
178
|
|
|
168
179
|
function invokeReq(headers: Record<string, string>): Request {
|
|
169
|
-
return new Request(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
180
|
+
return new Request(
|
|
181
|
+
"https://preview.example/deco/invoke/site/loaders/x.ts",
|
|
182
|
+
{
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: { "content-type": "application/json", ...headers },
|
|
185
|
+
body: "{}",
|
|
186
|
+
},
|
|
187
|
+
);
|
|
174
188
|
}
|
|
175
189
|
|
|
176
190
|
it("resolves loaders against the DRAFT when the request carries an allowed draft cookie", async () => {
|
|
@@ -183,7 +197,8 @@ describe("handleInvoke — draft binding", () => {
|
|
|
183
197
|
const res = await handleInvoke(
|
|
184
198
|
invokeReq({
|
|
185
199
|
"x-forwarded-host": "preview.example",
|
|
186
|
-
cookie:
|
|
200
|
+
cookie:
|
|
201
|
+
"__deco_draft=studio.decocms.com/api/o/decofile/m/main%3Ftoken%3Dt@vX",
|
|
187
202
|
}),
|
|
188
203
|
);
|
|
189
204
|
expect(await res.json()).toEqual({ value: "draft" });
|
|
@@ -197,7 +212,9 @@ describe("handleInvoke — draft binding", () => {
|
|
|
197
212
|
return new Response("{}");
|
|
198
213
|
}) as unknown as typeof fetch;
|
|
199
214
|
|
|
200
|
-
const res = await handleInvoke(
|
|
215
|
+
const res = await handleInvoke(
|
|
216
|
+
invokeReq({ "x-forwarded-host": "preview.example" }),
|
|
217
|
+
);
|
|
201
218
|
expect(await res.json()).toEqual({ value: "published" });
|
|
202
219
|
expect(called).toBe(false);
|
|
203
220
|
});
|
|
@@ -212,7 +229,8 @@ describe("handleInvoke — draft binding", () => {
|
|
|
212
229
|
const res = await handleInvoke(
|
|
213
230
|
invokeReq({
|
|
214
231
|
"x-forwarded-host": "prod.example",
|
|
215
|
-
cookie:
|
|
232
|
+
cookie:
|
|
233
|
+
"__deco_draft=studio.decocms.com/api/o/decofile/m/main%3Ftoken%3Dt@vX",
|
|
216
234
|
}),
|
|
217
235
|
);
|
|
218
236
|
expect(await res.json()).toEqual({ value: "published" });
|
package/src/admin/invoke.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* copies those headers into the final HTTP Response.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { resolveDraftForRequest,
|
|
16
|
+
import { resolveDraftForRequest, withDraftBlocks } from "@decocms/blocks/cms";
|
|
17
17
|
import { RequestContext } from "@decocms/blocks/sdk/requestContext";
|
|
18
18
|
|
|
19
19
|
export type InvokeLoader = (props: any, request: Request) => Promise<any>;
|
|
@@ -95,11 +95,13 @@ function forwardCtxHeadersTo(response: Response): void {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
const isDev =
|
|
98
|
-
typeof globalThis.process !== "undefined" &&
|
|
98
|
+
typeof globalThis.process !== "undefined" &&
|
|
99
|
+
globalThis.process.env?.NODE_ENV === "development";
|
|
99
100
|
|
|
100
101
|
function selectFields(data: unknown, select?: string[]): unknown {
|
|
101
102
|
if (!select?.length || !data || typeof data !== "object") return data;
|
|
102
|
-
if (Array.isArray(data))
|
|
103
|
+
if (Array.isArray(data))
|
|
104
|
+
return data.map((item) => selectFields(item, select));
|
|
103
105
|
const result: Record<string, unknown> = {};
|
|
104
106
|
for (const key of select) {
|
|
105
107
|
if (key in (data as Record<string, unknown>)) {
|
|
@@ -131,7 +133,9 @@ async function parseBody(request: Request): Promise<any> {
|
|
|
131
133
|
for (const [key, value] of formData.entries()) {
|
|
132
134
|
if (obj[key] !== undefined) {
|
|
133
135
|
// Multiple values → array
|
|
134
|
-
const existing = Array.isArray(obj[key])
|
|
136
|
+
const existing = Array.isArray(obj[key])
|
|
137
|
+
? (obj[key] as unknown[])
|
|
138
|
+
: [obj[key]];
|
|
135
139
|
existing.push(value);
|
|
136
140
|
obj[key] = existing;
|
|
137
141
|
} else {
|
|
@@ -193,10 +197,9 @@ function unknownHandlerMessage(key: string): string {
|
|
|
193
197
|
const kind = isAction ? "action" : isLoader ? "loader" : "loader/action";
|
|
194
198
|
const dir = isAction ? "src/actions/" : "src/loaders/";
|
|
195
199
|
const cliHint = "npm run generate:loaders";
|
|
196
|
-
const setupHint =
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
: "setInvokeLoaders(() => COMMERCE_LOADERS)";
|
|
200
|
+
const setupHint = isAction
|
|
201
|
+
? "setInvokeActions(() => COMMERCE_ACTIONS)"
|
|
202
|
+
: "setInvokeLoaders(() => COMMERCE_LOADERS)";
|
|
200
203
|
return (
|
|
201
204
|
`Unknown handler: ${key}. ` +
|
|
202
205
|
`Check that the ${kind} exists in ${dir} (file path must match the key — ` +
|
|
@@ -221,16 +224,17 @@ function warnMissingHandlerOnce(key: string): void {
|
|
|
221
224
|
* refs; without this, `/deco/invoke` — a separate request from the page render,
|
|
222
225
|
* so the page's request-scoped draft binding never reaches it — would resolve
|
|
223
226
|
* against PUBLISHED blocks and hand a reviewer stale content in any
|
|
224
|
-
* client-fetched (lazy/on-scroll) or self-fetched section. `
|
|
227
|
+
* client-fetched (lazy/on-scroll) or self-fetched section. `withDraftBlocks`
|
|
225
228
|
* uses AsyncLocalStorage, so each concurrent invoke gets its own override with
|
|
226
229
|
* no cross-request bleed (the same isolation the layout-cache race taught us to
|
|
227
|
-
* keep)
|
|
228
|
-
*
|
|
230
|
+
* keep) — and composes the draft as a SNAPSHOT, the same semantics the page
|
|
231
|
+
* render applies, so a block deleted in the draft is deleted here too. Inert
|
|
232
|
+
* unless the draft gate passes — no draft, no wrap, published path untouched.
|
|
229
233
|
*/
|
|
230
234
|
export async function handleInvoke(request: Request): Promise<Response> {
|
|
231
235
|
const draftBlocks = await resolveDraftForRequest(request);
|
|
232
236
|
return draftBlocks
|
|
233
|
-
?
|
|
237
|
+
? withDraftBlocks(draftBlocks, () => dispatchInvoke(request))
|
|
234
238
|
: dispatchInvoke(request);
|
|
235
239
|
}
|
|
236
240
|
|
|
@@ -258,7 +262,10 @@ async function dispatchInvoke(request: Request): Promise<Response> {
|
|
|
258
262
|
return result;
|
|
259
263
|
}
|
|
260
264
|
const filtered = selectFields(result, select);
|
|
261
|
-
const response = new Response(JSON.stringify(filtered), {
|
|
265
|
+
const response = new Response(JSON.stringify(filtered), {
|
|
266
|
+
status: 200,
|
|
267
|
+
headers: JSON_HEADERS,
|
|
268
|
+
});
|
|
262
269
|
forwardCtxHeadersTo(response);
|
|
263
270
|
return response;
|
|
264
271
|
} catch (error) {
|
|
@@ -267,7 +274,12 @@ async function dispatchInvoke(request: Request): Promise<Response> {
|
|
|
267
274
|
}
|
|
268
275
|
|
|
269
276
|
// Batch invoke
|
|
270
|
-
if (
|
|
277
|
+
if (
|
|
278
|
+
request.method === "POST" &&
|
|
279
|
+
body &&
|
|
280
|
+
typeof body === "object" &&
|
|
281
|
+
!Array.isArray(body)
|
|
282
|
+
) {
|
|
271
283
|
const results: Record<string, unknown> = {};
|
|
272
284
|
|
|
273
285
|
const entries = Object.entries(body as Record<string, unknown>);
|
|
@@ -286,7 +298,11 @@ async function dispatchInvoke(request: Request): Promise<Response> {
|
|
|
286
298
|
// RequestContext.responseHeaders (which forwardCtxHeadersTo()
|
|
287
299
|
// below propagates onto the batch response).
|
|
288
300
|
if (result instanceof Response) {
|
|
289
|
-
try {
|
|
301
|
+
try {
|
|
302
|
+
result = await result.json();
|
|
303
|
+
} catch {
|
|
304
|
+
result = null;
|
|
305
|
+
}
|
|
290
306
|
}
|
|
291
307
|
results[key] = selectFields(result, select);
|
|
292
308
|
} catch (error) {
|
|
@@ -299,7 +315,10 @@ async function dispatchInvoke(request: Request): Promise<Response> {
|
|
|
299
315
|
}),
|
|
300
316
|
);
|
|
301
317
|
|
|
302
|
-
const response = new Response(JSON.stringify(results), {
|
|
318
|
+
const response = new Response(JSON.stringify(results), {
|
|
319
|
+
status: 200,
|
|
320
|
+
headers: JSON_HEADERS,
|
|
321
|
+
});
|
|
303
322
|
// All batch handlers share the same RequestContext, so any Set-Cookie
|
|
304
323
|
// they appended (e.g. from VTEX vtexFetchWithCookies) is in
|
|
305
324
|
// `responseHeaders` by now. Forward it as N distinct Set-Cookie headers.
|