@webiny/mcp 6.1.0-beta.0 → 6.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/api/api-architect/SKILL.md +548 -60
- package/skills/api/event-handler-pattern/SKILL.md +191 -23
- package/skills/api/graphql-api/SKILL.md +227 -31
- package/skills/api/permissions/SKILL.md +291 -0
- package/skills/api/use-case-pattern/SKILL.md +347 -12
- package/skills/api/v5-to-v6-migration/SKILL.md +416 -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
|
@@ -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,6 +100,169 @@ 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
|
|
|
107
268
|
```tsx
|
|
@@ -114,18 +275,25 @@ Deploy with: `yarn webiny deploy api --env=dev`
|
|
|
114
275
|
|
|
115
276
|
**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
277
|
|
|
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.
|
|
278
|
+
1. Read the `abstractions.ts` file from the catalog `Source` path — it contains the payload interface
|
|
279
|
+
2. Read the `events.ts` file (sibling to `abstractions.ts`) — it contains the `Interface` and `Event` types
|
|
280
|
+
3. If the payload references domain types, follow the import and read that declaration
|
|
124
281
|
|
|
125
282
|
## Key Rules
|
|
126
283
|
|
|
127
284
|
- **Before handlers**: payload may be mutable — write to it to set computed fields. Throw to reject the operation.
|
|
128
285
|
- **After handlers**: payload reflects persisted state — do not mutate. Use for side effects.
|
|
129
286
|
- **Filter by entity**: handlers fire for ALL entities of that type. Always check `modelId`, `entity type`, etc.
|
|
287
|
+
- **Events extend** `DomainEvent<TPayload>` with `eventType` property (not `static type`)
|
|
288
|
+
- **Every event must** implement `getHandlerAbstraction()` returning its handler abstraction
|
|
289
|
+
- **Every handler abstraction** must have a namespace with `Interface` and `Event` types
|
|
290
|
+
- **Payload types** live in `abstractions.ts`; event classes live in `events.ts`
|
|
291
|
+
- **Publish order**: before event → operation → after event
|
|
130
292
|
- DI constructor parameter order must match the `dependencies` array order exactly
|
|
131
293
|
- Use `.js` extensions in import paths (ES modules)
|
|
294
|
+
|
|
295
|
+
## Related Skills
|
|
296
|
+
|
|
297
|
+
- **webiny-api-architect** — Architecture overview, Services vs UseCases, feature naming
|
|
298
|
+
- **webiny-use-case-pattern** — UseCase implementation where events are published from
|
|
299
|
+
- **webiny-dependency-injection** — Injectable services catalog
|
|
@@ -5,19 +5,20 @@ 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>`.
|
|
17
18
|
|
|
18
19
|
## The GraphQLSchemaFactory Pattern
|
|
19
20
|
|
|
20
|
-
The `execute` method receives a
|
|
21
|
+
The `execute` method receives a schema builder and returns it after adding type defs and resolvers.
|
|
21
22
|
|
|
22
23
|
```typescript
|
|
23
24
|
// extensions/mySchema/MyGraphQLSchema.ts
|
|
@@ -62,7 +63,7 @@ export const MySchema = () => {
|
|
|
62
63
|
};
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
## Builder API Reference
|
|
66
|
+
## Schema Builder API Reference
|
|
66
67
|
|
|
67
68
|
| Method | Description |
|
|
68
69
|
| --------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
@@ -93,7 +94,7 @@ Key points:
|
|
|
93
94
|
|
|
94
95
|
## Per-Resolver Dependency Injection
|
|
95
96
|
|
|
96
|
-
Dependencies in `addResolver` are resolved at request time from the request-scoped container. This is different from class-level constructor DI
|
|
97
|
+
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
98
|
|
|
98
99
|
```typescript
|
|
99
100
|
import { GraphQLSchemaFactory } from "webiny/api/graphql";
|
|
@@ -130,66 +131,259 @@ export default GraphQLSchemaFactory.createImplementation({
|
|
|
130
131
|
});
|
|
131
132
|
```
|
|
132
133
|
|
|
133
|
-
|
|
134
|
+
Note: `GraphQLSchemaFactory` implementations typically have `dependencies: []` because DI happens at the resolver level via `addResolver({ dependencies })`, not at the class constructor level.
|
|
134
135
|
|
|
135
|
-
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Query Schema with UseCase DI
|
|
139
|
+
|
|
140
|
+
Full pattern using `Response` / `ErrorResponse` wrappers and UseCase injection:
|
|
136
141
|
|
|
137
142
|
```typescript
|
|
138
|
-
import {
|
|
139
|
-
import {
|
|
140
|
-
import {
|
|
143
|
+
import { Response } from "@webiny/handler-graphql";
|
|
144
|
+
import { ErrorResponse } from "@webiny/handler-graphql";
|
|
145
|
+
import { GraphQLSchemaFactory } from "@webiny/handler-graphql/graphql/abstractions.js";
|
|
146
|
+
import { GetCurrentEntityUseCase } from "../features/getCurrentEntity/abstractions.js";
|
|
141
147
|
|
|
142
|
-
class
|
|
148
|
+
class GetCurrentEntitySchema implements GraphQLSchemaFactory.Interface {
|
|
143
149
|
async execute(
|
|
144
150
|
builder: GraphQLSchemaFactory.SchemaBuilder
|
|
145
151
|
): Promise<GraphQLSchemaFactory.SchemaBuilder> {
|
|
146
152
|
builder.addTypeDefs(/* GraphQL */ `
|
|
147
|
-
type
|
|
153
|
+
type EntityResponse {
|
|
154
|
+
data: Entity
|
|
155
|
+
error: Error
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
type Entity {
|
|
148
159
|
id: ID!
|
|
149
|
-
|
|
160
|
+
values: JSON!
|
|
150
161
|
}
|
|
151
162
|
|
|
152
|
-
type
|
|
153
|
-
|
|
163
|
+
type MyPackageQuery {
|
|
164
|
+
getCurrentEntity: EntityResponse
|
|
154
165
|
}
|
|
155
166
|
|
|
156
|
-
extend type
|
|
157
|
-
|
|
167
|
+
extend type Query {
|
|
168
|
+
myPackage: MyPackageQuery
|
|
158
169
|
}
|
|
159
170
|
`);
|
|
160
171
|
|
|
161
172
|
// Pass-through resolver for the namespace
|
|
162
173
|
builder.addResolver({
|
|
163
|
-
path: "
|
|
174
|
+
path: "Query.myPackage",
|
|
164
175
|
resolver: () => {
|
|
165
176
|
return () => ({});
|
|
166
177
|
}
|
|
167
178
|
});
|
|
168
179
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
180
|
+
builder.addResolver({
|
|
181
|
+
path: "MyPackageQuery.getCurrentEntity",
|
|
182
|
+
dependencies: [GetCurrentEntityUseCase],
|
|
183
|
+
resolver: (getEntity: GetCurrentEntityUseCase.Interface) => {
|
|
184
|
+
return async () => {
|
|
185
|
+
const result = await getEntity.execute();
|
|
186
|
+
if (result.isFail()) {
|
|
187
|
+
return new ErrorResponse(result.error);
|
|
188
|
+
}
|
|
189
|
+
return new Response(result.value);
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return builder;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export default GraphQLSchemaFactory.createImplementation({
|
|
199
|
+
implementation: GetCurrentEntitySchema,
|
|
200
|
+
dependencies: []
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Namespaced Mutation Pattern
|
|
207
|
+
|
|
208
|
+
For namespaced mutations (e.g. `mutation { myPackage { createEntity } }`):
|
|
209
|
+
|
|
210
|
+
1. **One schema** defines the base namespace type + extends `Mutation`
|
|
211
|
+
2. **Other schemas** extend the namespace type
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
// Schema 1: defines the namespace
|
|
215
|
+
builder.addTypeDefs(/* GraphQL */ `
|
|
216
|
+
type MyPackageMutation {
|
|
217
|
+
_empty: String
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
extend type Mutation {
|
|
221
|
+
myPackage: MyPackageMutation
|
|
222
|
+
}
|
|
223
|
+
`);
|
|
224
|
+
|
|
225
|
+
builder.addResolver({
|
|
226
|
+
path: "Mutation.myPackage",
|
|
227
|
+
resolver: () => {
|
|
228
|
+
return () => ({});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Schema 2: extends the namespace
|
|
233
|
+
builder.addTypeDefs(/* GraphQL */ `
|
|
234
|
+
extend type MyPackageMutation {
|
|
235
|
+
disableEntity(entityId: ID!): BooleanResponse
|
|
236
|
+
}
|
|
237
|
+
`);
|
|
238
|
+
|
|
239
|
+
builder.addResolver<{ entityId: string }>({
|
|
240
|
+
path: "MyPackageMutation.disableEntity",
|
|
241
|
+
dependencies: [DisableEntityUseCase],
|
|
242
|
+
resolver: (disableEntity: DisableEntityUseCase.Interface) => {
|
|
243
|
+
return async ({ args }) => {
|
|
244
|
+
const result = await disableEntity.execute(args.entityId);
|
|
245
|
+
if (result.isFail()) {
|
|
246
|
+
return new ErrorResponse(result.error);
|
|
247
|
+
}
|
|
248
|
+
return new Response(true);
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Dynamic Input Fields from CMS Model
|
|
257
|
+
|
|
258
|
+
When GraphQL inputs must reflect CMS model fields (e.g., an extensible "extensions" object):
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { GraphQLSchemaFactory } from "@webiny/handler-graphql/graphql/abstractions.js";
|
|
262
|
+
import { Response, ErrorResponse } from "@webiny/handler-graphql";
|
|
263
|
+
import { PluginsContainer } from "@webiny/api-headless-cms/legacy/abstractions.js";
|
|
264
|
+
import { renderInputFields } from "@webiny/api-headless-cms/utils/renderInputFields.js";
|
|
265
|
+
import { createFieldTypePluginRecords } from "@webiny/api-headless-cms/graphql/schema/createFieldTypePluginRecords.js";
|
|
266
|
+
import { ListModelsUseCase } from "@webiny/api-headless-cms/exports/api/cms/model.js";
|
|
267
|
+
import { CreateEntityUseCase } from "../features/createEntity/abstractions.js";
|
|
268
|
+
import { ENTITY_MODEL_ID } from "~/shared/constants.js";
|
|
269
|
+
|
|
270
|
+
class CreateEntitySchema implements GraphQLSchemaFactory.Interface {
|
|
271
|
+
constructor(
|
|
272
|
+
private pluginsContainer: PluginsContainer.Interface,
|
|
273
|
+
private listModelsUseCase: ListModelsUseCase.Interface
|
|
274
|
+
) {}
|
|
275
|
+
|
|
276
|
+
async execute(
|
|
277
|
+
builder: GraphQLSchemaFactory.SchemaBuilder
|
|
278
|
+
): Promise<GraphQLSchemaFactory.SchemaBuilder> {
|
|
279
|
+
const inputCreateFields = await this.getExtensionsInput();
|
|
280
|
+
|
|
281
|
+
builder.addTypeDefs(/* GraphQL */ `
|
|
282
|
+
${inputCreateFields.map(f => f.typeDefs).join("\n")}
|
|
283
|
+
|
|
284
|
+
input CreateEntityInput {
|
|
285
|
+
id: ID
|
|
286
|
+
name: String!
|
|
287
|
+
description: String
|
|
288
|
+
${inputCreateFields.map(f => f.fields).join("\n")}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
extend type MyPackageMutation {
|
|
292
|
+
createEntity(input: CreateEntityInput!): BooleanResponse
|
|
293
|
+
}
|
|
294
|
+
`);
|
|
295
|
+
|
|
296
|
+
builder.addResolver<{ input: CreateEntityUseCase.Input }>({
|
|
297
|
+
path: "MyPackageMutation.createEntity",
|
|
298
|
+
dependencies: [CreateEntityUseCase],
|
|
299
|
+
resolver: (createEntity: CreateEntityUseCase.Interface) => {
|
|
174
300
|
return async ({ args }) => {
|
|
175
|
-
|
|
176
|
-
|
|
301
|
+
const result = await createEntity.execute(args.input);
|
|
302
|
+
if (result.isFail()) {
|
|
303
|
+
return new ErrorResponse(result.error);
|
|
177
304
|
}
|
|
178
|
-
return
|
|
305
|
+
return new Response(true);
|
|
179
306
|
};
|
|
180
307
|
}
|
|
181
308
|
});
|
|
182
309
|
|
|
183
310
|
return builder;
|
|
184
311
|
}
|
|
312
|
+
|
|
313
|
+
private async getExtensionsInput() {
|
|
314
|
+
const fieldTypePlugins = createFieldTypePluginRecords(this.pluginsContainer);
|
|
315
|
+
const modelsResult = await this.listModelsUseCase.execute({
|
|
316
|
+
includePlugins: true,
|
|
317
|
+
includePrivate: false
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
if (modelsResult.isFail()) {
|
|
321
|
+
return [{ typeDefs: "", fields: "extensions: JSON" }];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const models = modelsResult.value;
|
|
325
|
+
const model = models.find(m => m.modelId === ENTITY_MODEL_ID)!;
|
|
326
|
+
|
|
327
|
+
return renderInputFields({
|
|
328
|
+
models,
|
|
329
|
+
model,
|
|
330
|
+
fields: model.fields.filter(f => f.fieldId === "extensions"),
|
|
331
|
+
fieldTypePlugins
|
|
332
|
+
});
|
|
333
|
+
}
|
|
185
334
|
}
|
|
186
335
|
|
|
336
|
+
// Note: constructor DI needed here because of PluginsContainer + ListModelsUseCase
|
|
187
337
|
export default GraphQLSchemaFactory.createImplementation({
|
|
188
|
-
implementation:
|
|
338
|
+
implementation: CreateEntitySchema,
|
|
339
|
+
dependencies: [PluginsContainer, ListModelsUseCase]
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Permission Transformer (Adding CMS Permissions)
|
|
346
|
+
|
|
347
|
+
When your package needs CMS access, implement a `PermissionTransformer` to expand your custom permission into the required CMS permissions:
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
// features/addCmsPermissions/AddCmsPermissions.ts
|
|
351
|
+
import { PermissionTransformer } from "@webiny/api-core/features/security/authorization/AuthorizationContext/abstractions.js";
|
|
352
|
+
|
|
353
|
+
class AddCmsPermissions implements PermissionTransformer.Interface {
|
|
354
|
+
execute(permission: PermissionTransformer.Permission) {
|
|
355
|
+
if (permission.name !== "mypackage.*") {
|
|
356
|
+
return permission;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return [
|
|
360
|
+
permission,
|
|
361
|
+
{ name: "cms.endpoint.manage" },
|
|
362
|
+
{ name: "cms.contentModel", own: false, rwd: "r", pw: "", models: ["myEntityModelId"] },
|
|
363
|
+
{ name: "cms.contentModelGroup", own: false, rwd: "r", pw: "", groups: ["hidden"] },
|
|
364
|
+
{ name: "cms.contentEntry", own: false, rwd: "rwd", pw: "" }
|
|
365
|
+
];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export default PermissionTransformer.createImplementation({
|
|
370
|
+
implementation: AddCmsPermissions,
|
|
189
371
|
dependencies: []
|
|
190
372
|
});
|
|
191
373
|
```
|
|
192
374
|
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Key Rules
|
|
378
|
+
|
|
379
|
+
- Implement `GraphQLSchemaFactory.Interface`
|
|
380
|
+
- Use `builder.addTypeDefs()` for schema definitions and `builder.addResolver()` for resolvers
|
|
381
|
+
- Resolver `dependencies` array lists DI abstractions; resolver function receives resolved instances in same order
|
|
382
|
+
- Type the resolver args generic: `builder.addResolver<{ input: UseCaseAbstraction.Input }>`
|
|
383
|
+
- The root Query/Mutation types define a namespace type (e.g., `MyPackageQuery`, `MyPackageMutation`) extended by individual schemas
|
|
384
|
+
- Use `Response` for success, `ErrorResponse` for failure (from `@webiny/handler-graphql`)
|
|
385
|
+
- Export as `default`
|
|
386
|
+
|
|
193
387
|
## Quick Reference
|
|
194
388
|
|
|
195
389
|
```
|
|
@@ -200,10 +394,12 @@ Return: Promise<GraphQLSchemaFactory.SchemaBuilder>
|
|
|
200
394
|
Export: GraphQLSchemaFactory.createImplementation({ implementation, dependencies })
|
|
201
395
|
Register: <Api.Extension src={"@/extensions/mySchema/MyGraphQLSchema.ts"} />
|
|
202
396
|
Deploy: yarn webiny deploy api --env=dev
|
|
397
|
+
Response: import { Response, ErrorResponse } from "@webiny/handler-graphql"
|
|
203
398
|
```
|
|
204
399
|
|
|
205
400
|
## Related Skills
|
|
206
401
|
|
|
207
|
-
-
|
|
208
|
-
-
|
|
209
|
-
-
|
|
402
|
+
- **webiny-api-architect** — Architecture overview, Services vs UseCases, feature naming, anti-patterns
|
|
403
|
+
- **webiny-use-case-pattern** — UseCase implementation consumed by GraphQL resolvers
|
|
404
|
+
- **webiny-dependency-injection** — Full DI reference for all injectable services
|
|
405
|
+
- **webiny-project-structure** — How to register extensions in `webiny.config.tsx`
|