@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,502 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Adding FileRepository to Adobe I/O Runtime actions using @adobe-commerce/aio-toolkit
|
|
3
|
+
globs: '**/{actions,lib}/**/*.{ts,js}'
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Using FileRepository
|
|
8
|
+
|
|
9
|
+
## Trigger Conditions
|
|
10
|
+
|
|
11
|
+
This rule applies when the user requests to add persistent file storage to an action using any of these phrases:
|
|
12
|
+
|
|
13
|
+
- "Store data in App Builder"
|
|
14
|
+
- "Save records to file storage"
|
|
15
|
+
- "Persist data across invocations"
|
|
16
|
+
- "Use FileRepository"
|
|
17
|
+
- "Add file storage to [action-name]"
|
|
18
|
+
- "CRUD file operations in App Builder"
|
|
19
|
+
- "Save to Adobe I/O Files"
|
|
20
|
+
- "Create a repository for [entity]"
|
|
21
|
+
- "Store [entity] data"
|
|
22
|
+
- "List / load / save / delete records"
|
|
23
|
+
|
|
24
|
+
**Important**: This rule does NOT create new actions. It adds persistent file storage to existing actions only.
|
|
25
|
+
|
|
26
|
+
**Integration with Other Action Creation Rules:**
|
|
27
|
+
|
|
28
|
+
When using action creation rules (RuntimeAction, WebhookAction, EventConsumerAction, GraphQlAction, OpenwhiskAction) and the developer mentions storing or persisting data:
|
|
29
|
+
|
|
30
|
+
- **Automatically trigger this rule** after the action is created
|
|
31
|
+
- Examples: "save the result to storage", "persist records", "CRUD operations on [entity]", "store data between invocations"
|
|
32
|
+
- This rule handles creating the repository class, integrating it into the action, and documenting ID/overwrite behaviour
|
|
33
|
+
|
|
34
|
+
**If you need to create a new action first:**
|
|
35
|
+
|
|
36
|
+
- Use the appropriate action creation command first
|
|
37
|
+
- Then this rule will add FileRepository integration to that action
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Step 1: Verify Prerequisites
|
|
42
|
+
|
|
43
|
+
Before proceeding, verify:
|
|
44
|
+
|
|
45
|
+
1. **Toolkit installed**: Check `@adobe-commerce/aio-toolkit` in `package.json` dependencies
|
|
46
|
+
- If NOT installed: `npm install @adobe-commerce/aio-toolkit`
|
|
47
|
+
|
|
48
|
+
2. **App Builder Files Services API**: `FileRepository` uses `@adobe/aio-sdk`'s Files client, which is included with standard App Builder credentials. No additional configuration is required — the Files Services API is automatically available in App Builder workspaces.
|
|
49
|
+
|
|
50
|
+
3. **Detect Language (TypeScript vs JavaScript)**:
|
|
51
|
+
|
|
52
|
+
**Check for TypeScript indicators (check ALL of these)**:
|
|
53
|
+
1. **Package Dependencies**: Look for `typescript` in `package.json` dependencies or devDependencies
|
|
54
|
+
2. **Config File**: Check if `tsconfig.json` exists in project root
|
|
55
|
+
3. **TypeScript Loaders**: Look for `ts-loader`, `ts-node`, or `@types/*` packages in `package.json`
|
|
56
|
+
4. **Existing Files**: Check for `.ts` or `.tsx` files in key directories (`actions/`, `src/`, `lib/`)
|
|
57
|
+
|
|
58
|
+
**Detection Logic**:
|
|
59
|
+
- **If ANY of the following are true → TypeScript project**:
|
|
60
|
+
- `typescript` package installed AND `tsconfig.json` exists
|
|
61
|
+
- Multiple TypeScript indicators (2 or more from above list)
|
|
62
|
+
- Existing `.ts` files found in `actions/` or `lib/` directories
|
|
63
|
+
- **Otherwise → JavaScript project**
|
|
64
|
+
|
|
65
|
+
4. **Detect Project Structure**:
|
|
66
|
+
- Check `app.config.yaml` for `application:` section (root actions)
|
|
67
|
+
- Check `app.config.yaml` for `extensions:` section (extension point actions)
|
|
68
|
+
- If extensions exist, parse the `$include` path for the extension directory
|
|
69
|
+
|
|
70
|
+
5. **Check Existing Actions**: List all existing actions and their file paths
|
|
71
|
+
- From root `actions/` directory
|
|
72
|
+
- From `[extension-path]/actions/` directory
|
|
73
|
+
- If NO actions exist: Stop and recommend creating an action first
|
|
74
|
+
|
|
75
|
+
6. Only proceed to Step 2 after confirming all prerequisites
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Step 2: Ask Required Questions
|
|
80
|
+
|
|
81
|
+
Before generating any code, ask the user the following questions:
|
|
82
|
+
|
|
83
|
+
**Important**: Do not make assumptions — keep asking until all details are clear.
|
|
84
|
+
|
|
85
|
+
1. **Entity Name**: What entity will this repository store?
|
|
86
|
+
- Examples: `Order`, `Product`, `Customer`, `Session`, `Config`
|
|
87
|
+
- Used to name the repository class (e.g. `OrderRepository`) and derive a default storage path
|
|
88
|
+
|
|
89
|
+
2. **Base Filepath**: What base directory path should records be stored under?
|
|
90
|
+
- Format: `/[app-name]/[entity-plural]` — e.g. `/my-app/orders`
|
|
91
|
+
- All records are stored as `{filepath}/{id}.json`
|
|
92
|
+
- **Default suggestion**: derive from entity name, e.g. `Order` → `/my-app/orders`
|
|
93
|
+
|
|
94
|
+
3. **Target Action(s)**: Which existing action(s) will use this repository?
|
|
95
|
+
- List all discovered actions for the user to choose from
|
|
96
|
+
- Multiple actions can share the same repository class
|
|
97
|
+
|
|
98
|
+
4. **CRUD Operations**: Which operations are needed?
|
|
99
|
+
- **`list()`** — retrieve all records (reads every file; for large datasets use `metadata()` instead)
|
|
100
|
+
- **`metadata()`** — retrieve file metadata without reading content (fast; timestamps, sizes, existence checks)
|
|
101
|
+
- **`load(id)`** — retrieve a single record by ID
|
|
102
|
+
- **`save(payload, id?, overwrite?)`** — create or update a record
|
|
103
|
+
- **`delete(ids[])`** — delete one or more records
|
|
104
|
+
- Ask: which of these does the action need?
|
|
105
|
+
|
|
106
|
+
5. **ID Strategy** (for `save()` operations): How should record IDs be assigned?
|
|
107
|
+
- **Auto-generated** (default): millisecond timestamp (`new Date().getTime()`)
|
|
108
|
+
- **From payload**: use `payload.id` when the record already carries its own ID
|
|
109
|
+
- **Explicit parameter**: pass a specific ID from `params` (e.g. `params.orderId`) — will be sanitized (`order-123` → `order_123`)
|
|
110
|
+
|
|
111
|
+
6. **Overwrite vs Merge** (for `save()` operations):
|
|
112
|
+
- **Merge** (default, `overwrite: false`): shallow-merges new payload into existing record — existing fields not in the payload are preserved
|
|
113
|
+
- **Overwrite** (`overwrite: true`): replaces the entire file — existing fields are lost
|
|
114
|
+
- Ask: which behaviour is needed for each save operation?
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Step 3: Confirm Implementation Details
|
|
119
|
+
|
|
120
|
+
After receiving answers to ALL questions, present a comprehensive summary and **wait for user confirmation** before proceeding to Step 4:
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### 📋 FileRepository Integration Summary
|
|
125
|
+
|
|
126
|
+
**Entity:** `[EntityName]`
|
|
127
|
+
**Repository Class:** `[EntityName]Repository`
|
|
128
|
+
**Storage Path:** `[/app-name/entity-plural]`
|
|
129
|
+
**Language:** [TypeScript/JavaScript] (auto-detected)
|
|
130
|
+
|
|
131
|
+
**Target Action(s):**
|
|
132
|
+
- `[action-name]` → `[path/to/action/index.js]`
|
|
133
|
+
|
|
134
|
+
**Operations to Integrate:**
|
|
135
|
+
|
|
136
|
+
| Operation | Details |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `list()` | Retrieve all records |
|
|
139
|
+
| `metadata()` | Retrieve file metadata without content |
|
|
140
|
+
| `load(id)` | Load single record by `[params.field]` |
|
|
141
|
+
| `save(payload, id?, overwrite?)` | ID: [auto / payload.id / params.field]; Overwrite: [true/false] |
|
|
142
|
+
| `delete(ids[])` | Delete records by `[params.field]` |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
### ✅ What Will Be Changed
|
|
147
|
+
|
|
148
|
+
**1. New Repository Class**
|
|
149
|
+
- ➕ `lib/[entityName]Repository.[js/ts]`
|
|
150
|
+
- Extends `FileRepository` with fixed filepath `[/app-name/entity-plural]`
|
|
151
|
+
- Reusable across all actions in the project
|
|
152
|
+
|
|
153
|
+
**2. Action Implementation**
|
|
154
|
+
- ✏️ `[action-path]/index.[js/ts]`
|
|
155
|
+
- Import `[EntityName]Repository` from `lib/`
|
|
156
|
+
- Construct `new [EntityName]Repository()` inside the handler
|
|
157
|
+
- Add selected CRUD operations with error handling
|
|
158
|
+
|
|
159
|
+
**3. No Configuration Changes Required**
|
|
160
|
+
- `FileRepository` requires no extra inputs in `app.config.yaml` — standard App Builder credentials are sufficient
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
**Should I proceed with this implementation?**
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Step 4: Generate Implementation
|
|
169
|
+
|
|
170
|
+
### Step 4.1: Create the Repository Class
|
|
171
|
+
|
|
172
|
+
Create a dedicated repository class in `lib/`. Always extend `FileRepository` rather than using it directly — this encapsulates the storage path and makes the class reusable.
|
|
173
|
+
|
|
174
|
+
#### JavaScript
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
// lib/[entityName]Repository.js
|
|
178
|
+
const { FileRepository } = require('@adobe-commerce/aio-toolkit');
|
|
179
|
+
|
|
180
|
+
class [EntityName]Repository extends FileRepository {
|
|
181
|
+
constructor() {
|
|
182
|
+
super('[/app-name/entity-plural]');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = [EntityName]Repository;
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### TypeScript
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// lib/[entityName]Repository.ts
|
|
193
|
+
import { FileRepository } from '@adobe-commerce/aio-toolkit';
|
|
194
|
+
|
|
195
|
+
export class [EntityName]Repository extends FileRepository {
|
|
196
|
+
constructor() {
|
|
197
|
+
super('[/app-name/entity-plural]');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
> Place this file in a shared `lib/` directory at the same level as `actions/` so it can be imported from any action.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### Step 4.2: Integrate into the Target Action
|
|
207
|
+
|
|
208
|
+
Add the repository to the existing action. Include only the CRUD operations selected in Step 2.
|
|
209
|
+
|
|
210
|
+
#### List All Records — JavaScript
|
|
211
|
+
|
|
212
|
+
```javascript
|
|
213
|
+
const { HttpMethod, RuntimeAction, RuntimeActionResponse } = require('@adobe-commerce/aio-toolkit');
|
|
214
|
+
const [EntityName]Repository = require('../../lib/[entityName]Repository');
|
|
215
|
+
|
|
216
|
+
exports.main = RuntimeAction.execute(
|
|
217
|
+
'[action-name]',
|
|
218
|
+
[HttpMethod.POST],
|
|
219
|
+
[],
|
|
220
|
+
['Authorization'],
|
|
221
|
+
async (params, ctx) => {
|
|
222
|
+
const repo = new [EntityName]Repository();
|
|
223
|
+
const records = await repo.list();
|
|
224
|
+
return RuntimeActionResponse.success(records);
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
#### Load a Single Record — JavaScript
|
|
230
|
+
|
|
231
|
+
```javascript
|
|
232
|
+
// Check for empty record — load() returns {} when the file does not exist
|
|
233
|
+
const record = await repo.load(params.id);
|
|
234
|
+
if (!record.id) {
|
|
235
|
+
return RuntimeActionResponse.error(HttpStatus.NOT_FOUND, `[EntityName] '${params.id}' not found`);
|
|
236
|
+
}
|
|
237
|
+
return RuntimeActionResponse.success(record);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
#### Save a Record (auto-generated ID) — JavaScript
|
|
241
|
+
|
|
242
|
+
```javascript
|
|
243
|
+
const savedId = await repo.save({
|
|
244
|
+
// payload fields
|
|
245
|
+
status: 'active',
|
|
246
|
+
...params,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// save() returns null on failure — always check
|
|
250
|
+
if (!savedId) {
|
|
251
|
+
return RuntimeActionResponse.error(HttpStatus.INTERNAL_ERROR, 'Failed to save [entityName]');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return RuntimeActionResponse.success({ id: savedId });
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
#### Save with Explicit ID (merge, default) — JavaScript
|
|
258
|
+
|
|
259
|
+
```javascript
|
|
260
|
+
// ID from params is sanitized: 'order-123' → 'order_123'
|
|
261
|
+
const savedId = await repo.save(
|
|
262
|
+
{ status: params.status },
|
|
263
|
+
params.[entityId] // explicit ID takes precedence over payload.id
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (!savedId) {
|
|
267
|
+
return RuntimeActionResponse.error(HttpStatus.INTERNAL_ERROR, 'Failed to update [entityName]');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return RuntimeActionResponse.success({ id: savedId });
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
#### Save with Overwrite (replace entire file) — JavaScript
|
|
274
|
+
|
|
275
|
+
```javascript
|
|
276
|
+
// overwrite: true replaces the entire file — no merging with existing content
|
|
277
|
+
const savedId = await repo.save(
|
|
278
|
+
{
|
|
279
|
+
// provide ALL fields — partial updates are not possible with overwrite
|
|
280
|
+
status: params.status,
|
|
281
|
+
customerId: params.customerId,
|
|
282
|
+
},
|
|
283
|
+
params.[entityId],
|
|
284
|
+
true // overwrite: true
|
|
285
|
+
);
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
#### Delete Records — JavaScript
|
|
289
|
+
|
|
290
|
+
```javascript
|
|
291
|
+
// Pass exact IDs as returned by save(), list(), or load()
|
|
292
|
+
// IDs are NOT sanitized by delete() — do not transform them before passing
|
|
293
|
+
const remaining = await repo.delete(params.ids);
|
|
294
|
+
return RuntimeActionResponse.success(remaining);
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
#### Get Metadata (without reading content) — JavaScript
|
|
298
|
+
|
|
299
|
+
```javascript
|
|
300
|
+
// All records — no JSON reads, fast for large datasets
|
|
301
|
+
const allMeta = await repo.metadata();
|
|
302
|
+
|
|
303
|
+
// Single record metadata
|
|
304
|
+
const singleMeta = params.id ? await repo.metadata(params.id) : null;
|
|
305
|
+
|
|
306
|
+
return RuntimeActionResponse.success({ all: allMeta, single: singleMeta });
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**TypeScript:** Same patterns with `import` syntax and optional type imports:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
import { FileRepository } from '@adobe-commerce/aio-toolkit';
|
|
313
|
+
import type { FileRecord, FileMetadata } from '@adobe-commerce/aio-toolkit';
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Step 5: Recommendations
|
|
319
|
+
|
|
320
|
+
### A. Always Check save() Return Value
|
|
321
|
+
|
|
322
|
+
`save()` is the only method that catches errors internally — it returns `null` on failure rather than throwing. All other methods propagate errors from the Files SDK.
|
|
323
|
+
|
|
324
|
+
```javascript
|
|
325
|
+
const savedId = await repo.save(payload, id);
|
|
326
|
+
if (!savedId) {
|
|
327
|
+
// Write failed — log and return an appropriate error response
|
|
328
|
+
logger.error({ message: '[action-name]-save-failed' });
|
|
329
|
+
return RuntimeActionResponse.error(HttpStatus.INTERNAL_ERROR, 'Storage write failed');
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### B. Checking for a Missing Record
|
|
334
|
+
|
|
335
|
+
`load()` returns `{}` (empty object cast to `FileRecord`) when no file exists — it does not throw. Always check for record existence before accessing fields:
|
|
336
|
+
|
|
337
|
+
```javascript
|
|
338
|
+
const record = await repo.load(params.id);
|
|
339
|
+
if (!record.id) {
|
|
340
|
+
// Record does not exist
|
|
341
|
+
return RuntimeActionResponse.error(HttpStatus.NOT_FOUND, `[EntityName] '${params.id}' not found`);
|
|
342
|
+
}
|
|
343
|
+
// record is valid — use record.id, record.status, etc.
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### C. ID Sanitization Rules
|
|
347
|
+
|
|
348
|
+
When an explicit `id` is passed to `save()`, it is sanitized:
|
|
349
|
+
|
|
350
|
+
| Input | Result |
|
|
351
|
+
|---|---|
|
|
352
|
+
| `'order-123'` | `'order_123'` |
|
|
353
|
+
| `'my.record'` | `'my_record'` |
|
|
354
|
+
| `'valid_id_123'` | `'valid_id_123'` (unchanged) |
|
|
355
|
+
| `'---'` | `'1714234567890'` (timestamp fallback) |
|
|
356
|
+
|
|
357
|
+
- Characters outside `[a-zA-Z0-9_]` are replaced with `_`
|
|
358
|
+
- If the result is empty or all underscores, falls back to millisecond timestamp
|
|
359
|
+
- `payload.id` is **not** sanitized — only explicit `id` parameter is
|
|
360
|
+
- IDs passed to `delete()` are **never** sanitized — pass exact IDs from `save()`/`list()`/`load()`
|
|
361
|
+
|
|
362
|
+
### D. Timestamps Are From the File System
|
|
363
|
+
|
|
364
|
+
`createdAt` and `updatedAt` fields on `FileRecord` come from file system metadata (creation time and last modified time). They are **not stored inside the JSON file**. This means:
|
|
365
|
+
- They are automatically updated on every `save()` call — no manual timestamp management needed
|
|
366
|
+
- You cannot set or override them via payload — they are stripped from `payload` before writing
|
|
367
|
+
|
|
368
|
+
### E. Performance: list() vs metadata()
|
|
369
|
+
|
|
370
|
+
| Situation | Use |
|
|
371
|
+
|---|---|
|
|
372
|
+
| Need full record content | `list()` — reads every JSON file |
|
|
373
|
+
| Only need sizes, timestamps, or existence | `metadata()` — no file content reads, much faster for large datasets |
|
|
374
|
+
| Check if a specific record exists | `metadata(id)` — single metadata fetch |
|
|
375
|
+
|
|
376
|
+
### F. Overwrite vs Merge — Choose Carefully
|
|
377
|
+
|
|
378
|
+
| Scenario | Overwrite Setting |
|
|
379
|
+
|---|---|
|
|
380
|
+
| Partial update (e.g. update status only) | `overwrite: false` (default) — merges with existing |
|
|
381
|
+
| Full replacement (e.g. PUT semantics) | `overwrite: true` — replaces entire file |
|
|
382
|
+
| New record | Both behave the same — file does not exist yet |
|
|
383
|
+
|
|
384
|
+
> **Warning**: With `overwrite: true`, any fields not in the new payload are permanently lost. Use it only when you intend full replacement (PUT), not partial update (PATCH).
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Key Components from @adobe-commerce/aio-toolkit
|
|
389
|
+
|
|
390
|
+
### FileRepository Constructor
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
constructor(filepath: string)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
- `filepath` — base directory for all file operations; all records stored as `{filepath}/{id}.json`
|
|
397
|
+
- Files SDK client is initialized lazily on first operation and cached for the lifetime of the instance
|
|
398
|
+
|
|
399
|
+
### FileRepository Methods
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
// List all records — reads every file in the directory
|
|
403
|
+
async list(): Promise<FileRecord[]>
|
|
404
|
+
|
|
405
|
+
// Get metadata only — no file content reads
|
|
406
|
+
async metadata(id?: string): Promise<FileMetadata | FileMetadata[]>
|
|
407
|
+
|
|
408
|
+
// Load a single record by ID — returns {} if not found
|
|
409
|
+
async load(id: string): Promise<FileRecord>
|
|
410
|
+
|
|
411
|
+
// Save a record — returns ID on success, null on failure
|
|
412
|
+
async save(
|
|
413
|
+
payload: Partial<FileRecord>,
|
|
414
|
+
id?: string | null, // explicit ID — sanitized; overrides payload.id
|
|
415
|
+
overwrite?: boolean // default: false (merge)
|
|
416
|
+
): Promise<string | null>
|
|
417
|
+
|
|
418
|
+
// Delete records — returns updated list() after deletion
|
|
419
|
+
async delete(ids: string[]): Promise<FileRecord[]>
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### FileRecord Type
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
interface FileRecord {
|
|
426
|
+
id: string;
|
|
427
|
+
createdAt: string; // ISO 8601 UTC — from file system metadata, NOT stored in JSON
|
|
428
|
+
updatedAt: string; // ISO 8601 UTC — from file system metadata, NOT stored in JSON
|
|
429
|
+
[key: string]: any; // your payload fields
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### FileMetadata Type
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
interface FileMetadata {
|
|
437
|
+
name: string;
|
|
438
|
+
creationTime: Date;
|
|
439
|
+
lastModified: Date;
|
|
440
|
+
etag: string;
|
|
441
|
+
contentLength: number;
|
|
442
|
+
contentType: string;
|
|
443
|
+
isDirectory: boolean;
|
|
444
|
+
isPublic: boolean;
|
|
445
|
+
url: string;
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Exports
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
import { FileRepository } from '@adobe-commerce/aio-toolkit';
|
|
453
|
+
import type { FileRecord, FileMetadata } from '@adobe-commerce/aio-toolkit';
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## Important Notes
|
|
459
|
+
|
|
460
|
+
1. **Always extend FileRepository**: Create a dedicated subclass per entity — do not use `FileRepository` directly with a hardcoded path in action code
|
|
461
|
+
2. **save() returns null on failure**: It is the only method that silently catches errors — always check the return value
|
|
462
|
+
3. **load() returns {} on missing file**: Never throws when the file doesn't exist — check `!record.id` before using the record
|
|
463
|
+
4. **Timestamps are file system metadata**: `createdAt`/`updatedAt` are never stored in the JSON file — they come from the Files SDK metadata on every `load()` or `list()` call
|
|
464
|
+
5. **IDs passed to delete() are NOT sanitized**: Pass the exact ID strings returned by `save()`, `list()`, or `load()`
|
|
465
|
+
6. **No extra credentials needed**: FileRepository uses standard App Builder credentials — no additional inputs are required in `app.config.yaml`
|
|
466
|
+
7. **Files Services API must be enabled**: The App Builder workspace must have the Files Services API enabled in Adobe Developer Console (enabled by default in new App Builder projects)
|
|
467
|
+
8. **Filepath convention**: Use `/[app-name]/[entity-plural]` for the base path — e.g. `/my-app/orders`, `/my-app/products`
|
|
468
|
+
9. **Lazy SDK initialization**: The Files SDK client is created on the first operation, not in the constructor — no setup overhead if the repository is instantiated but not used
|
|
469
|
+
10. **Works with any action type**: RuntimeAction, WebhookAction, EventConsumerAction, GraphQlAction, OpenwhiskAction
|
|
470
|
+
|
|
471
|
+
## Integration with Action Creation Rules
|
|
472
|
+
|
|
473
|
+
**This rule should be referenced in other action creation rules when data storage is mentioned:**
|
|
474
|
+
|
|
475
|
+
### Trigger Patterns in Other Rules
|
|
476
|
+
|
|
477
|
+
When developers mention these phrases during action creation in the "Business Logic" question:
|
|
478
|
+
|
|
479
|
+
- "Store data"
|
|
480
|
+
- "Persist records"
|
|
481
|
+
- "Save to file storage"
|
|
482
|
+
- "CRUD operations on [entity]"
|
|
483
|
+
- "Store data between invocations"
|
|
484
|
+
- "Create a repository"
|
|
485
|
+
- Any phrase containing "FileRepository" or "file storage" + "save"/"list"/"load"/"delete"
|
|
486
|
+
|
|
487
|
+
**Action to take:**
|
|
488
|
+
|
|
489
|
+
1. Complete the action creation first
|
|
490
|
+
2. Then inform the user: "I see you need to persist data. Let me help you integrate FileRepository."
|
|
491
|
+
3. Trigger/reference the "Using FileRepository" rule
|
|
492
|
+
4. This rule handles creating the repository class, integrating it into the action, and all ID/overwrite guidance
|
|
493
|
+
|
|
494
|
+
**Example Flow:**
|
|
495
|
+
|
|
496
|
+
```
|
|
497
|
+
User: "Create a runtime action that saves order records to persistent storage"
|
|
498
|
+
AI: ✓ Detected JavaScript project
|
|
499
|
+
AI: Creating runtime action... [generates index.js]
|
|
500
|
+
AI: I see you need persistent storage. Let me integrate FileRepository.
|
|
501
|
+
AI: [Creates lib/OrderRepository.js and integrates it into the action]
|
|
502
|
+
```
|