@webiny/mcp 6.1.0-beta.1 → 6.1.0-beta.3
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/agents/claude.d.ts +2 -0
- package/agents/claude.js +6 -0
- package/agents/claude.js.map +1 -1
- package/agents/cline.d.ts +2 -0
- package/agents/cline.js +6 -0
- package/agents/cline.js.map +1 -1
- package/agents/copilot.d.ts +2 -0
- package/agents/copilot.js +7 -0
- package/agents/copilot.js.map +1 -1
- package/agents/cursor.d.ts +2 -0
- package/agents/cursor.js +6 -0
- package/agents/cursor.js.map +1 -1
- package/agents/discover.d.ts +3 -0
- package/agents/discover.js +29 -0
- package/agents/discover.js.map +1 -0
- package/agents/instructions.d.ts +3 -1
- package/agents/instructions.js +15 -2
- package/agents/instructions.js.map +1 -1
- package/agents/kiro.d.ts +2 -0
- package/agents/kiro.js +6 -0
- package/agents/kiro.js.map +1 -1
- package/agents/opencode.d.ts +2 -0
- package/agents/opencode.js +7 -0
- package/agents/opencode.js.map +1 -1
- package/agents/types.d.ts +16 -0
- package/agents/types.js +3 -0
- package/agents/types.js.map +1 -0
- package/agents/windsurf.d.ts +2 -0
- package/agents/windsurf.js +6 -0
- package/agents/windsurf.js.map +1 -1
- package/cli/ConfigureMcp.js +21 -6
- package/cli/ConfigureMcp.js.map +1 -1
- package/package.json +3 -3
- package/skills/admin/admin-architect/SKILL.md +188 -0
- package/skills/admin/admin-permissions/SKILL.md +159 -0
- package/skills/admin/ui-extensions/SKILL.md +2 -0
- package/skills/api/api-architect/SKILL.md +548 -60
- package/skills/api/event-handler-pattern/SKILL.md +195 -23
- package/skills/api/graphql-api/SKILL.md +231 -31
- package/skills/api/permissions/SKILL.md +291 -0
- package/skills/api/use-case-pattern/SKILL.md +351 -12
- package/skills/api/v5-to-v6-migration/SKILL.md +416 -0
- package/skills/cli-extensions/SKILL.md +1 -1
- package/skills/content-models/SKILL.md +9 -5
- package/skills/full-stack-architect/SKILL.md +4 -0
- package/skills/generated/admin/SKILL.md +1 -1
- package/skills/generated/admin/aco/SKILL.md +1 -1
- package/skills/generated/admin/build-params/SKILL.md +1 -1
- package/skills/generated/admin/cms/SKILL.md +1 -1
- package/skills/generated/admin/configs/SKILL.md +1 -1
- package/skills/generated/admin/env-config/SKILL.md +1 -1
- package/skills/generated/admin/form/SKILL.md +1 -1
- package/skills/generated/admin/graphql-client/SKILL.md +1 -1
- package/skills/generated/admin/lexical/SKILL.md +1 -1
- package/skills/generated/admin/local-storage/SKILL.md +1 -1
- package/skills/generated/admin/router/SKILL.md +1 -1
- package/skills/generated/admin/security/SKILL.md +1 -1
- package/skills/generated/admin/tenancy/SKILL.md +1 -1
- package/skills/generated/admin/ui/SKILL.md +1 -1
- package/skills/generated/admin/website-builder/SKILL.md +1 -1
- package/skills/generated/api/SKILL.md +1 -1
- package/skills/generated/api/aco/SKILL.md +1 -1
- package/skills/generated/api/build-params/SKILL.md +1 -1
- package/skills/generated/api/cms/SKILL.md +1 -1
- package/skills/generated/api/event-publisher/SKILL.md +1 -1
- package/skills/generated/api/file-manager/SKILL.md +1 -1
- package/skills/generated/api/graphql/SKILL.md +1 -1
- package/skills/generated/api/key-value-store/SKILL.md +1 -1
- package/skills/generated/api/logger/SKILL.md +1 -1
- package/skills/generated/api/opensearch/SKILL.md +1 -1
- package/skills/generated/api/scheduler/SKILL.md +1 -1
- package/skills/generated/api/security/SKILL.md +1 -1
- package/skills/generated/api/system/SKILL.md +1 -1
- package/skills/generated/api/tasks/SKILL.md +1 -1
- package/skills/generated/api/tenancy/SKILL.md +1 -1
- package/skills/generated/api/tenant-manager/SKILL.md +1 -1
- package/skills/generated/api/website-builder/SKILL.md +1 -1
- package/skills/generated/cli/SKILL.md +1 -1
- package/skills/generated/cli/command/SKILL.md +1 -1
- package/skills/generated/extensions/SKILL.md +1 -1
- package/skills/generated/infra/SKILL.md +1 -1
- package/skills/infrastructure-extensions/SKILL.md +1 -1
- package/skills/project-structure/SKILL.md +4 -0
- package/skills/webiny-sdk/SKILL.md +1 -1
|
@@ -2,115 +2,412 @@
|
|
|
2
2
|
name: webiny-api-architect
|
|
3
3
|
context: webiny-extensions
|
|
4
4
|
description: >
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
The hub skill for all API/backend architecture in Webiny. Covers architecture overview,
|
|
6
|
+
Services vs UseCases, feature naming and organization, feature structure templates,
|
|
7
|
+
DI decision tree, anti-patterns, createFeature, createAbstraction, container registration,
|
|
8
|
+
domain errors, entity patterns, naming conventions, scoping rules, and code conventions.
|
|
9
|
+
Use this skill for ANY backend API work — it references sub-skills for deep implementation details.
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
# API Architecture Patterns
|
|
12
13
|
|
|
13
14
|
## TL;DR
|
|
14
15
|
|
|
15
|
-
API extensions use `createFeature`
|
|
16
|
+
API extensions use `createFeature` to register features into the DI container. Each feature is a vertical slice with abstractions, implementations, and a `feature.ts` registration file. The key abstractions are **Services** (multi-method, singleton) and **UseCases** (single-method orchestrators, transient). Repositories handle persistence via CMS. Features are named by **business capability**, files inside by **technical responsibility**.
|
|
17
|
+
|
|
18
|
+
## Architecture Overview
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Extension (root) ── registers ──> Features + GraphQL Schemas + Models
|
|
22
|
+
Feature ── registers ──> UseCase | Service | EventHandler + Repository
|
|
23
|
+
UseCase ── depends on ──> Service | Repository (+ EventPublisher)
|
|
24
|
+
Repository ── depends on ──> CMS Use Cases (GetModel, CreateEntry, etc.)
|
|
25
|
+
Service ── depends on ──> external APIs, other Services
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- **Extension**: Top-level entry point. Registers all features, GraphQL schemas, and CMS models.
|
|
29
|
+
- **Feature**: A vertical slice. Registers its use cases, services, repositories, and event handlers.
|
|
30
|
+
- **UseCase**: Single-method orchestrator (`execute()`). Coordinates services, repositories, and events. Transient scope.
|
|
31
|
+
- **Service**: Multi-method abstraction for external API calls or cohesive domain logic. Singleton scope.
|
|
32
|
+
- **Repository**: Persistence layer using CMS as storage. Singleton scope.
|
|
33
|
+
- **EventHandler**: Thin orchestrator reacting to domain events. Delegates to services/use cases.
|
|
34
|
+
- **GraphQL Schema**: Defines types, inputs, queries, and mutations. Resolvers delegate to use cases.
|
|
35
|
+
- **CMS Model**: Defines the data schema stored in headless CMS.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Services vs UseCases
|
|
40
|
+
|
|
41
|
+
### Services
|
|
42
|
+
|
|
43
|
+
Multi-method abstractions for **external API calls** or **cohesive domain logic**. A service groups related operations that belong together.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// abstractions.ts
|
|
47
|
+
export interface ILingotekService {
|
|
48
|
+
translate(documentId: string, targetLocale: string): Promise<Result<void, Error>>;
|
|
49
|
+
getTranslationStatus(documentId: string): Promise<Result<TranslationStatus, Error>>;
|
|
50
|
+
deleteProject(projectId: string): Promise<Result<void, Error>>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const LingotekService = createAbstraction<ILingotekService>("MyExt/LingotekService");
|
|
54
|
+
|
|
55
|
+
export namespace LingotekService {
|
|
56
|
+
export type Interface = ILingotekService;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- Registered in **singleton scope** (`.inSingletonScope()`)
|
|
61
|
+
- Located in: `features/{serviceName}/` or `features/services/{serviceName}/`
|
|
62
|
+
- One service per external system or cohesive domain area
|
|
63
|
+
- **If async bootstrap is needed** (loading settings from CMS, fetching remote config): use the **ServiceProvider pattern** — a provider abstraction with `async getService()` that lazily initializes and caches the service. Consumers inject the provider, not the service directly. See the ServiceProvider section below.
|
|
64
|
+
|
|
65
|
+
### UseCases
|
|
66
|
+
|
|
67
|
+
Single-method orchestrators with an `execute()` method. They coordinate services, repositories, and events.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
export interface ISyncProjectUseCase {
|
|
71
|
+
execute(input: SyncProjectInput): Promise<Result<Project, SyncProjectError>>;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- Registered in **transient scope** (default)
|
|
76
|
+
- Located in: `features/{ActionEntity}/`
|
|
77
|
+
- One use case per business operation
|
|
78
|
+
|
|
79
|
+
### When to Create a UseCase
|
|
80
|
+
|
|
81
|
+
- GraphQL mutations need the same logic as event handlers
|
|
82
|
+
- Need to coordinate multiple services or repositories
|
|
83
|
+
- Business logic must be reusable across entry points (GraphQL, events, CLI)
|
|
84
|
+
|
|
85
|
+
### When NOT to Create a UseCase
|
|
86
|
+
|
|
87
|
+
- Simple event handler that calls one service method — inject the service directly
|
|
88
|
+
- Simple read queries — inject the service or repository directly into the GraphQL resolver
|
|
89
|
+
- Logic that only exists in one place and is unlikely to be reused
|
|
90
|
+
|
|
91
|
+
### ServiceProvider Pattern (Async Bootstrap)
|
|
92
|
+
|
|
93
|
+
When a service requires async initialization (loading CMS settings, fetching remote config, API tokens), use a **ServiceProvider** — a provider abstraction with `async getService()` that lazily creates and caches the service. Both the provider and the service are part of the same feature. The provider is the primary abstraction exported from the feature. The service itself is not registered in the DI container.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// abstractions.ts
|
|
97
|
+
export interface ILingotekServiceProvider {
|
|
98
|
+
getService(): Promise<ILingotekService>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const LingotekServiceProvider = createAbstraction<ILingotekServiceProvider>(
|
|
102
|
+
"MyExt/LingotekServiceProvider"
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
export namespace LingotekServiceProvider {
|
|
106
|
+
export type Interface = ILingotekServiceProvider;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// LingotekServiceProvider.ts
|
|
112
|
+
class LingotekServiceProviderImpl implements ProviderAbstraction.Interface {
|
|
113
|
+
private service: ILingotekService | undefined;
|
|
114
|
+
|
|
115
|
+
constructor(private getSettings: GetSettingsUseCase.Interface) {}
|
|
116
|
+
|
|
117
|
+
async getService(): Promise<ILingotekService> {
|
|
118
|
+
if (!this.service) {
|
|
119
|
+
const result = await this.getSettings.execute();
|
|
120
|
+
const settings = result.isOk() ? result.value : defaultSettings;
|
|
121
|
+
this.service = new LingotekService(settings);
|
|
122
|
+
}
|
|
123
|
+
return this.service;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- Register the **provider** in singleton scope (it caches the service)
|
|
129
|
+
- The service itself is NOT registered in DI — it's created by the provider
|
|
130
|
+
- Consumers call `await provider.getService()` before using the service
|
|
131
|
+
- Use cases and handlers inject `LingotekServiceProvider`, not `LingotekService`
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Feature Naming Philosophy
|
|
136
|
+
|
|
137
|
+
Features use a **two-level naming convention**:
|
|
138
|
+
|
|
139
|
+
- **Feature directory** = business capability (what it does for the business)
|
|
140
|
+
- **Files inside** = technical responsibility (what each file handles)
|
|
141
|
+
|
|
142
|
+
This makes features **discoverable by what they DO**, and once inside a feature folder, you see the technical components clearly.
|
|
143
|
+
|
|
144
|
+
### Good
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
features/
|
|
148
|
+
├── syncToLingotek/ ← business capability
|
|
149
|
+
│ ├── abstractions.ts
|
|
150
|
+
│ ├── SyncProjectUseCase.ts ← technical responsibility
|
|
151
|
+
│ ├── EntryAfterCreateHandler.ts ← technical responsibility (fine as filename!)
|
|
152
|
+
│ ├── EntryAfterUpdateHandler.ts
|
|
153
|
+
│ └── feature.ts
|
|
154
|
+
├── cleanupLingotekDocument/
|
|
155
|
+
│ ├── EntryBeforeDeleteHandler.ts
|
|
156
|
+
│ └── feature.ts
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Bad
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
features/
|
|
163
|
+
├── EntryAfterCreateHandler/ ← ❌ technical name as feature directory
|
|
164
|
+
├── DocumentBeforeDeleteHandler/ ← ❌ technical name as feature directory
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Rules
|
|
168
|
+
|
|
169
|
+
- Feature directories describe **business capability**: `syncToLingotek`, `cleanupOnDelete`, `notifySlack`
|
|
170
|
+
- Files inside describe **technical responsibility**: `EntryAfterCreateHandler.ts`, `SyncProjectUseCase.ts`
|
|
171
|
+
- Event handlers ARE features — they live in `features/`, never in a separate `handlers/` directory
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Feature Structure Templates
|
|
176
|
+
|
|
177
|
+
### Simple Event Handler Feature
|
|
178
|
+
|
|
179
|
+
When: handler calls a service or use case, no new abstractions needed.
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
features/cleanupOnDelete/
|
|
183
|
+
├── CleanupOnDeleteHandler.ts # Implements an existing EventHandler abstraction
|
|
184
|
+
└── feature.ts # Registers the handler
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Complex Feature with UseCases
|
|
188
|
+
|
|
189
|
+
When: logic is reused by GraphQL + event handlers, or coordinates multiple services.
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
features/syncProjectToLingotek/
|
|
193
|
+
├── abstractions.ts # UseCase + error types for this feature
|
|
194
|
+
├── CreateProjectUseCase.ts
|
|
195
|
+
├── UpdateProjectUseCase.ts
|
|
196
|
+
├── DeleteProjectUseCase.ts
|
|
197
|
+
├── EntryAfterCreateHandler.ts # Thin handler → delegates to CreateProjectUseCase
|
|
198
|
+
├── EntryAfterUpdateHandler.ts # Thin handler → delegates to UpdateProjectUseCase
|
|
199
|
+
├── EntryAfterDeleteHandler.ts # Thin handler → delegates to DeleteProjectUseCase
|
|
200
|
+
└── feature.ts # Registers everything
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Service Feature
|
|
204
|
+
|
|
205
|
+
When: reusable multi-method service for an external API or domain area.
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
features/lingotekService/
|
|
209
|
+
├── abstractions.ts # Service interface (multi-method)
|
|
210
|
+
├── LingotekService.ts # Implementation
|
|
211
|
+
└── feature.ts # Registers in singleton scope
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## DI Decision Tree
|
|
217
|
+
|
|
218
|
+
### What to inject based on what you're building
|
|
219
|
+
|
|
220
|
+
| You're building a... | It needs to... | Inject |
|
|
221
|
+
| -------------------- | -------------------------- | ------------------------------------------ |
|
|
222
|
+
| **Event Handler** | Call external API | Service |
|
|
223
|
+
| **Event Handler** | Orchestrate CMS + external | UseCase |
|
|
224
|
+
| **Event Handler** | Just log/validate | Logger (or nothing) |
|
|
225
|
+
| **GraphQL Resolver** | Simple read | Service or Repository directly |
|
|
226
|
+
| **GraphQL Resolver** | Complex mutation | UseCase |
|
|
227
|
+
| **GraphQL Resolver** | Check permissions | IdentityContext or Permissions abstraction |
|
|
228
|
+
| **UseCase** | Call external API | Service |
|
|
229
|
+
| **UseCase** | Persist/read data | Repository |
|
|
230
|
+
| **UseCase** | Publish domain events | EventPublisher |
|
|
231
|
+
| **UseCase** | Check permissions | IdentityContext or Permissions abstraction |
|
|
232
|
+
| **Repository** | Access CMS | GetModelUseCase, CreateEntryUseCase, etc. |
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Anti-Patterns
|
|
237
|
+
|
|
238
|
+
### ❌ Creating one abstraction per operation instead of a multi-method Service
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
// WRONG — separate abstractions for related operations
|
|
242
|
+
export const DeleteDocumentService = createAbstraction(...)
|
|
243
|
+
export const CreateDocumentService = createAbstraction(...)
|
|
244
|
+
export const UpdateDocumentService = createAbstraction(...)
|
|
245
|
+
|
|
246
|
+
// CORRECT — one multi-method Service
|
|
247
|
+
export interface IDocumentService {
|
|
248
|
+
create(input: CreateInput): Promise<Result<Doc, Error>>;
|
|
249
|
+
update(id: string, input: UpdateInput): Promise<Result<Doc, Error>>;
|
|
250
|
+
delete(id: string): Promise<Result<void, Error>>;
|
|
251
|
+
}
|
|
252
|
+
export const DocumentService = createAbstraction<IDocumentService>("MyExt/DocumentService");
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### ❌ Naming features by technical implementation
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
features/DocumentBeforeDeleteHandler/ ← WRONG: technical name
|
|
259
|
+
features/cleanupLingotekDocument/ ← CORRECT: business capability
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### ❌ Assuming builders exist for factories
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
// WRONG — no builder pattern exists
|
|
266
|
+
builder.role({ ... }).permissions([...])
|
|
267
|
+
|
|
268
|
+
// CORRECT — factories return plain objects
|
|
269
|
+
async execute(): Promise<CodeRole[]> {
|
|
270
|
+
return [{ name: "Admin", slug: "admin", description: "...", permissions: [...] }];
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### ❌ Separate handlers/ directory
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
api/handlers/MyHandler.ts ← WRONG: handlers are features
|
|
278
|
+
features/myFeature/MyHandler.ts ← CORRECT: handler lives inside its feature
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### ❌ Using generic Error instead of domain-specific errors
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
// WRONG
|
|
285
|
+
throw new Error("Not found");
|
|
286
|
+
|
|
287
|
+
// CORRECT
|
|
288
|
+
return Result.fail(new EntityNotFoundError(id));
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### ❌ Not filtering event handlers by model/entity type
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
// WRONG — fires for ALL models
|
|
295
|
+
async handle(event) {
|
|
296
|
+
await this.service.doWork(event.payload.entry);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// CORRECT — filter by your model
|
|
300
|
+
async handle(event) {
|
|
301
|
+
if (event.payload.model.modelId !== MY_MODEL_ID) return;
|
|
302
|
+
await this.service.doWork(event.payload.entry);
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
16
307
|
|
|
17
308
|
## API Directory Structure
|
|
18
309
|
|
|
19
310
|
```
|
|
20
311
|
api/
|
|
21
|
-
├── Extension.ts
|
|
22
|
-
├── domain/
|
|
23
|
-
├──
|
|
24
|
-
│
|
|
25
|
-
│
|
|
26
|
-
│
|
|
27
|
-
|
|
312
|
+
├── Extension.ts # API entry point (createFeature, registers everything)
|
|
313
|
+
├── domain/
|
|
314
|
+
│ ├── errors.ts # Domain-specific errors (extend BaseError)
|
|
315
|
+
│ ├── EntityId.ts # Value object for entity IDs
|
|
316
|
+
│ ├── EntityModel.ts # CMS model definition (ModelFactory)
|
|
317
|
+
│ └── EntityModelExtension.ts # Abstraction for extending the model
|
|
318
|
+
├── features/
|
|
319
|
+
│ ├── createEntity/ # Feature: business capability
|
|
320
|
+
│ │ ├── abstractions.ts # UseCase + Repository abstractions + error types
|
|
321
|
+
│ │ ├── feature.ts # DI registration
|
|
322
|
+
│ │ ├── CreateEntityUseCase.ts
|
|
323
|
+
│ │ └── CreateEntityRepository.ts
|
|
324
|
+
│ ├── lingotekService/ # Service feature
|
|
325
|
+
│ │ ├── abstractions.ts
|
|
326
|
+
│ │ ├── LingotekService.ts
|
|
327
|
+
│ │ └── feature.ts
|
|
328
|
+
│ └── syncToLingotek/ # Event handler feature
|
|
329
|
+
│ ├── EntryAfterCreateHandler.ts
|
|
28
330
|
│ └── feature.ts
|
|
29
|
-
└── graphql/
|
|
30
|
-
|
|
331
|
+
└── graphql/
|
|
332
|
+
├── CreateEntitySchema.ts
|
|
333
|
+
└── GetEntitySchema.ts
|
|
31
334
|
```
|
|
32
335
|
|
|
33
336
|
## API Extension Entry Point
|
|
34
337
|
|
|
35
|
-
The API entry point uses `createFeature` to register all backend components into the DI container:
|
|
36
|
-
|
|
37
338
|
```ts
|
|
38
339
|
// src/api/Extension.ts
|
|
39
340
|
import { createFeature } from "webiny/api";
|
|
40
|
-
import
|
|
41
|
-
import
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
341
|
+
import EntityModel from "./domain/EntityModel.js";
|
|
342
|
+
import CreateEntitySchema from "./graphql/CreateEntitySchema.js";
|
|
343
|
+
import { CreateEntityFeature } from "./features/createEntity/feature.js";
|
|
344
|
+
import { LingotekServiceFeature } from "./features/lingotekService/feature.js";
|
|
345
|
+
import { SyncToLingotekFeature } from "./features/syncToLingotek/feature.js";
|
|
44
346
|
|
|
45
347
|
export const Extension = createFeature({
|
|
46
348
|
name: "MyExtension",
|
|
47
349
|
register(container) {
|
|
48
|
-
//
|
|
49
|
-
container.register(
|
|
350
|
+
// CMS model (register first)
|
|
351
|
+
container.register(EntityModel);
|
|
50
352
|
|
|
51
353
|
// GraphQL schemas
|
|
52
|
-
container.register(
|
|
354
|
+
container.register(CreateEntitySchema);
|
|
53
355
|
|
|
54
|
-
// Features (use
|
|
55
|
-
|
|
56
|
-
|
|
356
|
+
// Features (use Feature.register, NOT container.register)
|
|
357
|
+
CreateEntityFeature.register(container);
|
|
358
|
+
LingotekServiceFeature.register(container);
|
|
359
|
+
SyncToLingotekFeature.register(container);
|
|
57
360
|
}
|
|
58
361
|
});
|
|
59
362
|
```
|
|
60
363
|
|
|
364
|
+
**Rules:**
|
|
365
|
+
|
|
366
|
+
- Register the CMS model first.
|
|
367
|
+
- Register GraphQL schemas with `container.register()`.
|
|
368
|
+
- Register features with `Feature.register(container)` (not `container.register(Feature)`).
|
|
369
|
+
|
|
61
370
|
## Abstractions
|
|
62
371
|
|
|
63
372
|
Every piece of business logic starts with a typed abstraction token:
|
|
64
373
|
|
|
65
374
|
```ts
|
|
66
|
-
// src/api/features/
|
|
375
|
+
// src/api/features/createEntity/abstractions.ts
|
|
67
376
|
import { createAbstraction, Result } from "webiny/api";
|
|
68
377
|
import type { MyEntity } from "~/shared/MyEntity.js";
|
|
69
378
|
|
|
70
|
-
export interface
|
|
379
|
+
export interface ICreateEntityInput {
|
|
71
380
|
name: string;
|
|
72
381
|
}
|
|
73
382
|
|
|
74
|
-
export interface
|
|
75
|
-
execute(input:
|
|
383
|
+
export interface ICreateEntityUseCase {
|
|
384
|
+
execute(input: ICreateEntityInput): Promise<Result<MyEntity, Error>>;
|
|
76
385
|
}
|
|
77
386
|
|
|
78
|
-
export const
|
|
79
|
-
"MyExtension/
|
|
387
|
+
export const CreateEntityUseCase = createAbstraction<ICreateEntityUseCase>(
|
|
388
|
+
"MyExtension/CreateEntityUseCase"
|
|
80
389
|
);
|
|
81
390
|
|
|
82
391
|
// Namespace re-exports all related types for convenient access
|
|
83
|
-
export namespace
|
|
84
|
-
export type Interface =
|
|
85
|
-
export type Input =
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface ICreateThingRepository {
|
|
89
|
-
execute(entity: MyEntity): Promise<Result<MyEntity, Error>>;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export const CreateThingRepository = createAbstraction<ICreateThingRepository>(
|
|
93
|
-
"MyExtension/CreateThingRepository"
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
export namespace CreateThingRepository {
|
|
97
|
-
export type Interface = ICreateThingRepository;
|
|
392
|
+
export namespace CreateEntityUseCase {
|
|
393
|
+
export type Interface = ICreateEntityUseCase;
|
|
394
|
+
export type Input = ICreateEntityInput;
|
|
98
395
|
}
|
|
99
396
|
```
|
|
100
397
|
|
|
101
398
|
## Feature Registration
|
|
102
399
|
|
|
103
400
|
```ts
|
|
104
|
-
// src/api/features/
|
|
401
|
+
// src/api/features/createEntity/feature.ts
|
|
105
402
|
import { createFeature } from "webiny/api";
|
|
106
|
-
import
|
|
107
|
-
import
|
|
403
|
+
import CreateEntityUseCase from "./CreateEntityUseCase.js";
|
|
404
|
+
import CreateEntityRepository from "./CreateEntityRepository.js";
|
|
108
405
|
|
|
109
|
-
export const
|
|
110
|
-
name: "
|
|
406
|
+
export const CreateEntityFeature = createFeature({
|
|
407
|
+
name: "CreateEntity",
|
|
111
408
|
register(container) {
|
|
112
|
-
container.register(
|
|
113
|
-
container.register(
|
|
409
|
+
container.register(CreateEntityUseCase); // transient (default)
|
|
410
|
+
container.register(CreateEntityRepository).inSingletonScope(); // singleton
|
|
114
411
|
}
|
|
115
412
|
});
|
|
116
413
|
```
|
|
@@ -122,13 +419,13 @@ export const CreateThingFeature = createFeature({
|
|
|
122
419
|
| `container.register(Implementation)` | Register a class (created via `Abstraction.createImplementation`) |
|
|
123
420
|
| `container.registerInstance(abstraction, instance)` | Register a plain object that satisfies the interface |
|
|
124
421
|
| `container.registerFactory(abstraction, () => instance)` | Register a lazy factory |
|
|
422
|
+
| `container.registerDecorator(Decorator)` | Register a decorator (wraps existing implementation) |
|
|
125
423
|
|
|
126
424
|
## Reading API BuildParams
|
|
127
425
|
|
|
128
426
|
A deployed API must **NEVER** use `process.env` to read configuration. All configuration flows through `BuildParams` via DI:
|
|
129
427
|
|
|
130
428
|
```ts
|
|
131
|
-
// Inside an API service — use BuildParams, NEVER process.env
|
|
132
429
|
import { BuildParams } from "webiny/api/build-params";
|
|
133
430
|
|
|
134
431
|
class MyServiceImpl implements MyService.Interface {
|
|
@@ -151,11 +448,200 @@ export default MyService.createImplementation({
|
|
|
151
448
|
|
|
152
449
|
> **Note:** BuildParam _declarations_ (`<Api.BuildParam>`) live in the top-level extension component — see the **webiny-full-stack-architect** skill.
|
|
153
450
|
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Domain Errors
|
|
454
|
+
|
|
455
|
+
Every feature defines domain-specific errors extending `BaseError`:
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
// domain/errors.ts
|
|
459
|
+
import { BaseError } from "@webiny/feature/api";
|
|
460
|
+
|
|
461
|
+
export class EntityNotFoundError extends BaseError {
|
|
462
|
+
override readonly code = "Entity/NotFound" as const;
|
|
463
|
+
|
|
464
|
+
constructor(id: string) {
|
|
465
|
+
super({ message: `Entity with id "${id}" was not found!` });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export class EntityPersistenceError extends BaseError<{ error: Error }> {
|
|
470
|
+
override readonly code = "Entity/Persist" as const;
|
|
471
|
+
|
|
472
|
+
constructor(error: Error) {
|
|
473
|
+
super({ message: error.message, data: { error } });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
**Rules:**
|
|
479
|
+
|
|
480
|
+
- Extend `BaseError` from `@webiny/feature/api`
|
|
481
|
+
- Use `override readonly code` with a namespaced string (`"Domain/ErrorType"`)
|
|
482
|
+
- Use `as const` on the code for type narrowing
|
|
483
|
+
- If passing `data`, define a type and pass it as generic: `BaseError<TDataType>`
|
|
484
|
+
|
|
485
|
+
### Typed Error Unions in Abstractions
|
|
486
|
+
|
|
487
|
+
Define error interfaces and union types so consumers know exactly which errors can occur:
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
// features/createEntity/abstractions.ts
|
|
491
|
+
export interface ICreateEntityErrors {
|
|
492
|
+
persistence: EntityPersistenceError;
|
|
493
|
+
notFound: EntityModelNotFoundError;
|
|
494
|
+
notAuthorized: NotAuthorizedError;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
type CreateEntityError = ICreateEntityErrors[keyof ICreateEntityErrors];
|
|
498
|
+
|
|
499
|
+
export interface ICreateEntityUseCase {
|
|
500
|
+
execute(input: CreateEntityInput): Promise<Result<Entity, CreateEntityError>>;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export namespace CreateEntityUseCase {
|
|
504
|
+
export type Interface = ICreateEntityUseCase;
|
|
505
|
+
export type Input = CreateEntityInput;
|
|
506
|
+
export type Error = CreateEntityError;
|
|
507
|
+
export type Return = Promise<Result<Entity, CreateEntityError>>;
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
- Use case errors are a **superset** of repository errors (use case adds authorization, validation, etc.)
|
|
512
|
+
- Export `Error` and `Return` types in the namespace for consumers
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## Entity / Value Object Patterns
|
|
517
|
+
|
|
518
|
+
### Entity ID Value Object
|
|
519
|
+
|
|
520
|
+
```ts
|
|
521
|
+
// domain/EntityId.ts
|
|
522
|
+
import { EntryId } from "@webiny/api-headless-cms/exports/api/cms/entry.js";
|
|
523
|
+
|
|
524
|
+
export class EntityId {
|
|
525
|
+
static from(id?: string) {
|
|
526
|
+
if (id) {
|
|
527
|
+
return EntryId.from(id).id; // Ensure clean id without revision suffix
|
|
528
|
+
}
|
|
529
|
+
return EntryId.create().id;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### Domain Entity Class
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
// shared/Entity.ts
|
|
538
|
+
export interface EntityDto {
|
|
539
|
+
id: string;
|
|
540
|
+
values: EntityValues;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export class Entity {
|
|
544
|
+
private constructor(private dto: EntityDto) {}
|
|
545
|
+
|
|
546
|
+
static from(dto: EntityDto) {
|
|
547
|
+
return new Entity(dto);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
get id() {
|
|
551
|
+
return this.dto.id;
|
|
552
|
+
}
|
|
553
|
+
get values() {
|
|
554
|
+
return this.dto.values;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
## Public Exports (`index.ts`)
|
|
562
|
+
|
|
563
|
+
Each feature folder exports **only abstractions** — never features, events, or implementations:
|
|
564
|
+
|
|
565
|
+
```ts
|
|
566
|
+
// features/disableEntity/index.ts
|
|
567
|
+
export {
|
|
568
|
+
DisableEntityUseCase,
|
|
569
|
+
EntityBeforeDisableEventHandler,
|
|
570
|
+
EntityAfterDisableEventHandler
|
|
571
|
+
} from "./abstractions.js";
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
**Rules:**
|
|
575
|
+
|
|
576
|
+
- Use `export { }` syntax, NOT `export *`
|
|
577
|
+
- Do NOT export `feature.ts`, `events.ts`, or implementation files
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## Scoping Rules
|
|
582
|
+
|
|
583
|
+
| Layer | Scope | Rationale |
|
|
584
|
+
| -------------- | --------------------- | --------------------------------- |
|
|
585
|
+
| UseCase | Transient (default) | Fresh per invocation |
|
|
586
|
+
| Service | `.inSingletonScope()` | Stateful or expensive to create |
|
|
587
|
+
| Repository | `.inSingletonScope()` | One cache instance |
|
|
588
|
+
| Gateway | `.inSingletonScope()` | Stateless but expensive to create |
|
|
589
|
+
| EventHandler | Transient (default) | Fresh per event |
|
|
590
|
+
| CMS Model | Register normally | Registered once at boot |
|
|
591
|
+
| GraphQL Schema | Register normally | Registered once at boot |
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
## Naming Conventions
|
|
596
|
+
|
|
597
|
+
| Artifact | Pattern | Example |
|
|
598
|
+
| ----------- | --------------------------------------------- | --------------------------------- |
|
|
599
|
+
| Feature dir | `{businessCapability}` (camelCase) | `syncToLingotek`, `createEntity` |
|
|
600
|
+
| UseCase | `{Action}{Entity}UseCase` | `CreateTenantUseCase` |
|
|
601
|
+
| Service | `{Domain}Service` | `LingotekService` |
|
|
602
|
+
| Repository | `{Action}{Entity}Repository` | `CreateTenantRepository` |
|
|
603
|
+
| Event | `{Entity}{Before\|After}{Action}Event` | `TenantBeforeDisableEvent` |
|
|
604
|
+
| Handler | `{Entity}{Before\|After}{Action}EventHandler` | `TenantBeforeDisableEventHandler` |
|
|
605
|
+
| Decorator | `{Action}{Entity}With{Concern}` | `GetEntityByIdWithAuthorization` |
|
|
606
|
+
| Mapper | `EntryTo{Entity}Mapper` | `EntryToFolderMapper` |
|
|
607
|
+
| Error | `{Entity}{Problem}Error` | `EntityNotFoundError` |
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
## Code Conventions
|
|
612
|
+
|
|
613
|
+
- Use `createAbstraction` from `@webiny/feature/api` — never `new Abstraction()`
|
|
614
|
+
- All implementations use `createImplementation` with a `dependencies` array matching constructor order
|
|
615
|
+
- Implementation classes are **not exported** — only the `createImplementation` result (as `default`)
|
|
616
|
+
- One class per file. One named import per line.
|
|
617
|
+
- Use `.js` extensions in all relative imports (ESM)
|
|
618
|
+
- Use `~` alias for package-internal absolute imports
|
|
619
|
+
- All operations return `Result<T, E>`. Check `result.isFail()` before `result.value`
|
|
620
|
+
- Never return `null` — use domain-specific NotFoundError
|
|
621
|
+
- Wrap infrastructure errors in domain errors
|
|
622
|
+
|
|
623
|
+
## Checklist
|
|
624
|
+
|
|
625
|
+
When building a new API feature:
|
|
626
|
+
|
|
627
|
+
- [ ] Domain errors defined extending `BaseError` with `override readonly code`
|
|
628
|
+
- [ ] Abstractions define error interfaces, union types, and namespaces with `Interface` + `Error`
|
|
629
|
+
- [ ] UseCase implements abstraction `.Interface`, uses `createImplementation`
|
|
630
|
+
- [ ] Repository implements abstraction `.Interface`, uses CMS use cases, wraps errors
|
|
631
|
+
- [ ] Feature registers use case (transient) and repository (singleton)
|
|
632
|
+
- [ ] Decorators registered with `container.registerDecorator()`, decoratee is last constructor param
|
|
633
|
+
- [ ] Root Extension registers model, schemas, and features
|
|
634
|
+
- [ ] GraphQL schema implements `GraphQLSchemaFactory.Interface`
|
|
635
|
+
- [ ] Domain events have handler abstractions with `Interface` + `Event` namespace
|
|
636
|
+
- [ ] `index.ts` exports abstractions only — no features, no event classes, no implementations
|
|
637
|
+
- [ ] All relative imports use `.js` extension
|
|
638
|
+
- [ ] One class per file, one import per line
|
|
639
|
+
|
|
154
640
|
## Core APIs
|
|
155
641
|
|
|
156
642
|
### `createAbstraction<T>(name: string)`
|
|
157
643
|
|
|
158
|
-
Creates a typed DI token. The generic `T` is the interface that implementations must satisfy.
|
|
644
|
+
Creates a typed DI token. The generic `T` is the interface that implementations must satisfy.
|
|
159
645
|
|
|
160
646
|
| Import | `import { createAbstraction } from "webiny/api"` |
|
|
161
647
|
| ------- | ------------------------------------------------ |
|
|
@@ -177,13 +663,15 @@ Creates a feature definition that the framework loads as an extension.
|
|
|
177
663
|
3. **Name uniqueness** — feature names must be globally unique; use `"AppName/FeatureName"` convention.
|
|
178
664
|
4. **Constructor param order** — `dependencies` array must match constructor parameter order exactly.
|
|
179
665
|
5. **No `process.env` at runtime** — deployed API services must NEVER read `process.env`. All configuration flows through `BuildParams`.
|
|
180
|
-
6. **Scoping** — use cases = transient (default), repositories = singleton (`.inSingletonScope()`).
|
|
666
|
+
6. **Scoping** — use cases = transient (default), services/repositories = singleton (`.inSingletonScope()`).
|
|
181
667
|
7. **Import extensions** — always use `.js` extensions in import paths (ESM).
|
|
182
668
|
|
|
183
669
|
## Related Skills
|
|
184
670
|
|
|
671
|
+
- **webiny-use-case-pattern** — UseCase implementation, Result handling, error types, decorators, CMS repositories
|
|
672
|
+
- **webiny-api-permissions** — Schema-based permissions, CRUD authorization patterns, own-record scoping, testing
|
|
673
|
+
- **webiny-event-handler-pattern** — EventHandler lifecycle, domain event definition and publishing, handler abstractions
|
|
674
|
+
- **webiny-custom-graphql-api** — GraphQL schema creation, dynamic inputs, namespaced mutations
|
|
675
|
+
- **webiny-v5-to-v6-migration** — Side-by-side migration patterns for AI agents
|
|
185
676
|
- **webiny-full-stack-architect** — Top-level component, shared domain layer, BuildParam declarations
|
|
186
677
|
- **webiny-dependency-injection** — The `createImplementation` DI pattern and injectable services
|
|
187
|
-
- **webiny-custom-graphql-api** — GraphQL schema creation with `GraphQLSchemaFactory`
|
|
188
|
-
- **webiny-use-case-pattern** — UseCase pattern with `Result` type
|
|
189
|
-
- **webiny-event-handler-pattern** — EventHandler lifecycle hooks
|