@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.
- package/agents/claude.d.ts +2 -2
- package/agents/claude.js.map +1 -1
- package/agents/cline.d.ts +2 -2
- package/agents/cline.js.map +1 -1
- package/agents/copilot.d.ts +2 -2
- package/agents/copilot.js +1 -1
- package/agents/copilot.js.map +1 -1
- package/agents/cursor.d.ts +2 -2
- package/agents/cursor.js.map +1 -1
- package/agents/instructions.js +1 -1
- package/agents/instructions.js.map +1 -1
- package/agents/kiro.d.ts +15 -0
- package/agents/kiro.js +33 -0
- package/agents/kiro.js.map +1 -0
- package/agents/opencode.d.ts +22 -0
- package/agents/opencode.js +76 -0
- package/agents/opencode.js.map +1 -0
- package/agents/shared.d.ts +5 -5
- package/agents/shared.js +3 -3
- package/agents/shared.js.map +1 -1
- package/agents/windsurf.d.ts +2 -2
- package/agents/windsurf.js.map +1 -1
- package/bin.d.ts +2 -0
- package/bin.js +4 -0
- package/bin.js.map +1 -0
- package/cli/ConfigureMcp.d.ts +7 -14
- package/cli/ConfigureMcp.js +24 -52
- package/cli/ConfigureMcp.js.map +1 -1
- package/cli/McpServer.d.ts +3 -10
- package/cli/McpServer.js +83 -109
- package/cli/McpServer.js.map +1 -1
- package/cli.d.ts +1 -0
- package/cli.js +60 -0
- package/cli.js.map +1 -0
- package/index.d.ts +6 -1
- package/index.js +7 -1
- package/index.js.map +1 -1
- package/package.json +11 -23
- package/skills/admin/admin-architect/SKILL.md +389 -0
- package/skills/admin/ui-extensions/SKILL.md +268 -0
- package/skills/api/api-architect/SKILL.md +189 -0
- package/skills/api/custom-field-type/SKILL.md +263 -0
- package/skills/api/event-handler-pattern/SKILL.md +131 -0
- package/skills/{custom-graphql-api → api/graphql-api}/SKILL.md +3 -3
- package/skills/api/use-case-pattern/SKILL.md +102 -0
- package/skills/cli-extensions/SKILL.md +45 -47
- package/skills/configure-auth0/SKILL.md +4 -4
- package/skills/configure-okta/SKILL.md +3 -3
- package/skills/content-models/SKILL.md +197 -196
- package/skills/dependency-injection/SKILL.md +9 -219
- package/skills/full-stack-architect/SKILL.md +195 -0
- package/skills/generated/admin/SKILL.md +119 -0
- package/skills/generated/admin/aco/SKILL.md +28 -0
- package/skills/generated/admin/build-params/SKILL.md +33 -0
- package/skills/generated/admin/cms/SKILL.md +342 -0
- package/skills/generated/admin/configs/SKILL.md +23 -0
- package/skills/generated/admin/env-config/SKILL.md +30 -0
- package/skills/generated/admin/form/SKILL.md +88 -0
- package/skills/generated/admin/graphql-client/SKILL.md +23 -0
- package/skills/generated/admin/lexical/SKILL.md +105 -0
- package/skills/generated/admin/local-storage/SKILL.md +42 -0
- package/skills/generated/admin/router/SKILL.md +48 -0
- package/skills/generated/admin/security/SKILL.md +63 -0
- package/skills/generated/admin/tenancy/SKILL.md +64 -0
- package/skills/generated/admin/ui/SKILL.md +468 -0
- package/skills/generated/admin/website-builder/SKILL.md +318 -0
- package/skills/generated/api/SKILL.md +40 -0
- package/skills/generated/api/aco/SKILL.md +202 -0
- package/skills/generated/api/build-params/SKILL.md +31 -0
- package/skills/generated/api/cms/SKILL.md +646 -0
- package/skills/generated/api/event-publisher/SKILL.md +31 -0
- package/skills/generated/api/file-manager/SKILL.md +189 -0
- package/skills/generated/api/graphql/SKILL.md +61 -0
- package/skills/generated/api/key-value-store/SKILL.md +31 -0
- package/skills/generated/api/logger/SKILL.md +25 -0
- package/skills/generated/api/opensearch/SKILL.md +39 -0
- package/skills/generated/api/scheduler/SKILL.md +112 -0
- package/skills/generated/api/security/SKILL.md +317 -0
- package/skills/generated/api/system/SKILL.md +34 -0
- package/skills/generated/api/tasks/SKILL.md +31 -0
- package/skills/generated/api/tenancy/SKILL.md +124 -0
- package/skills/generated/api/tenant-manager/SKILL.md +34 -0
- package/skills/generated/api/website-builder/SKILL.md +356 -0
- package/skills/generated/cli/SKILL.md +28 -0
- package/skills/generated/cli/command/SKILL.md +24 -0
- package/skills/generated/extensions/SKILL.md +43 -0
- package/skills/generated/infra/SKILL.md +190 -0
- package/skills/infrastructure-extensions/SKILL.md +3 -2
- package/skills/local-development/SKILL.md +2 -28
- package/skills/project-structure/SKILL.md +78 -56
- package/skills/webiny-sdk/SKILL.md +77 -76
- package/skills/website-builder/SKILL.md +143 -149
- package/ui.d.ts +24 -0
- package/ui.js +31 -0
- package/ui.js.map +1 -0
- package/Extension.d.ts +0 -2
- package/Extension.js +0 -11
- package/Extension.js.map +0 -1
- package/skills/admin-ui-extensions/SKILL.md +0 -267
- package/skills/api-custom-feature/SKILL.md +0 -195
- 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-
|
|
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`
|