@webiny/mcp 6.1.0-beta.1 → 6.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/claude.d.ts +2 -0
- package/agents/claude.js +6 -0
- package/agents/claude.js.map +1 -1
- package/agents/cline.d.ts +2 -0
- package/agents/cline.js +6 -0
- package/agents/cline.js.map +1 -1
- package/agents/copilot.d.ts +2 -0
- package/agents/copilot.js +7 -0
- package/agents/copilot.js.map +1 -1
- package/agents/cursor.d.ts +2 -0
- package/agents/cursor.js +6 -0
- package/agents/cursor.js.map +1 -1
- package/agents/discover.d.ts +3 -0
- package/agents/discover.js +29 -0
- package/agents/discover.js.map +1 -0
- package/agents/instructions.d.ts +3 -1
- package/agents/instructions.js +15 -2
- package/agents/instructions.js.map +1 -1
- package/agents/kiro.d.ts +2 -0
- package/agents/kiro.js +6 -0
- package/agents/kiro.js.map +1 -1
- package/agents/opencode.d.ts +2 -0
- package/agents/opencode.js +7 -0
- package/agents/opencode.js.map +1 -1
- package/agents/types.d.ts +16 -0
- package/agents/types.js +3 -0
- package/agents/types.js.map +1 -0
- package/agents/windsurf.d.ts +2 -0
- package/agents/windsurf.js +6 -0
- package/agents/windsurf.js.map +1 -1
- package/cli/ConfigureMcp.js +21 -6
- package/cli/ConfigureMcp.js.map +1 -1
- package/package.json +3 -3
- package/skills/admin/admin-architect/SKILL.md +188 -0
- package/skills/admin/admin-permissions/SKILL.md +159 -0
- package/skills/admin/ui-extensions/SKILL.md +2 -0
- package/skills/api/api-architect/SKILL.md +548 -60
- package/skills/api/event-handler-pattern/SKILL.md +195 -23
- package/skills/api/graphql-api/SKILL.md +231 -31
- package/skills/api/permissions/SKILL.md +291 -0
- package/skills/api/use-case-pattern/SKILL.md +351 -12
- package/skills/api/v5-to-v6-migration/SKILL.md +416 -0
- package/skills/cli-extensions/SKILL.md +1 -1
- package/skills/content-models/SKILL.md +9 -5
- package/skills/full-stack-architect/SKILL.md +4 -0
- package/skills/generated/admin/SKILL.md +1 -1
- package/skills/generated/admin/aco/SKILL.md +1 -1
- package/skills/generated/admin/build-params/SKILL.md +1 -1
- package/skills/generated/admin/cms/SKILL.md +1 -1
- package/skills/generated/admin/configs/SKILL.md +1 -1
- package/skills/generated/admin/env-config/SKILL.md +1 -1
- package/skills/generated/admin/form/SKILL.md +1 -1
- package/skills/generated/admin/graphql-client/SKILL.md +1 -1
- package/skills/generated/admin/lexical/SKILL.md +1 -1
- package/skills/generated/admin/local-storage/SKILL.md +1 -1
- package/skills/generated/admin/router/SKILL.md +1 -1
- package/skills/generated/admin/security/SKILL.md +1 -1
- package/skills/generated/admin/tenancy/SKILL.md +1 -1
- package/skills/generated/admin/ui/SKILL.md +1 -1
- package/skills/generated/admin/website-builder/SKILL.md +1 -1
- package/skills/generated/api/SKILL.md +1 -1
- package/skills/generated/api/aco/SKILL.md +1 -1
- package/skills/generated/api/build-params/SKILL.md +1 -1
- package/skills/generated/api/cms/SKILL.md +1 -1
- package/skills/generated/api/event-publisher/SKILL.md +1 -1
- package/skills/generated/api/file-manager/SKILL.md +1 -1
- package/skills/generated/api/graphql/SKILL.md +1 -1
- package/skills/generated/api/key-value-store/SKILL.md +1 -1
- package/skills/generated/api/logger/SKILL.md +1 -1
- package/skills/generated/api/opensearch/SKILL.md +1 -1
- package/skills/generated/api/scheduler/SKILL.md +1 -1
- package/skills/generated/api/security/SKILL.md +1 -1
- package/skills/generated/api/system/SKILL.md +1 -1
- package/skills/generated/api/tasks/SKILL.md +1 -1
- package/skills/generated/api/tenancy/SKILL.md +1 -1
- package/skills/generated/api/tenant-manager/SKILL.md +1 -1
- package/skills/generated/api/website-builder/SKILL.md +1 -1
- package/skills/generated/cli/SKILL.md +1 -1
- package/skills/generated/cli/command/SKILL.md +1 -1
- package/skills/generated/extensions/SKILL.md +1 -1
- package/skills/generated/infra/SKILL.md +1 -1
- package/skills/infrastructure-extensions/SKILL.md +1 -1
- package/skills/project-structure/SKILL.md +4 -0
- package/skills/webiny-sdk/SKILL.md +1 -1
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
name: webiny-event-handler-pattern
|
|
3
3
|
context: webiny-api
|
|
4
4
|
description: >
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
EventHandler implementation pattern — handle method, event payloads, filtering, DI,
|
|
6
|
+
domain event definition, publishing events from UseCases, and reacting to external events.
|
|
7
|
+
Use this skill to implement any Webiny EventHandler (before/after hooks) or to define
|
|
8
|
+
and publish your own domain events.
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
# EventHandler Pattern
|
|
@@ -19,8 +21,6 @@ An **EventHandler** reacts to domain events in the Webiny lifecycle (e.g., `Entr
|
|
|
19
21
|
|
|
20
22
|
## Interface Shape
|
|
21
23
|
|
|
22
|
-
Every EventHandler follows this pattern:
|
|
23
|
-
|
|
24
24
|
```ts
|
|
25
25
|
interface SomeEventHandler.Interface {
|
|
26
26
|
handle(event: SomeEventHandler.Event): Promise<void>;
|
|
@@ -31,9 +31,9 @@ The `Event` is a `DomainEvent<Payload>` where the payload contains the entity an
|
|
|
31
31
|
|
|
32
32
|
## Architecture Rule: Always Wrap Logic in a Reusable Abstraction (MANDATORY)
|
|
33
33
|
|
|
34
|
-
**Never put business logic directly inside an EventHandler.** EventHandlers are thin orchestrators — they receive an event and delegate to an injected service
|
|
34
|
+
**Never put business logic directly inside an EventHandler.** EventHandlers are thin orchestrators — they receive an event and delegate to an injected service or use case. The real logic lives in a dedicated abstraction.
|
|
35
35
|
|
|
36
|
-
**Why:** Inline handler logic cannot be reused by other handlers, GraphQL resolvers, or CLI commands.
|
|
36
|
+
**Why:** Inline handler logic cannot be reused by other handlers, GraphQL resolvers, or CLI commands.
|
|
37
37
|
|
|
38
38
|
**Always follow this structure:**
|
|
39
39
|
|
|
@@ -43,12 +43,12 @@ features/
|
|
|
43
43
|
│ ├── abstractions.ts
|
|
44
44
|
│ ├── feature.ts
|
|
45
45
|
│ └── MyService.ts
|
|
46
|
-
└──
|
|
46
|
+
└── syncOnCreate/ ← thin handler that injects the service
|
|
47
47
|
├── feature.ts
|
|
48
|
-
└──
|
|
48
|
+
└── EntryAfterCreateHandler.ts
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
The EventHandler feature and the service feature are **registered separately** in `Extension.
|
|
51
|
+
The EventHandler feature and the service feature are **registered separately** in `Extension.ts`.
|
|
52
52
|
|
|
53
53
|
## How to Implement
|
|
54
54
|
|
|
@@ -61,10 +61,10 @@ class MyHandler implements SomeEventHandler.Interface {
|
|
|
61
61
|
constructor(private myService: MyService.Interface) {}
|
|
62
62
|
|
|
63
63
|
async handle(event: SomeEventHandler.Event) {
|
|
64
|
-
const { entity } = event.payload;
|
|
64
|
+
const { entity, model } = event.payload;
|
|
65
65
|
|
|
66
|
-
// For CMS handlers:
|
|
67
|
-
|
|
66
|
+
// For CMS handlers: ALWAYS filter by model
|
|
67
|
+
if (model.modelId !== "myModel") return;
|
|
68
68
|
|
|
69
69
|
await this.myService.doWork(entity);
|
|
70
70
|
}
|
|
@@ -80,7 +80,7 @@ See **webiny-api-architect** for how to define `MyService` as a proper abstracti
|
|
|
80
80
|
|
|
81
81
|
## Injecting Dependencies
|
|
82
82
|
|
|
83
|
-
EventHandlers can depend on UseCases, platform services, or
|
|
83
|
+
EventHandlers can depend on UseCases, platform services, or custom abstractions:
|
|
84
84
|
|
|
85
85
|
```ts
|
|
86
86
|
import { SomeEventHandler } from "webiny/api/<category>";
|
|
@@ -90,9 +90,7 @@ class MyHandler implements SomeEventHandler.Interface {
|
|
|
90
90
|
constructor(private someUseCase: SomeUseCase.Interface) {}
|
|
91
91
|
|
|
92
92
|
async handle(event: SomeEventHandler.Event) {
|
|
93
|
-
const result = await this.someUseCase.execute({
|
|
94
|
-
/* ... */
|
|
95
|
-
});
|
|
93
|
+
const result = await this.someUseCase.execute({ /* ... */ });
|
|
96
94
|
}
|
|
97
95
|
}
|
|
98
96
|
|
|
@@ -102,8 +100,175 @@ export default SomeEventHandler.createImplementation({
|
|
|
102
100
|
});
|
|
103
101
|
```
|
|
104
102
|
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Defining Your Own Domain Events
|
|
106
|
+
|
|
107
|
+
When your feature needs to notify other parts of the system about important domain actions, define your own events.
|
|
108
|
+
|
|
109
|
+
### Event Payload Types (in `abstractions.ts`)
|
|
110
|
+
|
|
111
|
+
Event payloads and handler abstractions live in `abstractions.ts`. The `events.ts` file only contains the event classes.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// features/disableEntity/abstractions.ts
|
|
115
|
+
import { createAbstraction } from "@webiny/feature/api";
|
|
116
|
+
import type { IEventHandler } from "@webiny/api-core/features/EventPublisher";
|
|
117
|
+
import type { Entity } from "~/shared/Entity.js";
|
|
118
|
+
// Forward declaration — actual classes are in events.ts
|
|
119
|
+
import type { EntityBeforeDisableEvent, EntityAfterDisableEvent } from "./events.js";
|
|
120
|
+
|
|
121
|
+
// Event Payload Types
|
|
122
|
+
export interface EntityBeforeDisablePayload {
|
|
123
|
+
entity: Entity;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface EntityAfterDisablePayload {
|
|
127
|
+
entity: Entity;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Handler Abstractions — one per event
|
|
131
|
+
export const EntityBeforeDisableEventHandler = createAbstraction<
|
|
132
|
+
IEventHandler<EntityBeforeDisableEvent>
|
|
133
|
+
>("MyPackage/EntityBeforeDisableEventHandler");
|
|
134
|
+
|
|
135
|
+
export namespace EntityBeforeDisableEventHandler {
|
|
136
|
+
export type Interface = IEventHandler<EntityBeforeDisableEvent>;
|
|
137
|
+
export type Event = EntityBeforeDisableEvent;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const EntityAfterDisableEventHandler = createAbstraction<
|
|
141
|
+
IEventHandler<EntityAfterDisableEvent>
|
|
142
|
+
>("MyPackage/EntityAfterDisableEventHandler");
|
|
143
|
+
|
|
144
|
+
export namespace EntityAfterDisableEventHandler {
|
|
145
|
+
export type Interface = IEventHandler<EntityAfterDisableEvent>;
|
|
146
|
+
export type Event = EntityAfterDisableEvent;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Event Class Definition (`events.ts`)
|
|
151
|
+
|
|
152
|
+
Event classes import payload types and handler abstractions from `abstractions.ts`.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// features/disableEntity/events.ts
|
|
156
|
+
import { DomainEvent } from "@webiny/api-core/features/EventPublisher";
|
|
157
|
+
import {
|
|
158
|
+
EntityBeforeDisableEventHandler,
|
|
159
|
+
EntityAfterDisableEventHandler
|
|
160
|
+
} from "./abstractions.js";
|
|
161
|
+
import type {
|
|
162
|
+
EntityBeforeDisablePayload,
|
|
163
|
+
EntityAfterDisablePayload
|
|
164
|
+
} from "./abstractions.js";
|
|
165
|
+
|
|
166
|
+
export class EntityBeforeDisableEvent extends DomainEvent<EntityBeforeDisablePayload> {
|
|
167
|
+
eventType = "entity.beforeDisable" as const;
|
|
168
|
+
|
|
169
|
+
getHandlerAbstraction() {
|
|
170
|
+
return EntityBeforeDisableEventHandler;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export class EntityAfterDisableEvent extends DomainEvent<EntityAfterDisablePayload> {
|
|
175
|
+
eventType = "entity.afterDisable" as const;
|
|
176
|
+
|
|
177
|
+
getHandlerAbstraction() {
|
|
178
|
+
return EntityAfterDisableEventHandler;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Publishing Events from a UseCase
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// features/disableEntity/DisableEntityUseCase.ts
|
|
187
|
+
import { EventPublisher } from "@webiny/api-core/features/EventPublisher";
|
|
188
|
+
import { EntityBeforeDisableEvent, EntityAfterDisableEvent } from "./events.js";
|
|
189
|
+
|
|
190
|
+
class DisableEntityUseCase implements UseCaseAbstraction.Interface {
|
|
191
|
+
constructor(
|
|
192
|
+
private eventPublisher: EventPublisher.Interface,
|
|
193
|
+
private getEntityById: GetEntityByIdUseCase.Interface,
|
|
194
|
+
private updateEntity: UpdateEntityUseCase.Interface
|
|
195
|
+
) {}
|
|
196
|
+
|
|
197
|
+
async execute(entityId: string): Promise<Result<void, UseCaseAbstraction.Error>> {
|
|
198
|
+
const getResult = await this.getEntityById.execute(entityId);
|
|
199
|
+
if (getResult.isFail()) return Result.fail(getResult.error);
|
|
200
|
+
|
|
201
|
+
const entity = getResult.value;
|
|
202
|
+
|
|
203
|
+
// Publish BEFORE event (can be intercepted to reject)
|
|
204
|
+
await this.eventPublisher.publish(new EntityBeforeDisableEvent({ entity }));
|
|
205
|
+
|
|
206
|
+
// Perform the operation
|
|
207
|
+
const updateResult = await this.updateEntity.execute(entityId, { status: "disabled" });
|
|
208
|
+
if (updateResult.isFail()) return Result.fail(updateResult.error);
|
|
209
|
+
|
|
210
|
+
// Publish AFTER event (for side effects)
|
|
211
|
+
await this.eventPublisher.publish(new EntityAfterDisableEvent({ entity: updateResult.value }));
|
|
212
|
+
|
|
213
|
+
return Result.ok();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export default UseCaseAbstraction.createImplementation({
|
|
218
|
+
implementation: DisableEntityUseCase,
|
|
219
|
+
dependencies: [EventPublisher, GetEntityByIdUseCase, UpdateEntityUseCase]
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Reacting to External Events
|
|
226
|
+
|
|
227
|
+
To react to events from other packages (e.g., CMS entry deletion), implement the external event's handler abstraction:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
// features/cleanupOnEntryDelete/CleanupOnEntryDeleteHandler.ts
|
|
231
|
+
import { EntryAfterDeleteEventHandler } from "@webiny/api-headless-cms/features/contentEntry/DeleteEntry/events.js";
|
|
232
|
+
import { CleanupService } from "../cleanupService/abstractions.js";
|
|
233
|
+
import { MY_MODEL_ID } from "~/shared/constants.js";
|
|
234
|
+
|
|
235
|
+
class CleanupOnEntryDeleteHandler implements EntryAfterDeleteEventHandler.Interface {
|
|
236
|
+
constructor(private cleanupService: CleanupService.Interface) {}
|
|
237
|
+
|
|
238
|
+
async handle(event: EntryAfterDeleteEventHandler.Event): Promise<void> {
|
|
239
|
+
const { entry, model } = event.payload;
|
|
240
|
+
|
|
241
|
+
// ALWAYS filter by model — handler fires for ALL models
|
|
242
|
+
if (model.modelId !== MY_MODEL_ID) return;
|
|
243
|
+
|
|
244
|
+
if (!event.payload.permanent) return;
|
|
245
|
+
|
|
246
|
+
await this.cleanupService.cleanup(entry.entryId);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export default EntryAfterDeleteEventHandler.createImplementation({
|
|
251
|
+
implementation: CleanupOnEntryDeleteHandler,
|
|
252
|
+
dependencies: [CleanupService]
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Event Naming Conventions
|
|
259
|
+
|
|
260
|
+
| Artifact | Pattern | Example |
|
|
261
|
+
| ------------------ | ---------------------------------------------- | ---------------------------------- |
|
|
262
|
+
| `eventType` | `"entity.beforeAction"` / `"entity.afterAction"` | `"tenant.beforeDisable"` |
|
|
263
|
+
| Handler abstraction | `{Entity}{Before\|After}{Action}EventHandler` | `TenantBeforeDisableEventHandler` |
|
|
264
|
+
| Event class | `{Entity}{Before\|After}{Action}Event` | `TenantBeforeDisableEvent` |
|
|
265
|
+
|
|
105
266
|
## Registration
|
|
106
267
|
|
|
268
|
+
**YOU MUST include the full file path with the `.ts` extension in the `src` prop.** For example, use `src={"@/extensions/my-handler.ts"}`, NOT `src={"@/extensions/my-handler"}`. Omitting the file extension will cause a build failure.
|
|
269
|
+
|
|
270
|
+
**YOU MUST use `export default` for the `createImplementation()` call** when the file is targeted directly by an Extension `src` prop. Using a named export (`export const Foo = SomeFactory.createImplementation(...)`) will cause a build failure. Named exports are only valid inside files registered via `createFeature`.
|
|
271
|
+
|
|
107
272
|
```tsx
|
|
108
273
|
<Api.Extension src={"@/extensions/my-handler.ts"} />
|
|
109
274
|
```
|
|
@@ -114,18 +279,25 @@ Deploy with: `yarn webiny deploy api --env=dev`
|
|
|
114
279
|
|
|
115
280
|
**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
281
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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.
|
|
282
|
+
1. Read the `abstractions.ts` file from the catalog `Source` path — it contains the payload interface
|
|
283
|
+
2. Read the `events.ts` file (sibling to `abstractions.ts`) — it contains the `Interface` and `Event` types
|
|
284
|
+
3. If the payload references domain types, follow the import and read that declaration
|
|
124
285
|
|
|
125
286
|
## Key Rules
|
|
126
287
|
|
|
127
288
|
- **Before handlers**: payload may be mutable — write to it to set computed fields. Throw to reject the operation.
|
|
128
289
|
- **After handlers**: payload reflects persisted state — do not mutate. Use for side effects.
|
|
129
290
|
- **Filter by entity**: handlers fire for ALL entities of that type. Always check `modelId`, `entity type`, etc.
|
|
291
|
+
- **Events extend** `DomainEvent<TPayload>` with `eventType` property (not `static type`)
|
|
292
|
+
- **Every event must** implement `getHandlerAbstraction()` returning its handler abstraction
|
|
293
|
+
- **Every handler abstraction** must have a namespace with `Interface` and `Event` types
|
|
294
|
+
- **Payload types** live in `abstractions.ts`; event classes live in `events.ts`
|
|
295
|
+
- **Publish order**: before event → operation → after event
|
|
130
296
|
- DI constructor parameter order must match the `dependencies` array order exactly
|
|
131
297
|
- Use `.js` extensions in import paths (ES modules)
|
|
298
|
+
|
|
299
|
+
## Related Skills
|
|
300
|
+
|
|
301
|
+
- **webiny-api-architect** — Architecture overview, Services vs UseCases, feature naming
|
|
302
|
+
- **webiny-use-case-pattern** — UseCase implementation where events are published from
|
|
303
|
+
- **webiny-dependency-injection** — Injectable services catalog
|
|
@@ -5,19 +5,24 @@ description: >
|
|
|
5
5
|
Adding custom GraphQL queries and mutations using GraphQLSchemaFactory.
|
|
6
6
|
Use this skill when the developer wants to add custom GraphQL endpoints, create custom
|
|
7
7
|
queries or mutations, add business logic to the API layer, build custom resolvers,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
inject backend services (identity, tenancy, CMS use-cases) into their GraphQL schema,
|
|
9
|
+
or build dynamic GraphQL inputs from CMS models. Covers the full pattern from simple
|
|
10
|
+
queries to complex resolvers with dependency injection and permission transformers.
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
# Custom GraphQL API
|
|
13
14
|
|
|
14
15
|
## TL;DR
|
|
15
16
|
|
|
16
|
-
Add custom GraphQL queries and mutations using
|
|
17
|
+
Add custom GraphQL queries and mutations using `GraphQLSchemaFactory`. Implement `GraphQLSchemaFactory.Interface`, use the schema builder to add type definitions and resolvers (with per-resolver DI), and export with `GraphQLSchemaFactory.createImplementation()`. Register as `<Api.Extension>`.
|
|
18
|
+
|
|
19
|
+
**YOU MUST include the full file path with the `.ts` extension in every `src` prop.** For example, use `src={"/extensions/MySchema.ts"}`, NOT `src={"/extensions/MySchema"}`. Omitting the file extension will cause a build failure.
|
|
20
|
+
|
|
21
|
+
**YOU MUST use `export default` for the `createImplementation()` call** when the file is targeted directly by an Extension `src` prop. Using a named export (`export const Foo = SomeFactory.createImplementation(...)`) will cause a build failure. Named exports are only valid inside files registered via `createFeature`.
|
|
17
22
|
|
|
18
23
|
## The GraphQLSchemaFactory Pattern
|
|
19
24
|
|
|
20
|
-
The `execute` method receives a
|
|
25
|
+
The `execute` method receives a schema builder and returns it after adding type defs and resolvers.
|
|
21
26
|
|
|
22
27
|
```typescript
|
|
23
28
|
// extensions/mySchema/MyGraphQLSchema.ts
|
|
@@ -62,7 +67,7 @@ export const MySchema = () => {
|
|
|
62
67
|
};
|
|
63
68
|
```
|
|
64
69
|
|
|
65
|
-
## Builder API Reference
|
|
70
|
+
## Schema Builder API Reference
|
|
66
71
|
|
|
67
72
|
| Method | Description |
|
|
68
73
|
| --------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
@@ -93,7 +98,7 @@ Key points:
|
|
|
93
98
|
|
|
94
99
|
## Per-Resolver Dependency Injection
|
|
95
100
|
|
|
96
|
-
Dependencies in `addResolver` are resolved at request time from the request-scoped container. This is different from class-level constructor DI
|
|
101
|
+
Dependencies in `addResolver` are resolved at request time from the request-scoped container. This is different from class-level constructor DI — it gives each resolver access to request-scoped services like identity and tenant context.
|
|
97
102
|
|
|
98
103
|
```typescript
|
|
99
104
|
import { GraphQLSchemaFactory } from "webiny/api/graphql";
|
|
@@ -130,66 +135,259 @@ export default GraphQLSchemaFactory.createImplementation({
|
|
|
130
135
|
});
|
|
131
136
|
```
|
|
132
137
|
|
|
133
|
-
|
|
138
|
+
Note: `GraphQLSchemaFactory` implementations typically have `dependencies: []` because DI happens at the resolver level via `addResolver({ dependencies })`, not at the class constructor level.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Query Schema with UseCase DI
|
|
134
143
|
|
|
135
|
-
|
|
144
|
+
Full pattern using `Response` / `ErrorResponse` wrappers and UseCase injection:
|
|
136
145
|
|
|
137
146
|
```typescript
|
|
138
|
-
import {
|
|
139
|
-
import {
|
|
140
|
-
import {
|
|
147
|
+
import { Response } from "@webiny/handler-graphql";
|
|
148
|
+
import { ErrorResponse } from "@webiny/handler-graphql";
|
|
149
|
+
import { GraphQLSchemaFactory } from "@webiny/handler-graphql/graphql/abstractions.js";
|
|
150
|
+
import { GetCurrentEntityUseCase } from "../features/getCurrentEntity/abstractions.js";
|
|
141
151
|
|
|
142
|
-
class
|
|
152
|
+
class GetCurrentEntitySchema implements GraphQLSchemaFactory.Interface {
|
|
143
153
|
async execute(
|
|
144
154
|
builder: GraphQLSchemaFactory.SchemaBuilder
|
|
145
155
|
): Promise<GraphQLSchemaFactory.SchemaBuilder> {
|
|
146
156
|
builder.addTypeDefs(/* GraphQL */ `
|
|
147
|
-
type
|
|
157
|
+
type EntityResponse {
|
|
158
|
+
data: Entity
|
|
159
|
+
error: Error
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type Entity {
|
|
148
163
|
id: ID!
|
|
149
|
-
|
|
164
|
+
values: JSON!
|
|
150
165
|
}
|
|
151
166
|
|
|
152
|
-
type
|
|
153
|
-
|
|
167
|
+
type MyPackageQuery {
|
|
168
|
+
getCurrentEntity: EntityResponse
|
|
154
169
|
}
|
|
155
170
|
|
|
156
|
-
extend type
|
|
157
|
-
|
|
171
|
+
extend type Query {
|
|
172
|
+
myPackage: MyPackageQuery
|
|
158
173
|
}
|
|
159
174
|
`);
|
|
160
175
|
|
|
161
176
|
// Pass-through resolver for the namespace
|
|
162
177
|
builder.addResolver({
|
|
163
|
-
path: "
|
|
178
|
+
path: "Query.myPackage",
|
|
164
179
|
resolver: () => {
|
|
165
180
|
return () => ({});
|
|
166
181
|
}
|
|
167
182
|
});
|
|
168
183
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
184
|
+
builder.addResolver({
|
|
185
|
+
path: "MyPackageQuery.getCurrentEntity",
|
|
186
|
+
dependencies: [GetCurrentEntityUseCase],
|
|
187
|
+
resolver: (getEntity: GetCurrentEntityUseCase.Interface) => {
|
|
188
|
+
return async () => {
|
|
189
|
+
const result = await getEntity.execute();
|
|
190
|
+
if (result.isFail()) {
|
|
191
|
+
return new ErrorResponse(result.error);
|
|
192
|
+
}
|
|
193
|
+
return new Response(result.value);
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return builder;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export default GraphQLSchemaFactory.createImplementation({
|
|
203
|
+
implementation: GetCurrentEntitySchema,
|
|
204
|
+
dependencies: []
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Namespaced Mutation Pattern
|
|
211
|
+
|
|
212
|
+
For namespaced mutations (e.g. `mutation { myPackage { createEntity } }`):
|
|
213
|
+
|
|
214
|
+
1. **One schema** defines the base namespace type + extends `Mutation`
|
|
215
|
+
2. **Other schemas** extend the namespace type
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// Schema 1: defines the namespace
|
|
219
|
+
builder.addTypeDefs(/* GraphQL */ `
|
|
220
|
+
type MyPackageMutation {
|
|
221
|
+
_empty: String
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
extend type Mutation {
|
|
225
|
+
myPackage: MyPackageMutation
|
|
226
|
+
}
|
|
227
|
+
`);
|
|
228
|
+
|
|
229
|
+
builder.addResolver({
|
|
230
|
+
path: "Mutation.myPackage",
|
|
231
|
+
resolver: () => {
|
|
232
|
+
return () => ({});
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Schema 2: extends the namespace
|
|
237
|
+
builder.addTypeDefs(/* GraphQL */ `
|
|
238
|
+
extend type MyPackageMutation {
|
|
239
|
+
disableEntity(entityId: ID!): BooleanResponse
|
|
240
|
+
}
|
|
241
|
+
`);
|
|
242
|
+
|
|
243
|
+
builder.addResolver<{ entityId: string }>({
|
|
244
|
+
path: "MyPackageMutation.disableEntity",
|
|
245
|
+
dependencies: [DisableEntityUseCase],
|
|
246
|
+
resolver: (disableEntity: DisableEntityUseCase.Interface) => {
|
|
247
|
+
return async ({ args }) => {
|
|
248
|
+
const result = await disableEntity.execute(args.entityId);
|
|
249
|
+
if (result.isFail()) {
|
|
250
|
+
return new ErrorResponse(result.error);
|
|
251
|
+
}
|
|
252
|
+
return new Response(true);
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Dynamic Input Fields from CMS Model
|
|
261
|
+
|
|
262
|
+
When GraphQL inputs must reflect CMS model fields (e.g., an extensible "extensions" object):
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import { GraphQLSchemaFactory } from "@webiny/handler-graphql/graphql/abstractions.js";
|
|
266
|
+
import { Response, ErrorResponse } from "@webiny/handler-graphql";
|
|
267
|
+
import { PluginsContainer } from "@webiny/api-headless-cms/legacy/abstractions.js";
|
|
268
|
+
import { renderInputFields } from "@webiny/api-headless-cms/utils/renderInputFields.js";
|
|
269
|
+
import { createFieldTypePluginRecords } from "@webiny/api-headless-cms/graphql/schema/createFieldTypePluginRecords.js";
|
|
270
|
+
import { ListModelsUseCase } from "@webiny/api-headless-cms/exports/api/cms/model.js";
|
|
271
|
+
import { CreateEntityUseCase } from "../features/createEntity/abstractions.js";
|
|
272
|
+
import { ENTITY_MODEL_ID } from "~/shared/constants.js";
|
|
273
|
+
|
|
274
|
+
class CreateEntitySchema implements GraphQLSchemaFactory.Interface {
|
|
275
|
+
constructor(
|
|
276
|
+
private pluginsContainer: PluginsContainer.Interface,
|
|
277
|
+
private listModelsUseCase: ListModelsUseCase.Interface
|
|
278
|
+
) {}
|
|
279
|
+
|
|
280
|
+
async execute(
|
|
281
|
+
builder: GraphQLSchemaFactory.SchemaBuilder
|
|
282
|
+
): Promise<GraphQLSchemaFactory.SchemaBuilder> {
|
|
283
|
+
const inputCreateFields = await this.getExtensionsInput();
|
|
284
|
+
|
|
285
|
+
builder.addTypeDefs(/* GraphQL */ `
|
|
286
|
+
${inputCreateFields.map(f => f.typeDefs).join("\n")}
|
|
287
|
+
|
|
288
|
+
input CreateEntityInput {
|
|
289
|
+
id: ID
|
|
290
|
+
name: String!
|
|
291
|
+
description: String
|
|
292
|
+
${inputCreateFields.map(f => f.fields).join("\n")}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
extend type MyPackageMutation {
|
|
296
|
+
createEntity(input: CreateEntityInput!): BooleanResponse
|
|
297
|
+
}
|
|
298
|
+
`);
|
|
299
|
+
|
|
300
|
+
builder.addResolver<{ input: CreateEntityUseCase.Input }>({
|
|
301
|
+
path: "MyPackageMutation.createEntity",
|
|
302
|
+
dependencies: [CreateEntityUseCase],
|
|
303
|
+
resolver: (createEntity: CreateEntityUseCase.Interface) => {
|
|
174
304
|
return async ({ args }) => {
|
|
175
|
-
|
|
176
|
-
|
|
305
|
+
const result = await createEntity.execute(args.input);
|
|
306
|
+
if (result.isFail()) {
|
|
307
|
+
return new ErrorResponse(result.error);
|
|
177
308
|
}
|
|
178
|
-
return
|
|
309
|
+
return new Response(true);
|
|
179
310
|
};
|
|
180
311
|
}
|
|
181
312
|
});
|
|
182
313
|
|
|
183
314
|
return builder;
|
|
184
315
|
}
|
|
316
|
+
|
|
317
|
+
private async getExtensionsInput() {
|
|
318
|
+
const fieldTypePlugins = createFieldTypePluginRecords(this.pluginsContainer);
|
|
319
|
+
const modelsResult = await this.listModelsUseCase.execute({
|
|
320
|
+
includePlugins: true,
|
|
321
|
+
includePrivate: false
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
if (modelsResult.isFail()) {
|
|
325
|
+
return [{ typeDefs: "", fields: "extensions: JSON" }];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const models = modelsResult.value;
|
|
329
|
+
const model = models.find(m => m.modelId === ENTITY_MODEL_ID)!;
|
|
330
|
+
|
|
331
|
+
return renderInputFields({
|
|
332
|
+
models,
|
|
333
|
+
model,
|
|
334
|
+
fields: model.fields.filter(f => f.fieldId === "extensions"),
|
|
335
|
+
fieldTypePlugins
|
|
336
|
+
});
|
|
337
|
+
}
|
|
185
338
|
}
|
|
186
339
|
|
|
340
|
+
// Note: constructor DI needed here because of PluginsContainer + ListModelsUseCase
|
|
187
341
|
export default GraphQLSchemaFactory.createImplementation({
|
|
188
|
-
implementation:
|
|
342
|
+
implementation: CreateEntitySchema,
|
|
343
|
+
dependencies: [PluginsContainer, ListModelsUseCase]
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Permission Transformer (Adding CMS Permissions)
|
|
350
|
+
|
|
351
|
+
When your package needs CMS access, implement a `PermissionTransformer` to expand your custom permission into the required CMS permissions:
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
// features/addCmsPermissions/AddCmsPermissions.ts
|
|
355
|
+
import { PermissionTransformer } from "@webiny/api-core/features/security/authorization/AuthorizationContext/abstractions.js";
|
|
356
|
+
|
|
357
|
+
class AddCmsPermissions implements PermissionTransformer.Interface {
|
|
358
|
+
execute(permission: PermissionTransformer.Permission) {
|
|
359
|
+
if (permission.name !== "mypackage.*") {
|
|
360
|
+
return permission;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return [
|
|
364
|
+
permission,
|
|
365
|
+
{ name: "cms.endpoint.manage" },
|
|
366
|
+
{ name: "cms.contentModel", own: false, rwd: "r", pw: "", models: ["myEntityModelId"] },
|
|
367
|
+
{ name: "cms.contentModelGroup", own: false, rwd: "r", pw: "", groups: ["hidden"] },
|
|
368
|
+
{ name: "cms.contentEntry", own: false, rwd: "rwd", pw: "" }
|
|
369
|
+
];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export default PermissionTransformer.createImplementation({
|
|
374
|
+
implementation: AddCmsPermissions,
|
|
189
375
|
dependencies: []
|
|
190
376
|
});
|
|
191
377
|
```
|
|
192
378
|
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Key Rules
|
|
382
|
+
|
|
383
|
+
- Implement `GraphQLSchemaFactory.Interface`
|
|
384
|
+
- Use `builder.addTypeDefs()` for schema definitions and `builder.addResolver()` for resolvers
|
|
385
|
+
- Resolver `dependencies` array lists DI abstractions; resolver function receives resolved instances in same order
|
|
386
|
+
- Type the resolver args generic: `builder.addResolver<{ input: UseCaseAbstraction.Input }>`
|
|
387
|
+
- The root Query/Mutation types define a namespace type (e.g., `MyPackageQuery`, `MyPackageMutation`) extended by individual schemas
|
|
388
|
+
- Use `Response` for success, `ErrorResponse` for failure (from `@webiny/handler-graphql`)
|
|
389
|
+
- Export as `default`
|
|
390
|
+
|
|
193
391
|
## Quick Reference
|
|
194
392
|
|
|
195
393
|
```
|
|
@@ -200,10 +398,12 @@ Return: Promise<GraphQLSchemaFactory.SchemaBuilder>
|
|
|
200
398
|
Export: GraphQLSchemaFactory.createImplementation({ implementation, dependencies })
|
|
201
399
|
Register: <Api.Extension src={"@/extensions/mySchema/MyGraphQLSchema.ts"} />
|
|
202
400
|
Deploy: yarn webiny deploy api --env=dev
|
|
401
|
+
Response: import { Response, ErrorResponse } from "@webiny/handler-graphql"
|
|
203
402
|
```
|
|
204
403
|
|
|
205
404
|
## Related Skills
|
|
206
405
|
|
|
207
|
-
-
|
|
208
|
-
-
|
|
209
|
-
-
|
|
406
|
+
- **webiny-api-architect** — Architecture overview, Services vs UseCases, feature naming, anti-patterns
|
|
407
|
+
- **webiny-use-case-pattern** — UseCase implementation consumed by GraphQL resolvers
|
|
408
|
+
- **webiny-dependency-injection** — Full DI reference for all injectable services
|
|
409
|
+
- **webiny-project-structure** — How to register extensions in `webiny.config.tsx`
|