@adobe-commerce/aio-toolkit 1.2.5 → 1.2.7

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 (31) hide show
  1. package/CHANGELOG.md +250 -0
  2. package/README.md +450 -1
  3. package/dist/aio-toolkit-cli-workflow/bin/cli.js +2048 -0
  4. package/dist/aio-toolkit-cli-workflow/bin/cli.js.map +1 -0
  5. package/dist/aio-toolkit-cursor-context/bin/cli.js +16 -0
  6. package/dist/aio-toolkit-cursor-context/bin/cli.js.map +1 -1
  7. package/dist/index.d.mts +61 -6
  8. package/dist/index.d.ts +61 -6
  9. package/dist/index.js +600 -42
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +610 -38
  12. package/dist/index.mjs.map +1 -1
  13. package/files/cursor-context/commands/aio-toolkit-analyze-adobe-commerce-module.md +612 -0
  14. package/files/cursor-context/commands/aio-toolkit-create-amazon-sqs-consumer.md +445 -0
  15. package/files/cursor-context/commands/aio-toolkit-create-event-consumer-action.md +6 -0
  16. package/files/cursor-context/commands/aio-toolkit-create-graphql-action.md +21 -7
  17. package/files/cursor-context/commands/aio-toolkit-create-openwhisk-action.md +326 -0
  18. package/files/cursor-context/commands/aio-toolkit-create-runtime-action.md +15 -5
  19. package/files/cursor-context/commands/aio-toolkit-create-shipping-carrier.md +681 -0
  20. package/files/cursor-context/commands/aio-toolkit-create-webhook-action.md +22 -9
  21. package/files/cursor-context/rules/aio-toolkit-create-adobe-commerce-client.mdc +252 -116
  22. package/files/cursor-context/rules/aio-toolkit-oop-best-practices.mdc +10 -4
  23. package/files/cursor-context/rules/aio-toolkit-setup-new-relic-telemetry.mdc +167 -2
  24. package/files/cursor-context/rules/aio-toolkit-use-abdb-collection.mdc +610 -0
  25. package/files/cursor-context/rules/aio-toolkit-use-abdb-repository.mdc +705 -0
  26. package/files/cursor-context/rules/aio-toolkit-use-adobe-auth.mdc +442 -0
  27. package/files/cursor-context/rules/aio-toolkit-use-amazon-sqs-publish.mdc +397 -0
  28. package/files/cursor-context/rules/aio-toolkit-use-file-repository.mdc +502 -0
  29. package/files/cursor-context/rules/aio-toolkit-use-publish-event.mdc +510 -0
  30. package/files/cursor-context/rules/aio-toolkit-use-runtime-api-gateway-service.mdc +542 -0
  31. package/package.json +4 -2
@@ -0,0 +1,705 @@
1
+ ---
2
+ description: Adding AbdbRepository CRUD data access to Adobe I/O Runtime actions using @adobe-commerce/aio-toolkit
3
+ globs: '**/{actions,lib}/**/*.{ts,js}'
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Using AbdbRepository
8
+
9
+ ## Trigger Conditions
10
+
11
+ This rule applies when the user requests full CRUD database operations using App Builder Data (ABDB) with any of these phrases:
12
+
13
+ - "Use AbdbRepository"
14
+ - "Insert records to ABDB"
15
+ - "Find / list records from the database"
16
+ - "Update a document in App Builder Data"
17
+ - "Delete from the database"
18
+ - "CRUD operations on ABDB"
19
+ - "Paginate database results"
20
+ - "Standard database CRUD in App Builder"
21
+ - "Save to App Builder Data"
22
+ - "Count documents in ABDB"
23
+ - "Check if a record exists"
24
+
25
+ **Important**: `AbdbRepository` always requires an `AbdbCollection` class to exist first. If the collection class for the target entity has not been created yet, use the "Using AbdbCollection" rule first before this one.
26
+
27
+ **Recommended approach**: Use `AbdbRepository` for all standard database operations. Extend it with custom methods using `getCollection().run()` for any queries not covered by the built-in methods — rather than calling `collection.run()` directly in action files.
28
+
29
+ **Integration with Other Action Creation Rules:**
30
+
31
+ When using action creation rules (RuntimeAction, WebhookAction, EventConsumerAction, GraphQlAction, OpenwhiskAction) and the developer mentions CRUD database operations:
32
+
33
+ - **Automatically trigger this rule** after the action is created and the collection class is confirmed
34
+ - Examples: "save records to the database", "find all active records", "update status", "delete a document"
35
+ - This rule handles creating the repository class, integrating it into the action, and all CRUD operation templates
36
+
37
+ ---
38
+
39
+ ## Step 1: Verify Prerequisites
40
+
41
+ Before proceeding, verify:
42
+
43
+ 1. **Toolkit installed**: Check `@adobe-commerce/aio-toolkit` in `package.json` dependencies
44
+ - If NOT installed: `npm install @adobe-commerce/aio-toolkit`
45
+
46
+ 2. **Peer dependency — aio-lib-db**: Check `@adobe/aio-lib-db` in `package.json` dependencies
47
+ - **Required**: `AbdbRepository` uses this internally
48
+ - If NOT installed: `npm install @adobe/aio-lib-db`
49
+ - Minimum version: `>= 1.0.0`
50
+
51
+ 3. **AIO SDK installed**: Check `@adobe/aio-sdk` in `package.json` dependencies
52
+ - Required for `Core.AuthClient.generateAccessToken(params)` to obtain an IMS access token
53
+ - If NOT installed: `npm install @adobe/aio-sdk`
54
+
55
+ 4. **Detect Language (TypeScript vs JavaScript)**:
56
+
57
+ **Check for TypeScript indicators (check ALL of these)**:
58
+ 1. **Package Dependencies**: Look for `typescript` in `package.json` dependencies or devDependencies
59
+ 2. **Config File**: Check if `tsconfig.json` exists in project root
60
+ 3. **TypeScript Loaders**: Look for `ts-loader`, `ts-node`, or `@types/*` packages in `package.json`
61
+ 4. **Existing Files**: Check for `.ts` or `.tsx` files in key directories (`actions/`, `src/`, `lib/`)
62
+
63
+ **Detection Logic**:
64
+ - **If ANY of the following are true → TypeScript project**:
65
+ - `typescript` package installed AND `tsconfig.json` exists
66
+ - Multiple TypeScript indicators (2 or more from above list)
67
+ - Existing `.ts` files found in `actions/` or `lib/` directories
68
+ - **Otherwise → JavaScript project**
69
+
70
+ 5. **Check for Existing Collection Class** — REQUIRED:
71
+ - Look for `lib/database/collection/` directory
72
+ - **If a collection class exists for the target entity**: proceed to Step 2
73
+ - **If NO matching collection class exists**: STOP — inform the user:
74
+ > "AbdbRepository requires an AbdbCollection class first. Let me help you create it using the 'Using AbdbCollection' rule before setting up the repository."
75
+ - List discovered collection classes for the user to choose from
76
+
77
+ 6. **Check for Existing Repository Classes**: Look for `lib/database/repository/` directory
78
+ - List any existing repository classes
79
+ - Inform the user if a repository for the same entity already exists
80
+
81
+ 7. **Detect Project Structure**:
82
+ - Check `app.config.yaml` for `application:` and `extensions:` sections
83
+ - List all existing actions and their file paths
84
+
85
+ 8. Only proceed to Step 2 after confirming all prerequisites
86
+
87
+ ---
88
+
89
+ ## Step 2: Ask Required Questions
90
+
91
+ Before generating any code, ask the user the following questions:
92
+
93
+ **Important**: Do not make assumptions — keep asking until all details are clear.
94
+
95
+ 1. **Entity Name / Collection**: Which entity will this repository operate on?
96
+ - List discovered collection classes for the user to choose from
97
+ - The repository will wrap the matching collection (e.g. `OrdersCollection` → `OrdersRepository`)
98
+
99
+ 2. **Data Region**: Which region should the repository connect to?
100
+ - Options: `amer` (default), `emea`, `apac`
101
+ - **Default**: `amer` — suggest unless the user knows otherwise
102
+
103
+ 3. **Target Action(s)**: Which existing action(s) will use this repository?
104
+ - List all discovered actions for the user to choose from
105
+
106
+ 4. **CRUD Operations**: Which operations does the action need?
107
+ - **Insert** (`save()` without ID or `insertOne()`) — insert a new document
108
+ - **Update** (`save(payload, id)` or `updateOne()`) — partial update by ID or filter
109
+ - **Find all / filtered** (`find()`) — retrieve documents with optional filter, sort, pagination
110
+ - **Find by ID** (`findById()`) — retrieve a single document by `_id` string
111
+ - **Delete by ID** (`deleteById()`) — delete a single document by `_id` string
112
+ - **Delete by filter** (`deleteOne()` or `delete()`) — delete by matching criteria
113
+ - **Existence check** (`isIdExists()`, `exists()`) — check presence without fetching content
114
+ - **Count** (`count()`) — count matching documents
115
+ - **Bulk insert** (`insert([])`) — insert multiple documents in one call
116
+ - **Custom query** (`getCollection().run()` inside repository subclass) — anything not covered above
117
+
118
+ 5. **Pagination** (if `find()` is selected):
119
+ - Is pagination needed? (`page_size` and `current_page`)
120
+ - Default sort column and direction? (e.g. `_created_at` descending)
121
+
122
+ 6. **TypeScript: Define Record Interface?** (TypeScript projects only)
123
+ - Should a typed `AbdbRecord` interface be created for this entity?
124
+ - Example: `interface OrderRecord extends AbdbRecord { status: string; total: number; }`
125
+
126
+ 7. **Custom Methods**: Are there any custom queries beyond standard CRUD?
127
+ - If yes: what is the query? (e.g. "find all records with status = X, sorted by date, limit 50")
128
+ - These will be added as methods on the repository subclass using `getCollection().run()`
129
+
130
+ ---
131
+
132
+ ## Step 3: Confirm Implementation Details
133
+
134
+ After receiving answers to ALL questions, present a comprehensive summary and **wait for user confirmation** before proceeding to Step 4:
135
+
136
+ ---
137
+
138
+ ### 📋 AbdbRepository Integration Summary
139
+
140
+ **Entity:** `[EntityName]`
141
+ **Repository Class:** `[EntityName]Repository`
142
+ **Wraps Collection:** `[EntityName]Collection` (from `lib/database/collection/[entity-name]`)
143
+ **Language:** [TypeScript/JavaScript] (auto-detected)
144
+ **Region:** `[amer/emea/apac]`
145
+
146
+ **Target Action(s):**
147
+ - `[action-name]` → `[path/to/action/index.js]`
148
+
149
+ **Operations to Integrate:**
150
+
151
+ | Operation | Method | Details |
152
+ |---|---|---|
153
+ | Insert | `save(payload)` / `insertOne(payload)` | Full validation — all required fields |
154
+ | Update | `save(payload, id)` / `updateOne(payload, filter)` | Partial validation — only provided fields |
155
+ | Find all / filtered | `find(filter, options)` | Sort: `[column]` `[asc/desc]`; Page size: `[n]` |
156
+ | Find by ID | `findById(id)` | Auto-wraps `ObjectId` |
157
+ | Delete by ID | `deleteById(id)` | Auto-wraps `ObjectId` |
158
+ | Exists / Count | `isIdExists(id)` / `exists(filter)` / `count(filter)` | No content fetch |
159
+ | Custom query | `getCollection().run()` inside subclass | `[describe query]` |
160
+
161
+ [TypeScript] **Record Interface**: `[EntityName]Record extends AbdbRecord`
162
+
163
+ ---
164
+
165
+ ### ✅ What Will Be Changed
166
+
167
+ **1. New Repository Class**
168
+ - ➕ `lib/database/repository/[entity-name]/index.[js/ts]`
169
+ - Extends `AbdbRepository` wrapping `[EntityName]Collection`
170
+ - Token and region passed in constructor
171
+ - Optional: custom methods using `getCollection().run()`
172
+
173
+ **2. Action Implementation**
174
+ - ✏️ `[action-path]/index.[js/ts]`
175
+ - Import `[EntityName]Repository` from `lib/database/repository/[entity-name]`
176
+ - Import `Core` from `@adobe/aio-sdk`
177
+ - Generate IMS access token via `Core.AuthClient.generateAccessToken(params)`
178
+ - Construct `new [EntityName]Repository(accessToken, '[region]')`
179
+ - Add selected CRUD operations with null/empty result handling
180
+
181
+ **3. Action Configuration**
182
+ - ✏️ `app.config.yaml` / `ext.config.yaml`
183
+ - Add `include-ims-credentials: true` annotation (if not already present)
184
+ - Confirm database auto-provision block is present
185
+
186
+ **4. No Additional `.env` Variables Required**
187
+ - `AbdbRepository` uses standard IMS credentials injected via `include-ims-credentials: true`
188
+
189
+ ---
190
+
191
+ **Should I proceed with this implementation?**
192
+
193
+ ---
194
+
195
+ ## Step 4: Generate Implementation
196
+
197
+ ### Step 4.1: Create the Repository Class
198
+
199
+ Create a dedicated repository class under `lib/database/repository/`. Always extend `AbdbRepository` — pass the matching collection instance and token in the constructor.
200
+
201
+ #### Simple Repository — TypeScript
202
+
203
+ ```typescript
204
+ // lib/database/repository/[entity-name]/index.ts
205
+
206
+ import { AbdbRepository } from '@adobe-commerce/aio-toolkit';
207
+ import { [EntityName]Collection } from '@lib/database/collection/[entity-name]';
208
+
209
+ export class [EntityName]Repository extends AbdbRepository {
210
+ constructor(token: string, region = 'amer') {
211
+ super(new [EntityName]Collection(), token, region);
212
+ }
213
+ }
214
+ ```
215
+
216
+ #### Repository with Custom Method — TypeScript
217
+
218
+ ```typescript
219
+ // lib/database/repository/[entity-name]/index.ts
220
+
221
+ import { AbdbRepository } from '@adobe-commerce/aio-toolkit';
222
+ import { [EntityName]Collection } from '@lib/database/collection/[entity-name]';
223
+
224
+ export class [EntityName]Repository extends AbdbRepository {
225
+ constructor(token: string, region = 'amer') {
226
+ super(new [EntityName]Collection(), token, region);
227
+ }
228
+
229
+ // Custom query — use getCollection().run() for operations not covered by AbdbRepository
230
+ async findByStatus(status: string): Promise<[EntityName]Record[]> {
231
+ return this.getCollection().run(async (col) => {
232
+ return await col
233
+ .find({ status })
234
+ .sort({ _created_at: 1 })
235
+ .toArray();
236
+ }, this['_token']);
237
+ }
238
+ }
239
+ ```
240
+
241
+ #### TypeScript Record Interface (optional but recommended)
242
+
243
+ ```typescript
244
+ import type { AbdbRecord } from '@adobe-commerce/aio-toolkit';
245
+
246
+ export interface [EntityName]Record extends AbdbRecord {
247
+ // your domain fields — match the collection columns
248
+ [column_1]: string;
249
+ [column_2]: number;
250
+ [column_3]?: boolean;
251
+ }
252
+
253
+ export class [EntityName]Repository extends AbdbRepository<[EntityName]Record> {
254
+ constructor(token: string, region = 'amer') {
255
+ super(new [EntityName]Collection(), token, region);
256
+ }
257
+ }
258
+ ```
259
+
260
+ #### JavaScript
261
+
262
+ ```javascript
263
+ // lib/database/repository/[entity-name]/index.js
264
+
265
+ const { AbdbRepository } = require('@adobe-commerce/aio-toolkit');
266
+ const { [EntityName]Collection } = require('../collection/[entity-name]');
267
+
268
+ class [EntityName]Repository extends AbdbRepository {
269
+ constructor(token, region = 'amer') {
270
+ super(new [EntityName]Collection(), token, region);
271
+ }
272
+ }
273
+
274
+ module.exports = { [EntityName]Repository };
275
+ ```
276
+
277
+ ---
278
+
279
+ ### Step 4.2: Integrate into the Target Action
280
+
281
+ Generate an IMS token, construct the repository, then call the required CRUD methods.
282
+
283
+ #### Insert a New Document — JavaScript
284
+
285
+ ```javascript
286
+ const { RuntimeAction, RuntimeActionResponse, HttpMethod } = require('@adobe-commerce/aio-toolkit');
287
+ const { Core } = require('@adobe/aio-sdk');
288
+ const { [EntityName]Repository } = require('../../lib/database/repository/[entity-name]');
289
+
290
+ const name = '[action-name]';
291
+
292
+ exports.main = RuntimeAction.execute(
293
+ name,
294
+ [HttpMethod.POST],
295
+ ['[required_param_1]', '[required_param_2]'],
296
+ ['Authorization'],
297
+ async (params, ctx) => {
298
+ const { logger } = ctx;
299
+
300
+ const tokenResponse = await Core.AuthClient.generateAccessToken(params);
301
+ const accessToken = tokenResponse && tokenResponse.access_token;
302
+
303
+ const repo = new [EntityName]Repository(accessToken);
304
+
305
+ const result = await repo.save({
306
+ [column_1]: params.[column_1],
307
+ [column_2]: params.[column_2],
308
+ });
309
+
310
+ logger.info({ message: `${name}-created`, id: result.insertedId });
311
+
312
+ return RuntimeActionResponse.success({ id: result.insertedId });
313
+ }
314
+ );
315
+ ```
316
+
317
+ #### Find All / Filtered with Pagination — JavaScript
318
+
319
+ ```javascript
320
+ const messages = await repo.find(
321
+ { status: 'active' }, // filter — pass {} for all documents
322
+ {
323
+ sort: { column: '_created_at', direction: 'desc' },
324
+ current_page: params.page || 1, // 1-based; requires page_size
325
+ page_size: params.limit || 20,
326
+ }
327
+ );
328
+ ```
329
+
330
+ #### Find by ID — JavaScript
331
+
332
+ ```javascript
333
+ const { ObjectId } = require('bson'); // only needed for custom _id filters
334
+
335
+ // findById() wraps ObjectId internally — pass plain string ID
336
+ const record = await repo.findById(params.id);
337
+ if (!record) {
338
+ return RuntimeActionResponse.error(HttpStatus.NOT_FOUND, `[EntityName] '${params.id}' not found`);
339
+ }
340
+ return RuntimeActionResponse.success(record);
341
+ ```
342
+
343
+ #### Partial Update — JavaScript
344
+
345
+ ```javascript
346
+ // save(payload, id) = upsert update — only validated fields in payload are written
347
+ const result = await repo.save(
348
+ { status: params.status }, // partial payload — partial validation only
349
+ params.id // existing document _id as plain string
350
+ );
351
+
352
+ logger.info({ message: `${name}-updated`, id: params.id, modifiedCount: result.modifiedCount });
353
+ return RuntimeActionResponse.success({ id: params.id, modifiedCount: result.modifiedCount });
354
+ ```
355
+
356
+ #### Delete by ID — JavaScript
357
+
358
+ ```javascript
359
+ // deleteById() wraps ObjectId internally — pass plain string ID
360
+ const result = await repo.deleteById(params.id);
361
+ logger.info({ message: `${name}-deleted`, deletedCount: result.deletedCount });
362
+ return RuntimeActionResponse.success({ deletedCount: result.deletedCount });
363
+ ```
364
+
365
+ #### Existence Check and Count — JavaScript
366
+
367
+ ```javascript
368
+ // Check if specific ID exists — returns true/false, no content fetch
369
+ const exists = await repo.isIdExists(params.id);
370
+
371
+ // Check if any matching document exists
372
+ const hasActive = await repo.exists({ status: 'active' });
373
+
374
+ // Count matching documents
375
+ const activeCount = await repo.count({ status: 'active' });
376
+ const total = await repo.count(); // all documents
377
+ ```
378
+
379
+ #### Bulk Insert — JavaScript
380
+
381
+ ```javascript
382
+ const payloads = params.items.map(item => ({
383
+ [column_1]: item.[column_1],
384
+ [column_2]: item.[column_2],
385
+ }));
386
+
387
+ const result = await repo.insert(payloads);
388
+ logger.info({ message: `${name}-bulk-inserted`, count: Object.keys(result.insertedIds).length });
389
+ return RuntimeActionResponse.success({ insertedIds: result.insertedIds });
390
+ ```
391
+
392
+ **TypeScript:** Same patterns with `import` syntax:
393
+
394
+ ```typescript
395
+ import { HttpMethod, RuntimeAction, RuntimeActionResponse, HttpStatus } from '@adobe-commerce/aio-toolkit';
396
+ import { Core } from '@adobe/aio-sdk';
397
+ import { [EntityName]Repository } from '../../lib/database/repository/[entity-name]';
398
+ ```
399
+
400
+ ---
401
+
402
+ ### Step 4.3: Update Action Configuration
403
+
404
+ ```yaml
405
+ [action-name]:
406
+ function: [action-path]/index.[js/ts]
407
+ web: 'yes'
408
+ runtime: nodejs:22
409
+ annotations:
410
+ require-adobe-auth: true
411
+ final: true
412
+ include-ims-credentials: true # REQUIRED: injects IMS credentials for token generation
413
+ inputs:
414
+ LOG_LEVEL: debug
415
+ ```
416
+
417
+ > **Why `include-ims-credentials: true`?** `AbdbRepository` requires an IMS access token from `Core.AuthClient.generateAccessToken(params)`. Without this annotation, token generation fails and the repository constructor will throw.
418
+
419
+ Confirm the database auto-provision block is present in `app.config.yaml` (add once per project if not already there):
420
+
421
+ ```yaml
422
+ application:
423
+ runtimeManifest:
424
+ database:
425
+ auto-provision: true
426
+ region: [amer/emea/apac]
427
+ ```
428
+
429
+ ---
430
+
431
+ ## Step 5: Recommendations
432
+
433
+ ### A. `save()` Semantics — Insert vs Update
434
+
435
+ `save()` behaves differently depending on whether an `id` is passed:
436
+
437
+ | Call | Operation | Validation | Returns |
438
+ |---|---|---|---|
439
+ | `save(payload)` — no ID | `insertOne` | **Full** — all required fields | `{ insertedId: '...' }` |
440
+ | `save(payload, id)` — with ID | `updateOne` with `upsert: true` | **Partial** — only provided fields | `{ modifiedCount: 1 }` |
441
+
442
+ Use `save()` as the unified insert/update method. Use `insertOne()` or `updateOne()` when you need more explicit control.
443
+
444
+ ### B. `ObjectId` for `_id` Filters
445
+
446
+ The convenience methods automatically handle `ObjectId` wrapping internally:
447
+
448
+ | Method | `ObjectId` handling |
449
+ |---|---|
450
+ | `findById(id)` | Wrapped internally — pass plain string |
451
+ | `deleteById(id)` | Wrapped internally — pass plain string |
452
+ | `isIdExists(id)` | Wrapped internally — pass plain string |
453
+ | `findOne({ _id: ... })` | **Manual** — wrap with `new ObjectId(id)` from `bson` |
454
+ | `updateOne(payload, { _id: ... })` | **Manual** — wrap with `new ObjectId(id)` from `bson` |
455
+ | `deleteOne({ _id: ... })` | **Manual** — wrap with `new ObjectId(id)` from `bson` |
456
+
457
+ ```javascript
458
+ const { ObjectId } = require('bson'); // bson is already installed via @adobe/aio-lib-db
459
+
460
+ // When filtering by _id manually:
461
+ await repo.findOne({ _id: new ObjectId(params.id) });
462
+ await repo.updateOne({ status: 'done' }, { _id: new ObjectId(params.id) });
463
+ await repo.deleteOne({ _id: new ObjectId(params.id) });
464
+ ```
465
+
466
+ > `bson` is a transitive dependency of `@adobe/aio-lib-db` — no separate install needed.
467
+
468
+ ### C. Automatic Timestamps — Do Not Declare Them in the Schema
469
+
470
+ `AbdbRepository` automatically manages `_created_at` and `_updated_at` on every write:
471
+
472
+ | Timestamp | Set on | Value |
473
+ |---|---|---|
474
+ | `_created_at` | First insert only | ISO 8601 UTC string |
475
+ | `_updated_at` | Every insert and update | ISO 8601 UTC string |
476
+
477
+ These are registered on the collection by the repository constructor. **Do not add `_created_at` or `_updated_at` columns to your `AbdbCollection` subclass** — the repository handles this, and duplicate column registration will throw.
478
+
479
+ ### D. `find()` Pagination Options
480
+
481
+ `find()` supports server-side sorting and pagination via `AbdbFindOptions`:
482
+
483
+ ```javascript
484
+ // Sort only — no pagination
485
+ await repo.find({ status: 'active' }, {
486
+ sort: { column: '_created_at', direction: 'desc' }
487
+ });
488
+
489
+ // Limit only
490
+ await repo.find({}, { page_size: 10 });
491
+
492
+ // Full pagination — page 2, 20 per page
493
+ await repo.find({}, { current_page: 2, page_size: 20, sort: { column: 'name' } });
494
+ ```
495
+
496
+ - `direction` defaults to `'asc'` when omitted
497
+ - `current_page` is 1-based and requires `page_size` to have effect
498
+ - Omit both for all documents
499
+
500
+ ### E. Validation Behaviour by Operation
501
+
502
+ | Operation | Type | What is checked |
503
+ |---|---|---|
504
+ | `save()` (insert) | **Full** | All required columns must be present |
505
+ | `save(payload, id)` (update) | **Partial** | Only fields in payload |
506
+ | `insertOne()` | **Full** | All required columns |
507
+ | `insert([])` | **Full** | Each document independently |
508
+ | `updateOne()` | **Partial** | Only fields in payload |
509
+ | `update()` | **Partial** | Only fields in payload |
510
+
511
+ Partial validation intentionally skips required columns absent from the payload — they already exist in the stored document.
512
+
513
+ ### F. `delete({})` Danger
514
+
515
+ Calling `delete()` with no filter (or `{}`) deletes **all documents** in the collection. Always pass a meaningful filter:
516
+
517
+ ```javascript
518
+ // DANGER — deletes everything in the collection
519
+ await repo.delete();
520
+ await repo.delete({});
521
+
522
+ // Safe — targeted deletion
523
+ await repo.delete({ status: 'expired' });
524
+ await repo.deleteById(params.id);
525
+ ```
526
+
527
+ ### G. Custom Queries — Extend the Repository, Not the Action
528
+
529
+ When you need a query not covered by `AbdbRepository`'s built-in methods, add a method to the repository subclass using `getCollection().run()`:
530
+
531
+ ```typescript
532
+ // In the repository class — NOT in the action file
533
+ async findByQueueAndStatus(queueName: string, status: string) {
534
+ return this.getCollection().run(async (col) => {
535
+ return await col
536
+ .find({ queue_name: queueName, status })
537
+ .sort({ _created_at: 1 })
538
+ .toArray();
539
+ }, this['_token']);
540
+ }
541
+ ```
542
+
543
+ Then call it like any other repository method in the action:
544
+
545
+ ```typescript
546
+ const results = await repo.findByQueueAndStatus(params.queue_name, params.status);
547
+ ```
548
+
549
+ This keeps all database access in the repository layer and makes the action code clean and testable.
550
+
551
+ ---
552
+
553
+ ## Key Components from @adobe-commerce/aio-toolkit
554
+
555
+ ### AbdbRepository Constructor
556
+
557
+ ```typescript
558
+ new AbdbRepository<T extends AbdbRecord>(
559
+ collection: AbdbCollection, // the collection schema instance
560
+ token: string, // IMS access token — throws if blank
561
+ region?: string // default: 'amer' — options: 'amer', 'emea', 'apac'
562
+ )
563
+ ```
564
+
565
+ Automatically registers `_created_at` and `_updated_at` columns on the collection at construction time.
566
+
567
+ ### Single-Document Methods
568
+
569
+ ```typescript
570
+ save(payload: Partial<T>, id?: string): Promise<Record<string, any>>
571
+ // No id: insertOne (full validation). With id: updateOne upsert (partial validation)
572
+
573
+ insertOne(payload: Partial<T>): Promise<Record<string, any>>
574
+ // Full validation. Returns { acknowledged, insertedId }
575
+
576
+ updateOne(payload: Partial<T>, filter?: Record<string, unknown>, options?: Record<string, unknown>): Promise<Record<string, any>>
577
+ // Partial validation. Uses { $set: payload }. Returns { acknowledged, modifiedCount }
578
+
579
+ findOne(filter: Record<string, unknown>): Promise<T | null>
580
+ // Returns first match or null. Wrap _id with new ObjectId(id) for ID filters
581
+
582
+ findById(id: string): Promise<T | null>
583
+ // Shorthand for findOne({ _id: new ObjectId(id) }). Returns null if not found
584
+
585
+ deleteOne(filter?: Record<string, unknown>): Promise<Record<string, any>>
586
+ // Deletes first match. Wrap _id with new ObjectId(id) for ID filters
587
+
588
+ deleteById(id: string): Promise<Record<string, any>>
589
+ // Shorthand for deleteOne({ _id: new ObjectId(id) })
590
+
591
+ isIdExists(id: string): Promise<boolean>
592
+ // Returns true if _id exists. Uses countDocuments — no content fetched
593
+
594
+ exists(filter?: Record<string, unknown>, options?: Record<string, unknown>): Promise<boolean>
595
+ // Returns true if any matching document exists
596
+
597
+ count(filter?: Record<string, unknown>, options?: Record<string, unknown>): Promise<number>
598
+ // Counts matching documents. No filter = count all
599
+ ```
600
+
601
+ ### Bulk Methods
602
+
603
+ ```typescript
604
+ insert(payloads: Partial<T>[]): Promise<Record<string, any>>
605
+ // Bulk insertMany. Full validation per document. Returns { acknowledged, insertedIds }
606
+
607
+ update(payload: Partial<T>, filter?: Record<string, unknown>, options?: Record<string, unknown>): Promise<Record<string, any>>
608
+ // updateMany. Partial validation. Returns { acknowledged, modifiedCount }
609
+
610
+ delete(filter?: Record<string, unknown>): Promise<Record<string, any>>
611
+ // deleteMany. No filter = delete ALL. Returns { acknowledged, deletedCount }
612
+ ```
613
+
614
+ ### Query with Pagination
615
+
616
+ ```typescript
617
+ find(filter?: Record<string, unknown>, options?: AbdbFindOptions): Promise<T[]>
618
+
619
+ interface AbdbFindOptions {
620
+ sort?: { column: string; direction?: 'asc' | 'desc' }; // direction default: 'asc'
621
+ page_size?: number; // max documents to return
622
+ current_page?: number; // 1-based; requires page_size
623
+ }
624
+ ```
625
+
626
+ ### Accessor Methods
627
+
628
+ ```typescript
629
+ getName(): string // returns the collection name
630
+ getCollection(): AbdbCollection // returns the AbdbCollection instance (for getCollection().run())
631
+ ```
632
+
633
+ ### AbdbRecord Type
634
+
635
+ ```typescript
636
+ interface AbdbRecord {
637
+ _id?: string;
638
+ _created_at?: string; // auto-managed by AbdbRepository — do not declare in collection schema
639
+ _updated_at?: string; // auto-managed by AbdbRepository — do not declare in collection schema
640
+ [key: string]: unknown;
641
+ }
642
+ ```
643
+
644
+ ### Exports
645
+
646
+ ```typescript
647
+ import { AbdbRepository } from '@adobe-commerce/aio-toolkit';
648
+
649
+ // Types (TypeScript only)
650
+ import type {
651
+ AbdbRecord,
652
+ AbdbFindOptions,
653
+ AbdbFindSort,
654
+ AbdbFindSortDirection,
655
+ AbdbRepositoryFilter,
656
+ } from '@adobe-commerce/aio-toolkit';
657
+ ```
658
+
659
+ ---
660
+
661
+ ## Important Notes
662
+
663
+ 1. **AbdbCollection must exist first**: Always create the collection class using "Using AbdbCollection" before setting up the repository
664
+ 2. **`include-ims-credentials: true` is required**: Token generation fails without this annotation; the repository constructor throws if token is blank
665
+ 3. **Always extend `AbdbRepository`**: Never use `AbdbRepository` directly with a raw `AbdbCollection` in action code — create a dedicated subclass per entity in `lib/database/repository/`
666
+ 4. **Do not declare `_created_at` / `_updated_at` in the collection schema**: These are registered automatically by the repository constructor; duplicate registration throws
667
+ 5. **`findById()`, `deleteById()`, `isIdExists()` wrap `ObjectId` internally**: Use these for ID-based operations instead of manual `ObjectId` construction
668
+ 6. **`findOne()`, `updateOne()`, `deleteOne()` with `_id` need manual `ObjectId`**: Wrap with `new ObjectId(id)` from `bson` when filtering by `_id`
669
+ 7. **`delete()` with no filter deletes ALL documents**: Always pass a meaningful filter; use `deleteById()` or `deleteOne()` for targeted deletes
670
+ 8. **`save()` without ID = insert; with ID = update upsert**: Full validation on insert, partial on update
671
+ 9. **Custom queries belong in the repository subclass**: Use `getCollection().run()` inside a repository method — not `collection.run()` directly in action files
672
+ 10. **Database must be provisioned**: `aio app db provision --region amer` or `auto-provision: true` in `app.config.yaml`
673
+ 11. **Peer dependency is mandatory**: `@adobe/aio-lib-db >= 1.0.0` must be installed
674
+
675
+ ## Integration with Action Creation Rules
676
+
677
+ **This rule should be referenced in other action creation rules when CRUD database operations are mentioned:**
678
+
679
+ ### Trigger Patterns in Other Rules
680
+
681
+ When developers mention these phrases during action creation:
682
+
683
+ - "Insert / save / update / delete records in ABDB"
684
+ - "Find documents from the database"
685
+ - "Use AbdbRepository"
686
+ - "CRUD on App Builder Data"
687
+ - "Paginate database results"
688
+ - Any phrase containing "AbdbRepository" or "CRUD" + "ABDB" / "App Builder Data"
689
+
690
+ **Action to take:**
691
+
692
+ 1. Check if an `AbdbCollection` class already exists for the entity
693
+ 2. If not: run "Using AbdbCollection" first, then come back to this rule
694
+ 3. Create the repository class and integrate into the action
695
+ 4. This rule handles `ObjectId` guidance, pagination, timestamp management, and all CRUD templates
696
+
697
+ **Example Flow:**
698
+
699
+ ```
700
+ User: "Create a runtime action that inserts and retrieves queue messages from App Builder Data"
701
+ AI: ✓ Found QueueMessagesCollection in lib/database/collection/queue-messages
702
+ AI: Creating repository class... [creates lib/database/repository/queue-messages/index.ts]
703
+ AI: Integrating save() and find() into the action... [updates action/index.ts]
704
+ AI: Updated app.config.yaml with include-ims-credentials: true
705
+ ```