@adobe-commerce/aio-toolkit 1.2.5 → 1.2.6
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 +134 -0
- package/README.md +169 -0
- package/dist/aio-toolkit-cli-workflow/bin/cli.js +2048 -0
- package/dist/aio-toolkit-cli-workflow/bin/cli.js.map +1 -0
- package/dist/aio-toolkit-cursor-context/bin/cli.js +16 -0
- package/dist/aio-toolkit-cursor-context/bin/cli.js.map +1 -1
- package/dist/index.d.mts +51 -6
- package/dist/index.d.ts +51 -6
- package/dist/index.js +209 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +213 -0
- package/dist/index.mjs.map +1 -1
- package/files/cursor-context/commands/aio-toolkit-analyze-adobe-commerce-module.md +612 -0
- package/files/cursor-context/commands/aio-toolkit-create-amazon-sqs-consumer.md +445 -0
- package/files/cursor-context/commands/aio-toolkit-create-event-consumer-action.md +6 -0
- package/files/cursor-context/commands/aio-toolkit-create-graphql-action.md +21 -7
- package/files/cursor-context/commands/aio-toolkit-create-openwhisk-action.md +326 -0
- package/files/cursor-context/commands/aio-toolkit-create-runtime-action.md +15 -5
- package/files/cursor-context/commands/aio-toolkit-create-shipping-carrier.md +681 -0
- package/files/cursor-context/commands/aio-toolkit-create-webhook-action.md +22 -9
- package/files/cursor-context/rules/aio-toolkit-create-adobe-commerce-client.mdc +252 -116
- package/files/cursor-context/rules/aio-toolkit-oop-best-practices.mdc +10 -4
- package/files/cursor-context/rules/aio-toolkit-setup-new-relic-telemetry.mdc +167 -2
- package/files/cursor-context/rules/aio-toolkit-use-abdb-collection.mdc +610 -0
- package/files/cursor-context/rules/aio-toolkit-use-abdb-repository.mdc +705 -0
- package/files/cursor-context/rules/aio-toolkit-use-adobe-auth.mdc +442 -0
- package/files/cursor-context/rules/aio-toolkit-use-amazon-sqs-publish.mdc +397 -0
- package/files/cursor-context/rules/aio-toolkit-use-file-repository.mdc +502 -0
- package/files/cursor-context/rules/aio-toolkit-use-publish-event.mdc +510 -0
- package/files/cursor-context/rules/aio-toolkit-use-runtime-api-gateway-service.mdc +542 -0
- 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
|
+
```
|