@webiny/mcp 6.0.0 → 6.1.0-beta.1

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 (101) hide show
  1. package/agents/claude.d.ts +2 -2
  2. package/agents/claude.js.map +1 -1
  3. package/agents/cline.d.ts +2 -2
  4. package/agents/cline.js.map +1 -1
  5. package/agents/copilot.d.ts +2 -2
  6. package/agents/copilot.js +1 -1
  7. package/agents/copilot.js.map +1 -1
  8. package/agents/cursor.d.ts +2 -2
  9. package/agents/cursor.js.map +1 -1
  10. package/agents/instructions.js +1 -1
  11. package/agents/instructions.js.map +1 -1
  12. package/agents/kiro.d.ts +15 -0
  13. package/agents/kiro.js +33 -0
  14. package/agents/kiro.js.map +1 -0
  15. package/agents/opencode.d.ts +22 -0
  16. package/agents/opencode.js +76 -0
  17. package/agents/opencode.js.map +1 -0
  18. package/agents/shared.d.ts +5 -5
  19. package/agents/shared.js +3 -3
  20. package/agents/shared.js.map +1 -1
  21. package/agents/windsurf.d.ts +2 -2
  22. package/agents/windsurf.js.map +1 -1
  23. package/bin.d.ts +2 -0
  24. package/bin.js +4 -0
  25. package/bin.js.map +1 -0
  26. package/cli/ConfigureMcp.d.ts +7 -14
  27. package/cli/ConfigureMcp.js +24 -52
  28. package/cli/ConfigureMcp.js.map +1 -1
  29. package/cli/McpServer.d.ts +3 -10
  30. package/cli/McpServer.js +83 -109
  31. package/cli/McpServer.js.map +1 -1
  32. package/cli.d.ts +1 -0
  33. package/cli.js +60 -0
  34. package/cli.js.map +1 -0
  35. package/index.d.ts +6 -1
  36. package/index.js +7 -1
  37. package/index.js.map +1 -1
  38. package/package.json +11 -23
  39. package/skills/admin/admin-architect/SKILL.md +389 -0
  40. package/skills/admin/ui-extensions/SKILL.md +268 -0
  41. package/skills/api/api-architect/SKILL.md +189 -0
  42. package/skills/api/custom-field-type/SKILL.md +263 -0
  43. package/skills/api/event-handler-pattern/SKILL.md +131 -0
  44. package/skills/{custom-graphql-api → api/graphql-api}/SKILL.md +3 -3
  45. package/skills/api/use-case-pattern/SKILL.md +102 -0
  46. package/skills/cli-extensions/SKILL.md +45 -47
  47. package/skills/configure-auth0/SKILL.md +4 -4
  48. package/skills/configure-okta/SKILL.md +3 -3
  49. package/skills/content-models/SKILL.md +197 -196
  50. package/skills/dependency-injection/SKILL.md +9 -219
  51. package/skills/full-stack-architect/SKILL.md +195 -0
  52. package/skills/generated/admin/SKILL.md +119 -0
  53. package/skills/generated/admin/aco/SKILL.md +28 -0
  54. package/skills/generated/admin/build-params/SKILL.md +33 -0
  55. package/skills/generated/admin/cms/SKILL.md +342 -0
  56. package/skills/generated/admin/configs/SKILL.md +23 -0
  57. package/skills/generated/admin/env-config/SKILL.md +30 -0
  58. package/skills/generated/admin/form/SKILL.md +88 -0
  59. package/skills/generated/admin/graphql-client/SKILL.md +23 -0
  60. package/skills/generated/admin/lexical/SKILL.md +105 -0
  61. package/skills/generated/admin/local-storage/SKILL.md +42 -0
  62. package/skills/generated/admin/router/SKILL.md +48 -0
  63. package/skills/generated/admin/security/SKILL.md +63 -0
  64. package/skills/generated/admin/tenancy/SKILL.md +64 -0
  65. package/skills/generated/admin/ui/SKILL.md +468 -0
  66. package/skills/generated/admin/website-builder/SKILL.md +318 -0
  67. package/skills/generated/api/SKILL.md +40 -0
  68. package/skills/generated/api/aco/SKILL.md +202 -0
  69. package/skills/generated/api/build-params/SKILL.md +31 -0
  70. package/skills/generated/api/cms/SKILL.md +646 -0
  71. package/skills/generated/api/event-publisher/SKILL.md +31 -0
  72. package/skills/generated/api/file-manager/SKILL.md +189 -0
  73. package/skills/generated/api/graphql/SKILL.md +61 -0
  74. package/skills/generated/api/key-value-store/SKILL.md +31 -0
  75. package/skills/generated/api/logger/SKILL.md +25 -0
  76. package/skills/generated/api/opensearch/SKILL.md +39 -0
  77. package/skills/generated/api/scheduler/SKILL.md +112 -0
  78. package/skills/generated/api/security/SKILL.md +317 -0
  79. package/skills/generated/api/system/SKILL.md +34 -0
  80. package/skills/generated/api/tasks/SKILL.md +31 -0
  81. package/skills/generated/api/tenancy/SKILL.md +124 -0
  82. package/skills/generated/api/tenant-manager/SKILL.md +34 -0
  83. package/skills/generated/api/website-builder/SKILL.md +356 -0
  84. package/skills/generated/cli/SKILL.md +28 -0
  85. package/skills/generated/cli/command/SKILL.md +24 -0
  86. package/skills/generated/extensions/SKILL.md +43 -0
  87. package/skills/generated/infra/SKILL.md +190 -0
  88. package/skills/infrastructure-extensions/SKILL.md +3 -2
  89. package/skills/local-development/SKILL.md +2 -28
  90. package/skills/project-structure/SKILL.md +78 -56
  91. package/skills/webiny-sdk/SKILL.md +77 -76
  92. package/skills/website-builder/SKILL.md +143 -149
  93. package/ui.d.ts +24 -0
  94. package/ui.js +31 -0
  95. package/ui.js.map +1 -0
  96. package/Extension.d.ts +0 -2
  97. package/Extension.js +0 -11
  98. package/Extension.js.map +0 -1
  99. package/skills/admin-ui-extensions/SKILL.md +0 -267
  100. package/skills/api-custom-feature/SKILL.md +0 -195
  101. package/skills/lifecycle-events/SKILL.md +0 -348
@@ -0,0 +1,189 @@
1
+ ---
2
+ name: webiny-api-architect
3
+ context: webiny-extensions
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.
9
+ ---
10
+
11
+ # API Architecture Patterns
12
+
13
+ ## TL;DR
14
+
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
+
17
+ ## API Directory Structure
18
+
19
+ ```
20
+ 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
28
+ │ └── feature.ts
29
+ └── graphql/ # GraphQL schema definitions
30
+ └── CreateThingSchema.ts
31
+ ```
32
+
33
+ ## API Extension Entry Point
34
+
35
+ The API entry point uses `createFeature` to register all backend components into the DI container:
36
+
37
+ ```ts
38
+ // src/api/Extension.ts
39
+ 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";
44
+
45
+ export const Extension = createFeature({
46
+ name: "MyExtension",
47
+ register(container) {
48
+ // Domain models (CMS content models, etc.)
49
+ container.register(MyModel);
50
+
51
+ // GraphQL schemas
52
+ container.register(CreateThingSchema);
53
+
54
+ // Features (use cases + repositories)
55
+ CreateThingFeature.register(container);
56
+ GetThingFeature.register(container);
57
+ }
58
+ });
59
+ ```
60
+
61
+ ## Abstractions
62
+
63
+ Every piece of business logic starts with a typed abstraction token:
64
+
65
+ ```ts
66
+ // src/api/features/CreateThing/abstractions.ts
67
+ import { createAbstraction, Result } from "webiny/api";
68
+ import type { MyEntity } from "~/shared/MyEntity.js";
69
+
70
+ export interface ICreateThingInput {
71
+ name: string;
72
+ }
73
+
74
+ export interface ICreateThingUseCase {
75
+ execute(input: ICreateThingInput): Promise<Result<MyEntity, Error>>;
76
+ }
77
+
78
+ export const CreateThingUseCase = createAbstraction<ICreateThingUseCase>(
79
+ "MyExtension/CreateThingUseCase"
80
+ );
81
+
82
+ // 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;
98
+ }
99
+ ```
100
+
101
+ ## Feature Registration
102
+
103
+ ```ts
104
+ // src/api/features/CreateThing/feature.ts
105
+ import { createFeature } from "webiny/api";
106
+ import CreateThingUseCase from "./CreateThingUseCase.js";
107
+ import CreateThingRepository from "./CreateThingRepository.js";
108
+
109
+ export const CreateThingFeature = createFeature({
110
+ name: "CreateThing",
111
+ register(container) {
112
+ container.register(CreateThingUseCase); // transient (default)
113
+ container.register(CreateThingRepository).inSingletonScope();
114
+ }
115
+ });
116
+ ```
117
+
118
+ ## Container Registration Methods
119
+
120
+ | Method | When to Use |
121
+ | -------------------------------------------------------- | ----------------------------------------------------------------- |
122
+ | `container.register(Implementation)` | Register a class (created via `Abstraction.createImplementation`) |
123
+ | `container.registerInstance(abstraction, instance)` | Register a plain object that satisfies the interface |
124
+ | `container.registerFactory(abstraction, () => instance)` | Register a lazy factory |
125
+
126
+ ## Reading API BuildParams
127
+
128
+ A deployed API must **NEVER** use `process.env` to read configuration. All configuration flows through `BuildParams` via DI:
129
+
130
+ ```ts
131
+ // Inside an API service — use BuildParams, NEVER process.env
132
+ import { BuildParams } from "webiny/api/build-params";
133
+
134
+ class MyServiceImpl implements MyService.Interface {
135
+ constructor(private buildParams: BuildParams.Interface) {}
136
+
137
+ doSomething() {
138
+ // buildParams.get() returns T | null — always handle null
139
+ const endpoint = this.buildParams.get<string>("MY_API_ENDPOINT");
140
+ if (!endpoint) {
141
+ throw new Error("MY_API_ENDPOINT build param is not configured.");
142
+ }
143
+ }
144
+ }
145
+
146
+ export default MyService.createImplementation({
147
+ implementation: MyServiceImpl,
148
+ dependencies: [BuildParams]
149
+ });
150
+ ```
151
+
152
+ > **Note:** BuildParam _declarations_ (`<Api.BuildParam>`) live in the top-level extension component — see the **webiny-full-stack-architect** skill.
153
+
154
+ ## Core APIs
155
+
156
+ ### `createAbstraction<T>(name: string)`
157
+
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.
159
+
160
+ | Import | `import { createAbstraction } from "webiny/api"` |
161
+ | ------- | ------------------------------------------------ |
162
+ | Returns | `Abstraction<T>` |
163
+
164
+ ### `createFeature(def)`
165
+
166
+ Creates a feature definition that the framework loads as an extension.
167
+
168
+ | Import | `import { createFeature } from "webiny/api"` |
169
+ | ------------------------- | --------------------------------------------------------- |
170
+ | `def.name` | Unique feature name (convention: `"AppName/FeatureName"`) |
171
+ | `def.register(container)` | Called at startup with the DI `Container` instance |
172
+
173
+ ## Key Rules
174
+
175
+ 1. **Abstractions first** — any new business logic MUST be encapsulated in `createAbstraction` + `createFeature`. Never put logic directly in an EventHandler, GraphQL resolver, or CLI command.
176
+ 2. **Namespace convention** — every abstraction exports `namespace MyAbstraction { export type Interface = ...; }` so consumers can type dependencies as `MyAbstraction.Interface`.
177
+ 3. **Name uniqueness** — feature names must be globally unique; use `"AppName/FeatureName"` convention.
178
+ 4. **Constructor param order** — `dependencies` array must match constructor parameter order exactly.
179
+ 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()`).
181
+ 7. **Import extensions** — always use `.js` extensions in import paths (ESM).
182
+
183
+ ## Related Skills
184
+
185
+ - **webiny-full-stack-architect** — Top-level component, shared domain layer, BuildParam declarations
186
+ - **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
@@ -0,0 +1,263 @@
1
+ ---
2
+ name: webiny-api-cms-custom-field-type
3
+ context: webiny-extensions
4
+ description: >
5
+ How to implement a custom CMS field type that integrates with the model builder's
6
+ fluent API. Covers extending DataFieldBuilder, composing validator interfaces,
7
+ creating a FieldTypeFactory, registering via DI, and module augmentation for
8
+ TypeScript autocomplete on the fields() registry.
9
+ ---
10
+
11
+ # Custom CMS Field Type
12
+
13
+ ## TL;DR
14
+
15
+ A custom field type is a class that extends `DataFieldBuilder<"yourType">`, paired with a factory class implementing `FieldType.Factory`. Register the factory with `container.register(YourFieldType)`. Add a module augmentation on `"webiny/api/cms/model"` so the `fields` registry method gets TypeScript autocomplete.
16
+
17
+ ## When to Use This
18
+
19
+ Use a custom field type when:
20
+
21
+ - You need a field with a storage format or validation logic not covered by the built-in types (`text`, `number`, `boolean`, `datetime`, `file`, `ref`, `object`, `richText`, `longText`, `json`, `dynamicZone`)
22
+ - You want to expose a fluent builder API (e.g., `fields.slug()`, `fields.color()`) in `ModelFactory` implementations
23
+
24
+ ## Field Type Structure
25
+
26
+ A custom field type consists of three parts:
27
+
28
+ 1. **Builder interface** — extends `DataFieldBuilder<"type">` plus `FieldTypeValidator.*` types
29
+ 2. **Builder class** — implements the interface, calls `this.validation()` for each validator
30
+ 3. **Factory class** — implements `FieldType.Factory`, creates builder instances
31
+
32
+ As a standalone extension (not part of a larger feature), the directory layout is:
33
+
34
+ ```
35
+ extensions/
36
+ └── SlugFieldType/
37
+ ├── SlugFieldType.ts # builder interface, builder class, factory class
38
+ └── feature.ts # createFeature — registers the factory into the DI container
39
+ ```
40
+
41
+ `feature.ts`:
42
+
43
+ ```ts
44
+ // extensions/SlugFieldType/feature.ts
45
+ import { createFeature } from "webiny/api";
46
+ import { SlugFieldType } from "./SlugFieldType.js";
47
+
48
+ export const SlugFieldTypeFeature = createFeature({
49
+ name: "SlugFieldType",
50
+ register(container) {
51
+ container.register(SlugFieldType);
52
+ }
53
+ });
54
+ ```
55
+
56
+ Register in the API entry point:
57
+
58
+ ```ts
59
+ // api/Extension.ts
60
+ import { createFeature } from "webiny/api";
61
+ import { SlugFieldTypeFeature } from "~/extensions/SlugFieldType/feature.js";
62
+
63
+ export const Extension = createFeature({
64
+ name: "MyExtension",
65
+ register(container) {
66
+ SlugFieldTypeFeature.register(container);
67
+ }
68
+ });
69
+ ```
70
+
71
+ ## Complete Example
72
+
73
+ ```ts
74
+ // extensions/SlugFieldType/SlugFieldType.ts
75
+ import { DataFieldBuilder, FieldType } from "webiny/api/cms/model";
76
+ import type { FieldTypeValidator } from "webiny/api/cms/model";
77
+
78
+ // 1. Builder interface — extends DataFieldBuilder + desired FieldTypeValidator types
79
+ export interface ISlugFieldBuilder
80
+ extends
81
+ DataFieldBuilder<"slug">,
82
+ FieldTypeValidator.Required,
83
+ FieldTypeValidator.Pattern,
84
+ FieldTypeValidator.Unique {}
85
+
86
+ // 2. Module augmentation — adds fields.slug() to the registry
87
+ declare module "webiny/api/cms/model" {
88
+ interface IFieldBuilderRegistry {
89
+ slug(): ISlugFieldBuilder;
90
+ }
91
+
92
+ interface IFieldRendererRegistry {
93
+ myCustomRenderer: {
94
+ fieldType: "text" | "number";
95
+ settings: undefined;
96
+ };
97
+ }
98
+ }
99
+
100
+ // 3. Builder class — implements each validator method via this.validation()
101
+ class SlugFieldBuilder extends DataFieldBuilder<"slug"> implements ISlugFieldBuilder {
102
+ constructor() {
103
+ super("slug");
104
+ }
105
+
106
+ required(message?: string): this {
107
+ return this.validation({
108
+ name: "required",
109
+ message: message || "Value is required.",
110
+ settings: {}
111
+ });
112
+ }
113
+
114
+ pattern(regex: string, flags = "", message?: string): this {
115
+ return this.validation({
116
+ name: "pattern",
117
+ message: message || "Invalid value.",
118
+ settings: { preset: "custom", regex, flags }
119
+ });
120
+ }
121
+
122
+ unique(message?: string): this {
123
+ return this.validation({
124
+ name: "unique",
125
+ message: message || "Value must be unique.",
126
+ settings: {}
127
+ });
128
+ }
129
+ }
130
+
131
+ // 4. Factory class — implements FieldType.Factory
132
+ class SlugFieldTypeFactory implements FieldType.Factory {
133
+ readonly type = "slug";
134
+
135
+ create(): ISlugFieldBuilder {
136
+ return new SlugFieldBuilder();
137
+ }
138
+ }
139
+
140
+ // 5. Export as a FieldType implementation
141
+ export const SlugFieldType = FieldType.createImplementation({
142
+ implementation: SlugFieldTypeFactory,
143
+ dependencies: []
144
+ });
145
+ ```
146
+
147
+ ## Using the Custom Field in a Model
148
+
149
+ After registration, `fields.slug()` is available in any `ModelFactory` implementation:
150
+
151
+ ```ts
152
+ import { ModelFactory } from "webiny/api/cms/model";
153
+
154
+ class ProductModelImpl implements ModelFactory.Interface {
155
+ async execute(builder: ModelFactory.Builder) {
156
+ return [
157
+ builder
158
+ .public({ modelId: "product", name: "Product", group: "ungrouped" })
159
+ .fields(fields => ({
160
+ name: fields.text().label("Name").required(),
161
+ slug: fields
162
+ .slug()
163
+ .label("Slug")
164
+ .required("Slug is required.")
165
+ .unique()
166
+ .pattern("^[a-z0-9-]+$", "", "Only lowercase letters, numbers, and hyphens.")
167
+ }))
168
+ .layout([["name", "slug"]])
169
+ .titleFieldId("name")
170
+ .singularApiName("Product")
171
+ .pluralApiName("Products")
172
+ ];
173
+ }
174
+ }
175
+ ```
176
+
177
+ ## DataFieldBuilder API
178
+
179
+ All methods return `this` for chaining.
180
+
181
+ | Method | Description |
182
+ | --------------------------- | --------------------------------------------- |
183
+ | `label(text)` | Field label shown in the Admin editor |
184
+ | `help(text)` | Help text shown below the field |
185
+ | `description(text)` | Field description |
186
+ | `fieldId(id)` | Override the auto-derived field ID |
187
+ | `storageId(id)` | Override the storage identifier |
188
+ | `placeholder(text)` | Placeholder text for the input |
189
+ | `defaultValue(value)` | Default value for new entries |
190
+ | `list()` | Make the field accept multiple values (array) |
191
+ | `listMinLength(n, msg?)` | Minimum number of list items |
192
+ | `listMaxLength(n, msg?)` | Maximum number of list items |
193
+ | `tags(tags)` | Arbitrary tags for filtering/querying |
194
+ | `renderer(name, settings?)` | Set the Admin UI renderer |
195
+ | `settings(settings)` | Set arbitrary field settings |
196
+
197
+ ### Protected Methods (for use inside validator implementations only)
198
+
199
+ | Method | Description |
200
+ | --------------------------- | ------------------------------------------------------------------ |
201
+ | `this.validation(rule)` | Append a `CmsModelFieldValidation` to the field's validation array |
202
+ | `this.listValidation(rule)` | Append a `CmsModelFieldValidation` to the list validation array |
203
+
204
+ A `CmsModelFieldValidation` has the shape:
205
+
206
+ ```ts
207
+ {
208
+ name: string; // validator name (e.g., "required", "minLength", "pattern")
209
+ message: string; // error message shown to the user
210
+ settings: Record<string, any>; // validator-specific config
211
+ }
212
+ ```
213
+
214
+ ## Available Validators
215
+
216
+ Import via `import type { FieldTypeValidator } from "webiny/api/cms/model"` and extend your builder interface with them. Each type adds one method to your interface:
217
+
218
+ | Type | Method signature |
219
+ | ----------------------------------- | ---------------------------------- |
220
+ | `FieldTypeValidator.Required` | `required(message?)` |
221
+ | `FieldTypeValidator.Unique` | `unique(message?)` |
222
+ | `FieldTypeValidator.MinLength` | `minLength(value, message?)` |
223
+ | `FieldTypeValidator.MaxLength` | `maxLength(value, message?)` |
224
+ | `FieldTypeValidator.Pattern` | `pattern(regex, flags?, message?)` |
225
+ | `FieldTypeValidator.Email` | `email(message?)` |
226
+ | `FieldTypeValidator.Url` | `url(message?)` |
227
+ | `FieldTypeValidator.LowerCase` | `lowerCase(message?)` |
228
+ | `FieldTypeValidator.UpperCase` | `upperCase(message?)` |
229
+ | `FieldTypeValidator.LowerCaseSpace` | `lowerCaseSpace(message?)` |
230
+ | `FieldTypeValidator.UpperCaseSpace` | `upperCaseSpace(message?)` |
231
+ | `FieldTypeValidator.Gte` | `gte(value, message?)` |
232
+ | `FieldTypeValidator.Lte` | `lte(value, message?)` |
233
+ | `FieldTypeValidator.DateGte` | `dateGte(value, message?)` |
234
+ | `FieldTypeValidator.DateLte` | `dateLte(value, message?)` |
235
+ | `FieldTypeValidator.ListMinLength` | `listMinLength(value, message?)` |
236
+ | `FieldTypeValidator.ListMaxLength` | `listMaxLength(value, message?)` |
237
+
238
+ When implementing a validator method in the builder class, call `this.validation()` with the appropriate `name` and `settings`. For `ListMinLength`/`ListMaxLength`, call `this.listValidation()` instead. The `settings` shapes:
239
+
240
+ | Validator | `name` | `settings` |
241
+ | ---------------------------- | ----------------------------- | ---------------------------------------------------------------------------- |
242
+ | Required, Unique | `"required"` / `"unique"` | `{}` |
243
+ | MinLength, MaxLength | `"minLength"` / `"maxLength"` | `{ value: String(n) }` |
244
+ | Gte, Lte | `"gte"` / `"lte"` | `{ value: String(n) }` |
245
+ | DateGte, DateLte | `"dateGte"` / `"dateLte"` | `{ value }` |
246
+ | Pattern | `"pattern"` | `{ preset: "custom", regex, flags }` |
247
+ | Email | `"pattern"` | `{ preset: "email", regex: null, flags: null }` |
248
+ | Url | `"pattern"` | `{ preset: "url", regex: null, flags: null }` |
249
+ | LowerCase / UpperCase / etc. | `"pattern"` | `{ preset: "lowerCase"` / `"upperCase"` / etc., `regex: null, flags: null }` |
250
+
251
+ ## Key Rules
252
+
253
+ 1. **`type` string must be unique** — the factory's `readonly type` must not collide with any built-in type (`text`, `number`, `boolean`, `datetime`, `file`, `ref`, `object`, `richText`, `longText`, `json`, `dynamicZone`) or other custom types.
254
+ 2. **Module augmentation target** — augment `"webiny/api/cms/model"` using `namespace FieldBuilderRegistry { interface Interface { yourType(): IYourFieldBuilder; } }`.
255
+ 3. **`validation()` is protected** — never call it from outside the builder class. Expose validators as named methods on the interface (e.g., `required()`, `minLength()`).
256
+ 4. **`dependencies: []`** — field type factories have no DI dependencies; always pass an empty array.
257
+ 5. **Registration order** — register custom `FieldType` implementations before `FieldBuilderRegistry` is resolved (i.e., in the same `register()` call or before it runs). The registry collects all `FieldType` instances at construction time.
258
+
259
+ ## Related Skills
260
+
261
+ - **webiny-api-cms-content-models** — Using the model builder's fluent API to define CMS models
262
+ - **webiny-api-cms-catalog** — Full catalog of CMS abstractions including `ModelFactory`, `FieldType`, `DataFieldBuilder`
263
+ - **webiny-dependency-injection** — The `createImplementation` pattern and DI scoping
@@ -0,0 +1,131 @@
1
+ ---
2
+ name: webiny-event-handler-pattern
3
+ context: webiny-api
4
+ description: >
5
+ Generic EventHandler implementation pattern — handle method, event payloads, filtering, DI.
6
+ Use this skill to understand how to implement any Webiny EventHandler (before/after hooks).
7
+ ---
8
+
9
+ # EventHandler Pattern
10
+
11
+ ## What It Is
12
+
13
+ An **EventHandler** reacts to domain events in the Webiny lifecycle (e.g., `EntryBeforeCreateEventHandler`, `TenantAfterDeleteEventHandler`). Each handler is a DI abstraction with a single `handle` method.
14
+
15
+ ## Naming Convention
16
+
17
+ - `{Entity}Before{Operation}EventHandler` — fires before persistence, can validate/transform/reject
18
+ - `{Entity}After{Operation}EventHandler` — fires after persistence, for side effects
19
+
20
+ ## Interface Shape
21
+
22
+ Every EventHandler follows this pattern:
23
+
24
+ ```ts
25
+ interface SomeEventHandler.Interface {
26
+ handle(event: SomeEventHandler.Event): Promise<void>;
27
+ }
28
+ ```
29
+
30
+ The `Event` is a `DomainEvent<Payload>` where the payload contains the entity and input data.
31
+
32
+ ## Architecture Rule: Always Wrap Logic in a Reusable Abstraction (MANDATORY)
33
+
34
+ **Never put business logic directly inside an EventHandler.** EventHandlers are thin orchestrators — they receive an event and delegate to an injected service (abstraction). The real logic lives in a dedicated service defined with `createAbstraction` and `createFeature`.
35
+
36
+ **Why:** Inline handler logic cannot be reused by other handlers, GraphQL resolvers, or CLI commands. Wrapping it in an abstraction makes it injectable, testable, and replaceable.
37
+
38
+ **Always follow this structure:**
39
+
40
+ ```
41
+ features/
42
+ ├── myService/ ← the reusable abstraction
43
+ │ ├── abstractions.ts
44
+ │ ├── feature.ts
45
+ │ └── MyService.ts
46
+ └── myHandler/ ← thin handler that injects the service
47
+ ├── feature.ts
48
+ └── MyHandler.ts
49
+ ```
50
+
51
+ The EventHandler feature and the service feature are **registered separately** in `Extension.tsx`.
52
+
53
+ ## How to Implement
54
+
55
+ ```ts
56
+ import { SomeEventHandler } from "webiny/api/<category>";
57
+ import { MyService } from "../myService/abstractions.js";
58
+
59
+ // ✅ Handler is a thin orchestrator — no business logic here
60
+ class MyHandler implements SomeEventHandler.Interface {
61
+ constructor(private myService: MyService.Interface) {}
62
+
63
+ async handle(event: SomeEventHandler.Event) {
64
+ const { entity } = event.payload;
65
+
66
+ // For CMS handlers: always filter by model
67
+ // if (entity.modelId !== "myModel") return;
68
+
69
+ await this.myService.doWork(entity);
70
+ }
71
+ }
72
+
73
+ export default SomeEventHandler.createImplementation({
74
+ implementation: MyHandler,
75
+ dependencies: [MyService]
76
+ });
77
+ ```
78
+
79
+ See **webiny-api-architect** for how to define `MyService` as a proper abstraction.
80
+
81
+ ## Injecting Dependencies
82
+
83
+ EventHandlers can depend on UseCases, platform services, or your own custom abstractions:
84
+
85
+ ```ts
86
+ import { SomeEventHandler } from "webiny/api/<category>";
87
+ import { SomeUseCase } from "webiny/api/<category>";
88
+
89
+ class MyHandler implements SomeEventHandler.Interface {
90
+ constructor(private someUseCase: SomeUseCase.Interface) {}
91
+
92
+ async handle(event: SomeEventHandler.Event) {
93
+ const result = await this.someUseCase.execute({
94
+ /* ... */
95
+ });
96
+ }
97
+ }
98
+
99
+ export default SomeEventHandler.createImplementation({
100
+ implementation: MyHandler,
101
+ dependencies: [SomeUseCase]
102
+ });
103
+ ```
104
+
105
+ ## Registration
106
+
107
+ ```tsx
108
+ <Api.Extension src={"@/extensions/my-handler.ts"} />
109
+ ```
110
+
111
+ Deploy with: `yarn webiny deploy api --env=dev`
112
+
113
+ ## Resolving Types (MANDATORY)
114
+
115
+ **Before writing any code that accesses event payload properties or domain types (CmsEntry, CmsModel, etc.), you MUST read the source file listed in the catalog's `Source` field to verify the exact property names and types. Do not assume or guess property names from memory.**
116
+
117
+ To see the exact event payload types for a specific EventHandler:
118
+
119
+ 1. Read the `abstractions.ts` file from the catalog `Source` path — it contains the payload interface with all property names and types.
120
+ 2. Read the `events.ts` file (sibling to `abstractions.ts`) — it contains the `Interface` and `Event` type aliases.
121
+ 3. If the payload references domain types (e.g., `CmsEntry`, `CmsModel`), follow the import and read that type declaration too.
122
+
123
+ Only use properties that are confirmed to exist in the source type declarations.
124
+
125
+ ## Key Rules
126
+
127
+ - **Before handlers**: payload may be mutable — write to it to set computed fields. Throw to reject the operation.
128
+ - **After handlers**: payload reflects persisted state — do not mutate. Use for side effects.
129
+ - **Filter by entity**: handlers fire for ALL entities of that type. Always check `modelId`, `entity type`, etc.
130
+ - DI constructor parameter order must match the `dependencies` array order exactly
131
+ - Use `.js` extensions in import paths (ES modules)
@@ -204,6 +204,6 @@ Deploy: yarn webiny deploy api --env=dev
204
204
 
205
205
  ## Related Skills
206
206
 
207
- - `api-custom-feature` -- Define custom abstractions and services consumed by resolvers
208
- - `dependency-injection` -- Full DI reference for all injectable services
209
- - `project-structure` -- How to register extensions in `webiny.config.tsx`
207
+ - `webiny-api-architect` -- Define custom abstractions, features, and services consumed by resolvers
208
+ - `webiny-dependency-injection` -- Full DI reference for all injectable services
209
+ - `webiny-project-structure` -- How to register extensions in `webiny.config.tsx`