@webiny/mcp 6.1.0-beta.0 → 6.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/agents/claude.d.ts +2 -0
  2. package/agents/claude.js +6 -0
  3. package/agents/claude.js.map +1 -1
  4. package/agents/cline.d.ts +2 -0
  5. package/agents/cline.js +6 -0
  6. package/agents/cline.js.map +1 -1
  7. package/agents/copilot.d.ts +2 -0
  8. package/agents/copilot.js +7 -0
  9. package/agents/copilot.js.map +1 -1
  10. package/agents/cursor.d.ts +2 -0
  11. package/agents/cursor.js +6 -0
  12. package/agents/cursor.js.map +1 -1
  13. package/agents/discover.d.ts +3 -0
  14. package/agents/discover.js +29 -0
  15. package/agents/discover.js.map +1 -0
  16. package/agents/instructions.d.ts +3 -1
  17. package/agents/instructions.js +15 -2
  18. package/agents/instructions.js.map +1 -1
  19. package/agents/kiro.d.ts +2 -0
  20. package/agents/kiro.js +6 -0
  21. package/agents/kiro.js.map +1 -1
  22. package/agents/opencode.d.ts +2 -0
  23. package/agents/opencode.js +7 -0
  24. package/agents/opencode.js.map +1 -1
  25. package/agents/types.d.ts +16 -0
  26. package/agents/types.js +3 -0
  27. package/agents/types.js.map +1 -0
  28. package/agents/windsurf.d.ts +2 -0
  29. package/agents/windsurf.js +6 -0
  30. package/agents/windsurf.js.map +1 -1
  31. package/cli/ConfigureMcp.js +21 -6
  32. package/cli/ConfigureMcp.js.map +1 -1
  33. package/package.json +3 -3
  34. package/skills/admin/admin-architect/SKILL.md +188 -0
  35. package/skills/admin/admin-permissions/SKILL.md +159 -0
  36. package/skills/api/api-architect/SKILL.md +548 -60
  37. package/skills/api/event-handler-pattern/SKILL.md +191 -23
  38. package/skills/api/graphql-api/SKILL.md +227 -31
  39. package/skills/api/permissions/SKILL.md +291 -0
  40. package/skills/api/use-case-pattern/SKILL.md +347 -12
  41. package/skills/api/v5-to-v6-migration/SKILL.md +416 -0
  42. package/skills/generated/admin/SKILL.md +1 -1
  43. package/skills/generated/admin/aco/SKILL.md +1 -1
  44. package/skills/generated/admin/build-params/SKILL.md +1 -1
  45. package/skills/generated/admin/cms/SKILL.md +1 -1
  46. package/skills/generated/admin/configs/SKILL.md +1 -1
  47. package/skills/generated/admin/env-config/SKILL.md +1 -1
  48. package/skills/generated/admin/form/SKILL.md +1 -1
  49. package/skills/generated/admin/graphql-client/SKILL.md +1 -1
  50. package/skills/generated/admin/lexical/SKILL.md +1 -1
  51. package/skills/generated/admin/local-storage/SKILL.md +1 -1
  52. package/skills/generated/admin/router/SKILL.md +1 -1
  53. package/skills/generated/admin/security/SKILL.md +1 -1
  54. package/skills/generated/admin/tenancy/SKILL.md +1 -1
  55. package/skills/generated/admin/ui/SKILL.md +1 -1
  56. package/skills/generated/admin/website-builder/SKILL.md +1 -1
  57. package/skills/generated/api/SKILL.md +1 -1
  58. package/skills/generated/api/aco/SKILL.md +1 -1
  59. package/skills/generated/api/build-params/SKILL.md +1 -1
  60. package/skills/generated/api/cms/SKILL.md +1 -1
  61. package/skills/generated/api/event-publisher/SKILL.md +1 -1
  62. package/skills/generated/api/file-manager/SKILL.md +1 -1
  63. package/skills/generated/api/graphql/SKILL.md +1 -1
  64. package/skills/generated/api/key-value-store/SKILL.md +1 -1
  65. package/skills/generated/api/logger/SKILL.md +1 -1
  66. package/skills/generated/api/opensearch/SKILL.md +1 -1
  67. package/skills/generated/api/scheduler/SKILL.md +1 -1
  68. package/skills/generated/api/security/SKILL.md +1 -1
  69. package/skills/generated/api/system/SKILL.md +1 -1
  70. package/skills/generated/api/tasks/SKILL.md +1 -1
  71. package/skills/generated/api/tenancy/SKILL.md +1 -1
  72. package/skills/generated/api/tenant-manager/SKILL.md +1 -1
  73. package/skills/generated/api/website-builder/SKILL.md +1 -1
  74. package/skills/generated/cli/SKILL.md +1 -1
  75. package/skills/generated/cli/command/SKILL.md +1 -1
  76. package/skills/generated/extensions/SKILL.md +1 -1
  77. package/skills/generated/infra/SKILL.md +1 -1
@@ -2,115 +2,412 @@
2
2
  name: webiny-api-architect
3
3
  context: webiny-extensions
4
4
  description: >
5
- API-side architecture patterns for Webiny extensions. Use this skill when building
6
- backend features with createFeature, createAbstraction, UseCase/Repository layering,
7
- container registration, and API BuildParams. Covers the api/ directory structure
8
- and DI scoping rules for API extensions.
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` from `webiny/api` to register domain models, GraphQL schemas, and features into the DI container. Each feature is self-contained in its own directory with abstractions, implementations, and a `feature.ts` registration file. The layering is **UseCase Repository**, with use cases as transient and repositories as singletons.
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 # API entry point (createFeature)
22
- ├── domain/ # Domain models, errors, value objects
23
- ├── features/ # One directory per use case
24
- └── CreateThing/
25
- ├── abstractions.ts
26
- ├── CreateThingUseCase.ts
27
- ├── CreateThingRepository.ts
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/ # GraphQL schema definitions
30
- └── CreateThingSchema.ts
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 MyModel from "./domain/MyModel.js";
41
- import CreateThingSchema from "./graphql/CreateThingSchema.js";
42
- import { CreateThingFeature } from "./features/CreateThing/feature.js";
43
- import { GetThingFeature } from "./features/GetThing/feature.js";
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
- // Domain models (CMS content models, etc.)
49
- container.register(MyModel);
350
+ // CMS model (register first)
351
+ container.register(EntityModel);
50
352
 
51
353
  // GraphQL schemas
52
- container.register(CreateThingSchema);
354
+ container.register(CreateEntitySchema);
53
355
 
54
- // Features (use cases + repositories)
55
- CreateThingFeature.register(container);
56
- GetThingFeature.register(container);
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/CreateThing/abstractions.ts
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 ICreateThingInput {
379
+ export interface ICreateEntityInput {
71
380
  name: string;
72
381
  }
73
382
 
74
- export interface ICreateThingUseCase {
75
- execute(input: ICreateThingInput): Promise<Result<MyEntity, Error>>;
383
+ export interface ICreateEntityUseCase {
384
+ execute(input: ICreateEntityInput): Promise<Result<MyEntity, Error>>;
76
385
  }
77
386
 
78
- export const CreateThingUseCase = createAbstraction<ICreateThingUseCase>(
79
- "MyExtension/CreateThingUseCase"
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 CreateThingUseCase {
84
- export type Interface = ICreateThingUseCase;
85
- export type Input = ICreateThingInput;
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/CreateThing/feature.ts
401
+ // src/api/features/createEntity/feature.ts
105
402
  import { createFeature } from "webiny/api";
106
- import CreateThingUseCase from "./CreateThingUseCase.js";
107
- import CreateThingRepository from "./CreateThingRepository.js";
403
+ import CreateEntityUseCase from "./CreateEntityUseCase.js";
404
+ import CreateEntityRepository from "./CreateEntityRepository.js";
108
405
 
109
- export const CreateThingFeature = createFeature({
110
- name: "CreateThing",
406
+ export const CreateEntityFeature = createFeature({
407
+ name: "CreateEntity",
111
408
  register(container) {
112
- container.register(CreateThingUseCase); // transient (default)
113
- container.register(CreateThingRepository).inSingletonScope();
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. The `name` string is used for debugging and error messages.
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