@reactionary/ucp 0.0.1

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.
@@ -0,0 +1,715 @@
1
+ import { jsonResponse } from "./reactionary-ucp-http.js";
2
+ async function handleRestRequest(request, client, path, sessionId, sessionStore) {
3
+ if (request.method === "POST" && path === "/catalog/search") {
4
+ return jsonResponse(await handleCatalogSearch(client, await parseJsonBody(request)));
5
+ }
6
+ if (request.method === "POST" && path === "/catalog/lookup") {
7
+ return jsonResponse(await handleCatalogLookup(client, await parseJsonBody(request)));
8
+ }
9
+ if (request.method === "POST" && path === "/catalog/product") {
10
+ return jsonResponse(await handleCatalogProduct(client, await parseJsonBody(request)));
11
+ }
12
+ if (request.method === "POST" && path === "/carts") {
13
+ const body = await parseJsonBody(request);
14
+ return withRestIdempotency(
15
+ request,
16
+ sessionId,
17
+ "REST POST /carts",
18
+ body,
19
+ sessionStore,
20
+ async () => jsonResponse(await handleCreateCart(client, body), { status: 201 })
21
+ );
22
+ }
23
+ const cartMatch = /^\/carts\/([^/]+)$/.exec(path);
24
+ if (cartMatch && request.method === "GET") {
25
+ return jsonResponse(await handleGetCart(client, decodeURIComponent(cartMatch[1])));
26
+ }
27
+ if (cartMatch && request.method === "PUT") {
28
+ const body = await parseJsonBody(request);
29
+ return withRestIdempotency(
30
+ request,
31
+ sessionId,
32
+ `REST PUT /carts/${decodeURIComponent(cartMatch[1])}`,
33
+ body,
34
+ sessionStore,
35
+ async () => jsonResponse(await handleUpdateCart(client, decodeURIComponent(cartMatch[1]), body))
36
+ );
37
+ }
38
+ const cartCancelMatch = /^\/carts\/([^/]+)\/cancel$/.exec(path);
39
+ if (cartCancelMatch && request.method === "POST") {
40
+ return withRestIdempotency(
41
+ request,
42
+ sessionId,
43
+ `REST POST /carts/${decodeURIComponent(cartCancelMatch[1])}/cancel`,
44
+ {},
45
+ sessionStore,
46
+ async () => jsonResponse(await handleCancelCart(client, decodeURIComponent(cartCancelMatch[1])))
47
+ );
48
+ }
49
+ if (request.method === "POST" && path === "/checkout-sessions") {
50
+ const body = await parseJsonBody(request);
51
+ return withRestIdempotency(
52
+ request,
53
+ sessionId,
54
+ "REST POST /checkout-sessions",
55
+ body,
56
+ sessionStore,
57
+ async () => jsonResponse(await handleCreateCheckout(client, body), { status: 201 })
58
+ );
59
+ }
60
+ const checkoutMatch = /^\/checkout-sessions\/([^/]+)$/.exec(path);
61
+ if (checkoutMatch && request.method === "GET") {
62
+ return jsonResponse(await handleGetCheckout(client, decodeURIComponent(checkoutMatch[1])));
63
+ }
64
+ if (checkoutMatch && request.method === "PUT") {
65
+ const body = await parseJsonBody(request);
66
+ return withRestIdempotency(
67
+ request,
68
+ sessionId,
69
+ `REST PUT /checkout-sessions/${decodeURIComponent(checkoutMatch[1])}`,
70
+ body,
71
+ sessionStore,
72
+ async () => jsonResponse(await handleUpdateCheckout(client, decodeURIComponent(checkoutMatch[1]), body))
73
+ );
74
+ }
75
+ const checkoutCompleteMatch = /^\/checkout-sessions\/([^/]+)\/complete$/.exec(path);
76
+ if (checkoutCompleteMatch && request.method === "POST") {
77
+ return withRestIdempotency(
78
+ request,
79
+ sessionId,
80
+ `REST POST /checkout-sessions/${decodeURIComponent(checkoutCompleteMatch[1])}/complete`,
81
+ {},
82
+ sessionStore,
83
+ async () => jsonResponse(await handleCompleteCheckout(client, decodeURIComponent(checkoutCompleteMatch[1])))
84
+ );
85
+ }
86
+ const checkoutCancelMatch = /^\/checkout-sessions\/([^/]+)\/cancel$/.exec(path);
87
+ if (checkoutCancelMatch && request.method === "POST") {
88
+ return withRestIdempotency(
89
+ request,
90
+ sessionId,
91
+ `REST POST /checkout-sessions/${decodeURIComponent(checkoutCancelMatch[1])}/cancel`,
92
+ {},
93
+ sessionStore,
94
+ async () => jsonResponse(createUCPError("not_implemented", "Checkout cancellation is not represented by Reactionary checkout capabilities yet."), { status: 501 })
95
+ );
96
+ }
97
+ const orderMatch = /^\/orders\/([^/]+)$/.exec(path);
98
+ if (orderMatch && request.method === "GET") {
99
+ return jsonResponse(await handleGetOrder(client, decodeURIComponent(orderMatch[1])));
100
+ }
101
+ return void 0;
102
+ }
103
+ async function withRestIdempotency(request, sessionId, action, payload, sessionStore, createResponse) {
104
+ const idempotencyKey = request.headers.get("Idempotency-Key");
105
+ if (!idempotencyKey) {
106
+ return createResponse();
107
+ }
108
+ const fingerprint = JSON.stringify(payload);
109
+ const cached = await sessionStore.getIdempotencyRecord(sessionId, idempotencyKey);
110
+ if (cached) {
111
+ if (cached.action !== action || cached.fingerprint !== fingerprint) {
112
+ return jsonResponse(createUCPError("idempotency_key_conflict", "The supplied Idempotency-Key was already used for a different UCP REST mutation in this session."), { status: 409 });
113
+ }
114
+ return jsonResponse(cached.response, { status: cached.status ?? 200 });
115
+ }
116
+ const response = await createResponse();
117
+ const responseBody = await response.clone().json();
118
+ await sessionStore.putIdempotencyRecord(sessionId, idempotencyKey, {
119
+ action,
120
+ fingerprint,
121
+ status: response.status,
122
+ response: responseBody
123
+ });
124
+ return response;
125
+ }
126
+ async function parseJsonBody(request) {
127
+ if (!request.body) {
128
+ return {};
129
+ }
130
+ try {
131
+ return await request.json();
132
+ } catch (error) {
133
+ if (error instanceof SyntaxError) {
134
+ throw new UCPHttpError(400, createUCPError("invalid_json", "Request body must be valid JSON."));
135
+ }
136
+ throw error;
137
+ }
138
+ }
139
+ async function handleCatalogSearch(client, body) {
140
+ if (!client.productSearch) {
141
+ return createUCPError("not_available", "Product search capability is not available.");
142
+ }
143
+ const pageSize = getLimit(body);
144
+ const offset = getPaginationOffset(body);
145
+ if (offset === void 0) {
146
+ return createUCPError("invalid_request", "Catalog search pagination cursor must be a non-negative integer offset.");
147
+ }
148
+ const filterMapping = await getSearchFilterMapping(client, body.filters);
149
+ const result = await client.productSearch.queryByTerm({
150
+ search: {
151
+ term: body.query ?? "",
152
+ facets: [],
153
+ filters: filterMapping.filters,
154
+ paginationOptions: {
155
+ pageNumber: offsetToPageNumber(offset, pageSize),
156
+ pageSize
157
+ },
158
+ ...filterMapping.categoryFilter ? { categoryFilter: filterMapping.categoryFilter } : {}
159
+ }
160
+ });
161
+ if (!result.success) {
162
+ return createUCPError("search_failed", "Catalog search failed.");
163
+ }
164
+ return {
165
+ ucp: createUcpSuccessMetadata(),
166
+ products: result.value.items.map((product) => toUcpProduct(product)),
167
+ pagination: toUcpPagination(result.value),
168
+ ...filterMapping.messages.length > 0 ? { messages: filterMapping.messages } : {}
169
+ };
170
+ }
171
+ async function handleCatalogLookup(client, body) {
172
+ const products = await Promise.all(
173
+ body.ids.map((id) => getReactionaryProduct(client, id))
174
+ );
175
+ return {
176
+ ucp: createUcpSuccessMetadata(),
177
+ products: products.filter((product) => product !== void 0).map((product) => toUcpProduct(product))
178
+ };
179
+ }
180
+ async function handleCatalogProduct(client, body) {
181
+ const { id } = body;
182
+ if (!id) {
183
+ return createUCPError("invalid_request", "A product id, variant id, or sku is required.");
184
+ }
185
+ const product = await getReactionaryProduct(client, id);
186
+ if (!product) {
187
+ return createUCPError("not_found", `Product was not found: ${id}`);
188
+ }
189
+ return {
190
+ ucp: createUcpSuccessMetadata(),
191
+ product: toUcpProduct(product)
192
+ };
193
+ }
194
+ async function handleCreateCart(client, body) {
195
+ if (!client.cart) {
196
+ return createUCPError("not_available", "Cart capability is not available.");
197
+ }
198
+ const createResult = await client.cart.createCart({});
199
+ if (!createResult.success) {
200
+ return createUCPError("cart_create_failed", "Cart creation failed.");
201
+ }
202
+ let cart = createResult.value;
203
+ for (const lineItem of body["line_items"]) {
204
+ if (!lineItem.item.id) {
205
+ return createUCPError("cart_add_failed", "Unable to add cart line item without an item id.");
206
+ }
207
+ const addResult = await client.cart.add({
208
+ cart: cart.identifier,
209
+ variant: {
210
+ sku: lineItem.item.id
211
+ },
212
+ quantity: lineItem.quantity
213
+ });
214
+ if (!addResult.success) {
215
+ return createUCPError("cart_add_failed", `Unable to add item to cart: ${lineItem.item.id}`);
216
+ }
217
+ cart = addResult.value;
218
+ }
219
+ return toUcpCart(cart);
220
+ }
221
+ async function handleGetCart(client, cartId) {
222
+ if (!client.cart) {
223
+ return createUCPError("not_available", "Cart capability is not available.");
224
+ }
225
+ const result = await client.cart.getById({ cart: { key: cartId } });
226
+ if (!result.success) {
227
+ return createUCPError("not_found", `Cart was not found: ${cartId}`);
228
+ }
229
+ return toUcpCart(result.value);
230
+ }
231
+ async function handleUpdateCart(client, cartId, body) {
232
+ if (!client.cart) {
233
+ return createUCPError("not_available", "Cart capability is not available.");
234
+ }
235
+ const current = await client.cart.getById({ cart: { key: cartId } });
236
+ if (!current.success) {
237
+ return createUCPError("not_found", `Cart was not found: ${cartId}`);
238
+ }
239
+ const desiredItems = /* @__PURE__ */ new Map();
240
+ for (const lineItem of body.line_items) {
241
+ desiredItems.set(lineItem.item.id, lineItem);
242
+ }
243
+ let cart = current.value;
244
+ for (const existingItem of current.value.items) {
245
+ const sku = existingItem.variant.sku;
246
+ const desiredItem = desiredItems.get(sku);
247
+ if (!desiredItem) {
248
+ const removeResult = await client.cart.remove({
249
+ cart: current.value.identifier,
250
+ item: existingItem.identifier
251
+ });
252
+ if (!removeResult.success) {
253
+ return createUCPError("cart_update_failed", `Unable to remove item from cart: ${sku}`);
254
+ }
255
+ cart = removeResult.value;
256
+ continue;
257
+ }
258
+ if (existingItem.quantity !== desiredItem.quantity) {
259
+ const quantityResult = await client.cart.changeQuantity({
260
+ cart: current.value.identifier,
261
+ item: existingItem.identifier,
262
+ quantity: desiredItem.quantity
263
+ });
264
+ if (!quantityResult.success) {
265
+ return createUCPError("cart_update_failed", `Unable to change quantity for cart item: ${sku}`);
266
+ }
267
+ cart = quantityResult.value;
268
+ }
269
+ }
270
+ const existingSkus = new Set(current.value.items.map((item) => item.variant.sku));
271
+ for (const desiredItem of desiredItems.values()) {
272
+ if (existingSkus.has(desiredItem.item.id)) {
273
+ continue;
274
+ }
275
+ const addResult = await client.cart.add({
276
+ cart: current.value.identifier,
277
+ variant: {
278
+ sku: desiredItem.item.id
279
+ },
280
+ quantity: desiredItem.quantity
281
+ });
282
+ if (!addResult.success) {
283
+ return createUCPError("cart_update_failed", `Unable to add item to cart: ${desiredItem.item.id}`);
284
+ }
285
+ cart = addResult.value;
286
+ }
287
+ return toUcpCart(cart);
288
+ }
289
+ async function handleCancelCart(client, cartId) {
290
+ if (!client.cart) {
291
+ return createUCPError("not_available", "Cart capability is not available.");
292
+ }
293
+ const current = await client.cart.getById({ cart: { key: cartId } });
294
+ const deleted = await client.cart.deleteCart({ cart: { key: cartId } });
295
+ if (!deleted.success) {
296
+ return createUCPError("cart_cancel_failed", `Unable to cancel cart: ${cartId}`);
297
+ }
298
+ return current.success ? toUcpCart(current.value) : createEmptyUcpCart(cartId);
299
+ }
300
+ async function handleCreateCheckout(client, body) {
301
+ if (!client.checkout) {
302
+ return createUCPError("not_available", "Checkout capability is not available.");
303
+ }
304
+ const cart = body.cart_id ? await getReactionaryCart(client, body.cart_id) : await createCartForCheckout(client, body);
305
+ if (!cart) {
306
+ return createUCPError("invalid_request", "A checkout session requires cart_id or line_items.");
307
+ }
308
+ const result = await client.checkout.initiateCheckoutForCart({ cart });
309
+ if (!result.success) {
310
+ return createUCPError("checkout_create_failed", "Checkout session creation failed.");
311
+ }
312
+ return toUcpCheckout(result.value);
313
+ }
314
+ async function handleGetCheckout(client, checkoutId) {
315
+ if (!client.checkout) {
316
+ return createUCPError("not_available", "Checkout capability is not available.");
317
+ }
318
+ const result = await client.checkout.getById({ identifier: { key: checkoutId } });
319
+ if (!result.success) {
320
+ return createUCPError("not_found", `Checkout was not found: ${checkoutId}`);
321
+ }
322
+ return toUcpCheckout(result.value);
323
+ }
324
+ async function handleUpdateCheckout(client, checkoutId, body) {
325
+ if (!client.checkout) {
326
+ return createUCPError("not_available", "Checkout capability is not available.");
327
+ }
328
+ const current = await client.checkout.getById({ identifier: { key: checkoutId } });
329
+ if (!current.success) {
330
+ return createUCPError("not_found", `Checkout was not found: ${checkoutId}`);
331
+ }
332
+ let checkout = current.value;
333
+ const selectedInstrument = body.payment?.instruments?.find((instrument) => instrument.selected);
334
+ if (selectedInstrument) {
335
+ const paymentResult = await client.checkout.addPaymentInstruction({
336
+ checkout: checkout.identifier,
337
+ paymentInstruction: {
338
+ amount: checkout.price.grandTotal,
339
+ paymentMethod: {
340
+ method: selectedInstrument.type,
341
+ name: selectedInstrument.id,
342
+ paymentProcessor: selectedInstrument.handler_id
343
+ },
344
+ protocolData: [
345
+ { key: "ucp_payment_instrument_id", value: selectedInstrument.id },
346
+ { key: "ucp_payment_handler_id", value: selectedInstrument.handler_id },
347
+ { key: "ucp_payment_instrument_type", value: selectedInstrument.type }
348
+ ]
349
+ }
350
+ });
351
+ if (!paymentResult.success) {
352
+ return createUCPError("checkout_update_failed", "Unable to add selected payment instruction.");
353
+ }
354
+ checkout = paymentResult.value;
355
+ }
356
+ return toUcpCheckout(checkout);
357
+ }
358
+ async function handleCompleteCheckout(client, checkoutId) {
359
+ if (!client.checkout) {
360
+ return createUCPError("not_available", "Checkout capability is not available.");
361
+ }
362
+ const result = await client.checkout.finalizeCheckout({ checkout: { key: checkoutId } });
363
+ if (!result.success) {
364
+ return createUCPError("checkout_complete_failed", "Checkout completion failed.");
365
+ }
366
+ return toUcpCheckout(result.value, "completed");
367
+ }
368
+ async function handleGetOrder(client, orderId) {
369
+ if (!client.order) {
370
+ return createUCPError("not_available", "Order capability is not available.");
371
+ }
372
+ const result = await client.order.getById({ order: { key: orderId } });
373
+ if (!result.success) {
374
+ return createUCPError("not_found", `Order was not found: ${orderId}`);
375
+ }
376
+ return {
377
+ ucp: createUcpSuccessMetadata(),
378
+ id: result.value.identifier.key,
379
+ checkout_id: "",
380
+ permalink_url: "",
381
+ line_items: [],
382
+ currency: getMoneyCurrency(result.value.price?.grandTotal),
383
+ totals: toUcpTotals(result.value.price?.grandTotal),
384
+ fulfillment: {
385
+ expectations: [],
386
+ events: []
387
+ },
388
+ messages: []
389
+ };
390
+ }
391
+ async function getReactionaryCart(client, cartId) {
392
+ if (!client.cart) {
393
+ return void 0;
394
+ }
395
+ const result = await client.cart.getById({ cart: { key: cartId } });
396
+ return result.success ? result.value : void 0;
397
+ }
398
+ async function createCartForCheckout(client, checkout) {
399
+ if (!checkout.line_items?.length) {
400
+ return void 0;
401
+ }
402
+ const cartResponse = await handleCreateCart(client, {
403
+ id: "",
404
+ line_items: checkout.line_items,
405
+ currency: checkout.currency,
406
+ totals: checkout.totals,
407
+ ucp: createUcpSuccessMetadata()
408
+ });
409
+ return isUcpCart(cartResponse) ? getReactionaryCart(client, cartResponse.id) : void 0;
410
+ }
411
+ async function getReactionaryProduct(client, id) {
412
+ if (!client.product) {
413
+ return void 0;
414
+ }
415
+ const byId = await client.product.getById({ identifier: { key: id } });
416
+ if (byId.success) {
417
+ return byId.value;
418
+ }
419
+ const bySku = await client.product.getBySKU({ variant: { sku: id } });
420
+ return bySku.success ? bySku.value : void 0;
421
+ }
422
+ function toUcpProduct(product) {
423
+ const ucpVariants = getUcpProductVariants(product);
424
+ return {
425
+ id: product.identifier.key,
426
+ handle: product.slug,
427
+ title: product.name,
428
+ description: toUcpDescription(getProductDescription(product)),
429
+ url: product.slug ? `/${product.slug}` : void 0,
430
+ price_range: {
431
+ min: ucpVariants[0]?.price ?? createUcpPrice(),
432
+ max: ucpVariants[0]?.price ?? createUcpPrice()
433
+ },
434
+ variants: ucpVariants,
435
+ media: []
436
+ };
437
+ }
438
+ function getUcpProductVariants(product) {
439
+ if (isProduct(product)) {
440
+ return [
441
+ product.mainVariant,
442
+ ...product.variants
443
+ ].map((variant, index) => toUcpProductVariant(variant, product, index));
444
+ }
445
+ return product.variants.map((variant, index) => toUcpSearchVariant(variant, product, index));
446
+ }
447
+ function isProduct(product) {
448
+ return "mainVariant" in product;
449
+ }
450
+ function getProductDescription(product) {
451
+ return isProduct(product) ? product.description || product.longDescription : void 0;
452
+ }
453
+ function toUcpProductVariant(variant, product, index) {
454
+ const sku = variant.identifier.sku || `${product.identifier.key}-${index + 1}`;
455
+ return {
456
+ id: sku,
457
+ sku,
458
+ title: variant.name || product.name,
459
+ description: toUcpDescription(void 0),
460
+ price: createUcpPrice(),
461
+ availability: {
462
+ status: "available"
463
+ },
464
+ media: []
465
+ };
466
+ }
467
+ function toUcpSearchVariant(variant, product, index) {
468
+ const sku = variant.variant.sku || `${product.identifier.key}-${index + 1}`;
469
+ return {
470
+ id: sku,
471
+ sku,
472
+ title: product.name,
473
+ description: toUcpDescription(void 0),
474
+ price: createUcpPrice(),
475
+ availability: {
476
+ status: "available"
477
+ },
478
+ media: variant.image.sourceUrl ? [{
479
+ type: "image",
480
+ url: variant.image.sourceUrl,
481
+ alt_text: variant.image.altText
482
+ }] : []
483
+ };
484
+ }
485
+ function toUcpCart(cart) {
486
+ return {
487
+ id: cart.identifier.key,
488
+ line_items: cart.items.map(toUcpCartLineItem),
489
+ currency: getMoneyCurrency(cart.price?.grandTotal),
490
+ totals: toUcpTotals(cart.price?.grandTotal),
491
+ ucp: createUcpSuccessMetadata()
492
+ };
493
+ }
494
+ function createEmptyUcpCart(cartId) {
495
+ return {
496
+ id: cartId,
497
+ line_items: [],
498
+ currency: "EUR",
499
+ totals: [],
500
+ ucp: createUcpSuccessMetadata()
501
+ };
502
+ }
503
+ function isUcpCart(response) {
504
+ return "id" in response && "line_items" in response && "currency" in response;
505
+ }
506
+ function toUcpCartLineItem(lineItem) {
507
+ const sku = lineItem.variant.sku || lineItem.identifier.key;
508
+ return {
509
+ id: lineItem.identifier.key,
510
+ item: {
511
+ id: sku,
512
+ title: sku,
513
+ price: getMoneyValue(lineItem.price.unitPrice)
514
+ },
515
+ quantity: lineItem.quantity,
516
+ totals: toUcpTotals(lineItem.price.totalPrice)
517
+ };
518
+ }
519
+ function toUcpCheckout(checkout, status) {
520
+ return {
521
+ id: checkout.identifier.key,
522
+ status: status ?? (checkout.readyForFinalization ? "ready_for_complete" : "incomplete"),
523
+ line_items: checkout.items.map(toUcpCartLineItem),
524
+ currency: getMoneyCurrency(checkout.price?.grandTotal),
525
+ totals: toUcpTotals(checkout.price?.grandTotal),
526
+ links: [],
527
+ ucp: createUcpCheckoutSuccessMetadata()
528
+ };
529
+ }
530
+ function toUcpDescription(text) {
531
+ return text ? { plain: text } : { plain: "" };
532
+ }
533
+ function toUcpTotals(amount) {
534
+ return [
535
+ {
536
+ type: "total",
537
+ amount: getMoneyValue(amount)
538
+ }
539
+ ];
540
+ }
541
+ function getMoneyValue(amount) {
542
+ return amount.value;
543
+ }
544
+ function getMoneyCurrency(amount) {
545
+ return amount.currency;
546
+ }
547
+ function createUcpPrice(amount = 0, currency = "EUR") {
548
+ return {
549
+ amount,
550
+ currency
551
+ };
552
+ }
553
+ function createUcpSuccessMetadata() {
554
+ return {
555
+ version: "2026-08-25",
556
+ status: "success"
557
+ };
558
+ }
559
+ function createUcpCheckoutSuccessMetadata() {
560
+ return {
561
+ version: "2026-08-25",
562
+ status: "success",
563
+ payment_handlers: {}
564
+ };
565
+ }
566
+ function createUCPError(code, message) {
567
+ return {
568
+ ucp: {
569
+ version: "2026-08-25",
570
+ status: "error"
571
+ },
572
+ messages: [
573
+ {
574
+ message: {
575
+ type: "error",
576
+ content_type: "plain",
577
+ content: message,
578
+ code
579
+ }
580
+ }
581
+ ]
582
+ };
583
+ }
584
+ function createUcpWarning(code, content, path) {
585
+ return {
586
+ type: "warning",
587
+ code,
588
+ path,
589
+ content,
590
+ content_type: "plain",
591
+ presentation: "notice"
592
+ };
593
+ }
594
+ function getLimit(body) {
595
+ return body.pagination?.limit ?? 10;
596
+ }
597
+ async function getSearchFilterMapping(client, filters) {
598
+ if (!filters) {
599
+ return {
600
+ filters: [],
601
+ messages: []
602
+ };
603
+ }
604
+ const messages = [];
605
+ const categoryFilter = await getCategoryFilter(client, filters.categories, messages);
606
+ if (filters.price) {
607
+ messages.push(createUcpWarning(
608
+ "price_filter_ignored",
609
+ "Price filters are not supported by Reactionary product search yet and were ignored.",
610
+ "$.filters.price"
611
+ ));
612
+ }
613
+ return {
614
+ ...categoryFilter ? { categoryFilter } : {},
615
+ filters: getExtensionFilters(filters),
616
+ messages
617
+ };
618
+ }
619
+ async function getCategoryFilter(client, categories, messages) {
620
+ const [category, ...additionalCategories] = categories ?? [];
621
+ if (!category) {
622
+ return void 0;
623
+ }
624
+ if (additionalCategories.length > 0) {
625
+ messages.push(createUcpWarning(
626
+ "additional_categories_ignored",
627
+ "Reactionary product search currently supports one category filter; additional UCP category filters were ignored.",
628
+ "$.filters.categories"
629
+ ));
630
+ }
631
+ const productSearch = client.productSearch;
632
+ if (!productSearch?.createCategoryNavigationFilter) {
633
+ return {
634
+ facet: { key: "categories" },
635
+ key: category
636
+ };
637
+ }
638
+ const result = await productSearch.createCategoryNavigationFilter({
639
+ categoryPath: toReactionaryCategoryPath(category)
640
+ });
641
+ if (!result.success) {
642
+ messages.push(createUcpWarning(
643
+ "category_filter_ignored",
644
+ `Category filter could not be resolved and was ignored: ${category}`,
645
+ "$.filters.categories[0]"
646
+ ));
647
+ return void 0;
648
+ }
649
+ return result.value;
650
+ }
651
+ function toReactionaryCategoryPath(category) {
652
+ return category.split(">").map((part) => part.trim()).filter((part) => part.length > 0).map((part) => ({
653
+ identifier: { key: part },
654
+ name: part,
655
+ slug: "",
656
+ text: "",
657
+ images: []
658
+ }));
659
+ }
660
+ function getExtensionFilters(filters) {
661
+ if (!filters) {
662
+ return [];
663
+ }
664
+ return Object.entries(filters).filter(([key]) => key !== "categories" && key !== "price").flatMap(([key, value]) => toReactionaryFilterStrings(key, value));
665
+ }
666
+ function toReactionaryFilterStrings(key, value) {
667
+ if (value === void 0 || value === null) {
668
+ return [];
669
+ }
670
+ if (Array.isArray(value)) {
671
+ return value.flatMap((item) => toReactionaryFilterStrings(key, item));
672
+ }
673
+ return [`${key}:${formatFilterValue(value)}`];
674
+ }
675
+ function formatFilterValue(value) {
676
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
677
+ return String(value);
678
+ }
679
+ return JSON.stringify(value);
680
+ }
681
+ function getPaginationOffset(body) {
682
+ const cursor = body.pagination?.cursor;
683
+ if (cursor === void 0) {
684
+ return 0;
685
+ }
686
+ if (!/^(0|[1-9]\d*)$/.test(cursor)) {
687
+ return void 0;
688
+ }
689
+ return Number(cursor);
690
+ }
691
+ function offsetToPageNumber(offset, pageSize) {
692
+ return Math.floor(offset / pageSize) + 1;
693
+ }
694
+ function toUcpPagination(result) {
695
+ const nextOffset = result.pageNumber * result.pageSize;
696
+ const hasNextPage = nextOffset < result.totalCount;
697
+ return {
698
+ ...hasNextPage ? { cursor: String(nextOffset) } : {},
699
+ has_next_page: hasNextPage,
700
+ total_count: result.totalCount
701
+ };
702
+ }
703
+ class UCPHttpError extends Error {
704
+ constructor(status, body) {
705
+ super("UCP HTTP error");
706
+ this.status = status;
707
+ this.body = body;
708
+ }
709
+ status;
710
+ body;
711
+ }
712
+ export {
713
+ UCPHttpError,
714
+ handleRestRequest
715
+ };