@adobe-commerce/aio-toolkit 1.2.2 → 1.2.4

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/CHANGELOG.md CHANGED
@@ -5,6 +5,142 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.4] - 2026-04-20
9
+
10
+ ### ✨ Features
11
+
12
+ - **feat(repository): Add pagination and sort options to `AbdbRepository.find`**
13
+
14
+ `find` now accepts an optional second parameter `options: AbdbFindOptions` for
15
+ server-side pagination and sorting, applied via the MongoDB cursor chain:
16
+ `.sort(...)` → `.skip(...)` → `.limit(...)`.
17
+
18
+ ```typescript
19
+ // Page 2, 20 per page, newest first
20
+ await repo.find(
21
+ { active: true },
22
+ { current_page: 2, page_size: 20, sort: { column: '_created_at', direction: 'desc' } }
23
+ );
24
+ ```
25
+
26
+ **`AbdbFindOptions` fields (all optional):**
27
+ | Field | Type | Description |
28
+ |---|---|---|
29
+ | `sort` | `AbdbFindSort` | `{ column: string; direction?: 'asc' \| 'desc' }` — direction defaults to `'asc'` |
30
+ | `page_size` | `number` | Maximum documents to return; enables `.limit()` |
31
+ | `current_page` | `number` | 1-based page index; combined with `page_size` to compute `.skip()`. Page 1 skips the `.skip()` call. |
32
+
33
+ Calling `find` without options is fully backward compatible — no sort, skip, or limit is applied.
34
+
35
+ **New exported types:** `AbdbFindOptions`, `AbdbFindSort`, `AbdbFindSortDirection`
36
+
37
+ - **feat(integration): Refactor `RabbitMQClient` from push-based to pull-based consumption**
38
+
39
+ `RabbitMQClient.consume` now uses `channel.get` (AMQP `basic.get`) instead of
40
+ `channel.consume` (push subscription). This makes batch termination fully deterministic —
41
+ `channel.get` returns `false` immediately when the queue is empty, so there is no consumer
42
+ tag to cancel, no cancellation race condition, and no activation-timeout hang.
43
+
44
+ **What changed:**
45
+ - `processBatch` rewritten: windowed `Promise.all(maxParallel)` replaces `channel.prefetch` +
46
+ `channel.consume`. Each window pulls up to `maxParallel` messages sequentially via
47
+ `channel.get`, then processes them concurrently before moving to the next window.
48
+ - `publish` backpressure improved: `sendToQueue` returning `false` (write buffer full) now
49
+ counts the message as published and waits for the channel's `drain` event before
50
+ continuing. Previously, a full buffer was counted as a failure.
51
+ - `finally` blocks in `consume` and `publish` now wrap `channel.close()` in a try/catch so
52
+ a close error never prevents the connection from closing.
53
+
54
+ **No API changes** — `consume(queueName, options, handler)` and `publish(queueName, payloads)`
55
+ signatures, `RabbitMQConsumeOptions`, `ConsumeStats`, and `PublishStats` are unchanged.
56
+
57
+ ### 🐛 Bug Fixes
58
+
59
+ - **fix(webhook-action): Rename `WebhookActionExceptionResponse.class` → `type`**
60
+
61
+ Adobe Commerce webhooks expect `"type"` in the exception response payload,
62
+ not `"class"`. The previous field name was silently sending the wrong key,
63
+ causing an **Internal Server Error** instead of the intended exception
64
+ (e.g. `GraphQlInputException`).
65
+
66
+ **Before (broken):**
67
+ ```typescript
68
+ WebhookActionResponse.exception(
69
+ 'Lease term not available for this cart',
70
+ 'Magento\\Framework\\GraphQl\\Exception\\GraphQlInputException'
71
+ );
72
+ // sent: { "op": "exception", "class": "...", "message": "..." } ❌
73
+ ```
74
+
75
+ **After (correct):**
76
+ ```typescript
77
+ WebhookActionResponse.exception(
78
+ 'Lease term not available for this cart',
79
+ 'Magento\\Framework\\GraphQl\\Exception\\GraphQlInputException'
80
+ );
81
+ // sent: { "op": "exception", "type": "...", "message": "..." } ✅
82
+ ```
83
+
84
+ **Migration:** Replace any direct use of the `class` field on
85
+ `WebhookActionExceptionResponse` with `type`.
86
+
87
+ ---
88
+
89
+ ## [1.2.3] - 2026-04-01
90
+
91
+ ### ✨ Features
92
+
93
+ - **feat(integration): Add `RabbitMQClient` — push-based AMQP consumer and publisher**
94
+
95
+ New `RabbitMQClient` class in `src/integration/rabbit-mq-client` provides a
96
+ clean, typed interface for consuming and publishing messages via AMQP (RabbitMQ).
97
+
98
+ **`consume(queueName, options, handler)`**
99
+ - Checks queue depth via `channel.checkQueue` — skips immediately when empty
100
+ - Sets `channel.prefetch(maxParallel)` for broker-side concurrency control
101
+ - Processes `effectiveBatch = min(batchSize, messageCount)` messages
102
+ - Acks on successful handler execution; nacks on failure (requeue configurable)
103
+ - Optional exchange assertion and queue binding via `options.exchange`
104
+ - Returns `ConsumeStats` — consumed, acked, nacked counts plus per-failure details
105
+
106
+ **`publish(queueName, payloads)`**
107
+ - Asserts queue and sends each payload via `sendToQueue` with `persistent: true`
108
+ - Records write-buffer-full (`sendToQueue` returns `false`) and thrown errors
109
+ - Returns `PublishStats` — published and failed counts plus per-failure details
110
+
111
+ Both methods open a dedicated connection per call and always close channel and
112
+ connection in a `finally` block.
113
+
114
+ **Exported types** (from `rabbit-mq-client/types`):
115
+ `RabbitMQCredentials`, `RabbitMQConsumeOptions`, `MessageHandler`,
116
+ `ConsumeStats`, `PublishStats`
117
+
118
+ **Note:** `batchSize` and `maxParallel` are required in `RabbitMQConsumeOptions`
119
+ — the caller must choose appropriate limits for their workload.
120
+
121
+ ### 🐛 Bug Fixes
122
+
123
+ - **fix(abdb): Preserve `error.cause` on `AbdbCollection` thrown errors**
124
+
125
+ `run()` now assigns `.cause` to each re-thrown `Error` so callers can access
126
+ the original error via `err.cause`. Previously the root cause was silently
127
+ discarded. Assignment is done via `(err as any).cause` for compatibility with
128
+ the ES2020 TypeScript target (the ES2022 `Error(msg, { cause })` constructor
129
+ overload is not available in ES2020 lib typings).
130
+
131
+ - **fix(abdb): Guard `DbError instanceof` check against undefined export**
132
+
133
+ The `DbError` branch now uses `DbError && error instanceof DbError` so the
134
+ code does not throw a `TypeError` in environments where `@adobe/aio-lib-db`
135
+ does not export `DbError` at runtime. Unrecognised errors fall through to the
136
+ `unexpected error` path as expected.
137
+
138
+ - **fix(abdb): Correct file header comment in `adobe-aio-lib-db.d.ts`**
139
+
140
+ The comment incorrectly read `@adobe/aio-lib-ims`; updated to `@adobe/aio-lib-db`.
141
+
142
+ ---
143
+
8
144
  ## [1.2.2] - 2026-04-01
9
145
 
10
146
  ### 🐛 Bug Fixes
package/README.md CHANGED
@@ -151,11 +151,21 @@ const testWebhook = WebhookAction.execute(
151
151
 
152
152
  **WebhookActionResponse Operations:**
153
153
  - `success()`: Indicates successful webhook processing
154
- - `exception(message?, exceptionClass?)`: Returns error response
154
+ - `exception(message?, exceptionType?)`: Returns error response with an optional Magento exception type (e.g. `'Magento\\Framework\\GraphQl\\Exception\\GraphQlInputException'`)
155
155
  - `add(path, value, instance?)`: Adds data to response
156
156
  - `replace(path, value, instance?)`: Replaces data in response
157
157
  - `remove(path)`: Removes data from response
158
158
 
159
+ **Exception response example:**
160
+ ```typescript
161
+ // Return a typed Magento exception — Commerce will surface it as a proper error
162
+ return WebhookActionResponse.exception(
163
+ 'Lease term not available for this cart',
164
+ 'Magento\\Framework\\GraphQl\\Exception\\GraphQlInputException'
165
+ );
166
+ // Produces: { "op": "exception", "type": "Magento\\...", "message": "..." }
167
+ ```
168
+
159
169
  #### `PublishEvent`
160
170
  Event publishing component for Adobe I/O Events with CloudEvents support.
161
171
 
@@ -868,9 +878,10 @@ exports.main = async (params) => {
868
878
  // Read one by any field
869
879
  const byEmail = await repo.findOne({ email: 'jane@example.com' });
870
880
 
871
- // Read all (optional filter)
881
+ // Read all (optional filter + optional pagination/sort)
872
882
  const all = await repo.find();
873
883
  const active = await repo.find({ active: true });
884
+ const paged = await repo.find({ active: true }, { current_page: 2, page_size: 20, sort: { column: '_created_at', direction: 'desc' } });
874
885
 
875
886
  // Partial update — only provided fields are validated; required fields already in the DB are not re-checked
876
887
  await repo.save({ first_name: 'Janet' }, id);
@@ -903,7 +914,7 @@ These are added once per collection instance, so re-using the same collection ac
903
914
  | `updateOne(payload, filter?, options?)` | Single-document update via `{ $set: payload }` (partial validation, stamps `_updated_at`) | `Promise<Record<string, any>>` — raw `updateOne` result (e.g. `modifiedCount`) |
904
915
  | `findOne(filter)` | First document matching filter, e.g. `{ email: 'a@b.com' }` | `Promise<T \| null>` |
905
916
  | `findById(id)` | Shorthand for `findOne({ _id: new ObjectId(id) })` | `Promise<T \| null>` |
906
- | `find(filter?)` | All documents matching filter (default: all) | `Promise<T[]>` |
917
+ | `find(filter?, options?)` | All documents matching filter with optional pagination and sort (`AbdbFindOptions`) | `Promise<T[]>` |
907
918
  | `isIdExists(id)` | Returns `true` if a document with the given `_id` exists (no-op for empty id) | `Promise<boolean>` |
908
919
  | `exists(filter?, options?)` | Returns `true` if any document matching `filter` exists via `countDocuments` | `Promise<boolean>` |
909
920
  | `deleteOne(filter?)` | Delete first matching document via `deleteOne` | `Promise<Record<string, any>>` — raw `deleteOne` result |
@@ -925,6 +936,36 @@ These are added once per collection instance, so re-using the same collection ac
925
936
  | `getName()` | Returns the underlying collection name |
926
937
  | `getCollection()` | Returns the underlying `AbdbCollection` instance |
927
938
 
939
+ ##### Pagination and sorting
940
+
941
+ `find` accepts an optional second argument for server-side pagination and sorting:
942
+
943
+ ```javascript
944
+ // All active users, newest first
945
+ const all = await repo.find(
946
+ { active: true },
947
+ { sort: { column: '_created_at', direction: 'desc' } }
948
+ );
949
+
950
+ // Page 2, 20 results per page, sorted ascending by name
951
+ const page2 = await repo.find(
952
+ {},
953
+ { current_page: 2, page_size: 20, sort: { column: 'name', direction: 'asc' } }
954
+ );
955
+
956
+ // Just limit — no pagination offset
957
+ const top10 = await repo.find({}, { page_size: 10 });
958
+ ```
959
+
960
+ `AbdbFindOptions` fields (all optional):
961
+
962
+ | Field | Type | Default | Description |
963
+ |---|---|---|---|
964
+ | `sort.column` | `string` | — | Field to sort by |
965
+ | `sort.direction` | `'asc' \| 'desc'` | `'asc'` | Sort direction |
966
+ | `page_size` | `number` | — | Max documents to return (enables `.limit()`) |
967
+ | `current_page` | `number` | — | 1-based page index; computes `.skip((page-1) * page_size)` |
968
+
928
969
  ##### Custom region
929
970
 
930
971
  ```javascript
@@ -1508,6 +1549,107 @@ if (directInfo.isValid) {
1508
1549
  - `extract(headersOrParams)` - Extracts Bearer token from headers or OpenWhisk params
1509
1550
  - `info(token)` - Analyzes token string and returns validation/expiry details
1510
1551
 
1552
+ #### `RabbitMQClient`
1553
+ Pull-based AMQP consumer and publisher for RabbitMQ. Manages the full connection lifecycle per call (connect → channel → close) and exposes a clean, typed interface without exposing raw AMQP primitives to the caller.
1554
+
1555
+ **Installation**
1556
+ ```bash
1557
+ npm install amqplib
1558
+ ```
1559
+
1560
+ **Basic Usage**
1561
+ ```typescript
1562
+ const { RabbitMQClient } = require('@adobe-commerce/aio-toolkit');
1563
+
1564
+ const client = new RabbitMQClient({
1565
+ host: 'rabbitmq.example.com',
1566
+ port: '5672',
1567
+ username: 'user',
1568
+ password: 'secret',
1569
+ vhost: '/',
1570
+ secure: false, // set true to use amqps://
1571
+ });
1572
+ ```
1573
+
1574
+ **Consuming messages**
1575
+ ```typescript
1576
+ const stats = await client.consume(
1577
+ 'orders-queue',
1578
+ { batchSize: 100, maxParallel: 10 },
1579
+ async (queueName, content) => {
1580
+ const order = JSON.parse(content);
1581
+ await processOrder(order);
1582
+ }
1583
+ );
1584
+
1585
+ console.log(`consumed=${stats.consumed} acked=${stats.acked} nacked=${stats.nacked}`);
1586
+ if (stats.errors.length) {
1587
+ console.error('Failed messages:', stats.errors);
1588
+ }
1589
+ ```
1590
+
1591
+ **Consuming with exchange binding**
1592
+ ```typescript
1593
+ const stats = await client.consume(
1594
+ 'orders-queue',
1595
+ {
1596
+ batchSize: 50,
1597
+ maxParallel: 5,
1598
+ exchange: 'orders-exchange', // asserts exchange + binds queue before consuming
1599
+ nackRequeue: false, // dead-letter failed messages instead of requeueing
1600
+ },
1601
+ async (queueName, content) => {
1602
+ await processOrder(JSON.parse(content));
1603
+ }
1604
+ );
1605
+ ```
1606
+
1607
+ **Publishing messages**
1608
+ ```typescript
1609
+ const payloads = orders.map(o => JSON.stringify(o));
1610
+ const stats = await client.publish('orders-queue', payloads);
1611
+
1612
+ console.log(`published=${stats.published} failed=${stats.failed}`);
1613
+ if (stats.errors.length) {
1614
+ console.error('Failed payloads:', stats.errors);
1615
+ }
1616
+ ```
1617
+
1618
+ **API Reference**
1619
+
1620
+ | Method | Description | Returns |
1621
+ |---|---|---|
1622
+ | `consume(queueName, options, handler)` | Checks queue depth and pull-consumes up to `batchSize` messages via `channel.get` in parallel windows of `maxParallel` | `Promise<ConsumeStats>` |
1623
+ | `publish(queueName, payloads)` | Asserts queue and enqueues each payload as a persistent message | `Promise<PublishStats>` |
1624
+
1625
+ **`RabbitMQConsumeOptions`**
1626
+
1627
+ | Field | Type | Required | Description |
1628
+ |---|---|---|---|
1629
+ | `batchSize` | `number` | ✅ | Maximum number of messages to process in this call |
1630
+ | `maxParallel` | `number` | ✅ | Max messages processed concurrently per window (client-side) |
1631
+ | `nackRequeue` | `boolean` | — | Requeue on nack (default: `true`) |
1632
+ | `exchange` | `string` | — | If set, asserts a `direct` exchange and binds the queue before consuming |
1633
+
1634
+ **`ConsumeStats`**
1635
+
1636
+ | Field | Type | Description |
1637
+ |---|---|---|
1638
+ | `consumed` | `number` | Total messages received from the broker |
1639
+ | `acked` | `number` | Messages successfully processed and acked |
1640
+ | `nacked` | `number` | Messages that failed handler and were nacked |
1641
+ | `errors` | `Array<{ content: string; error: unknown }>` | Per-failure details |
1642
+
1643
+ **`PublishStats`**
1644
+
1645
+ | Field | Type | Description |
1646
+ |---|---|---|
1647
+ | `published` | `number` | Messages successfully enqueued |
1648
+ | `failed` | `number` | Messages that could not be enqueued |
1649
+ | `errors` | `Array<{ payload: string; error: unknown }>` | Per-failure details (thrown errors only; write-buffer-full is handled via backpressure) |
1650
+
1651
+ ---
1652
+
1511
1653
  #### `InfiniteLoopBreaker`
1512
1654
  Detect and prevent infinite loops in event-driven applications.
1513
1655