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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/agents/claude.d.ts +2 -0
  2. package/agents/claude.js +6 -0
  3. package/agents/claude.js.map +1 -1
  4. package/agents/cline.d.ts +2 -0
  5. package/agents/cline.js +6 -0
  6. package/agents/cline.js.map +1 -1
  7. package/agents/copilot.d.ts +2 -0
  8. package/agents/copilot.js +7 -0
  9. package/agents/copilot.js.map +1 -1
  10. package/agents/cursor.d.ts +2 -0
  11. package/agents/cursor.js +6 -0
  12. package/agents/cursor.js.map +1 -1
  13. package/agents/discover.d.ts +3 -0
  14. package/agents/discover.js +29 -0
  15. package/agents/discover.js.map +1 -0
  16. package/agents/instructions.d.ts +3 -1
  17. package/agents/instructions.js +15 -2
  18. package/agents/instructions.js.map +1 -1
  19. package/agents/kiro.d.ts +2 -0
  20. package/agents/kiro.js +6 -0
  21. package/agents/kiro.js.map +1 -1
  22. package/agents/opencode.d.ts +2 -0
  23. package/agents/opencode.js +7 -0
  24. package/agents/opencode.js.map +1 -1
  25. package/agents/types.d.ts +16 -0
  26. package/agents/types.js +3 -0
  27. package/agents/types.js.map +1 -0
  28. package/agents/windsurf.d.ts +2 -0
  29. package/agents/windsurf.js +6 -0
  30. package/agents/windsurf.js.map +1 -1
  31. package/cli/ConfigureMcp.js +21 -6
  32. package/cli/ConfigureMcp.js.map +1 -1
  33. package/package.json +3 -3
  34. package/skills/admin/admin-architect/SKILL.md +188 -0
  35. package/skills/admin/admin-permissions/SKILL.md +159 -0
  36. package/skills/api/api-architect/SKILL.md +548 -60
  37. package/skills/api/event-handler-pattern/SKILL.md +191 -23
  38. package/skills/api/graphql-api/SKILL.md +227 -31
  39. package/skills/api/permissions/SKILL.md +291 -0
  40. package/skills/api/use-case-pattern/SKILL.md +347 -12
  41. package/skills/api/v5-to-v6-migration/SKILL.md +416 -0
  42. package/skills/generated/admin/SKILL.md +1 -1
  43. package/skills/generated/admin/aco/SKILL.md +1 -1
  44. package/skills/generated/admin/build-params/SKILL.md +1 -1
  45. package/skills/generated/admin/cms/SKILL.md +1 -1
  46. package/skills/generated/admin/configs/SKILL.md +1 -1
  47. package/skills/generated/admin/env-config/SKILL.md +1 -1
  48. package/skills/generated/admin/form/SKILL.md +1 -1
  49. package/skills/generated/admin/graphql-client/SKILL.md +1 -1
  50. package/skills/generated/admin/lexical/SKILL.md +1 -1
  51. package/skills/generated/admin/local-storage/SKILL.md +1 -1
  52. package/skills/generated/admin/router/SKILL.md +1 -1
  53. package/skills/generated/admin/security/SKILL.md +1 -1
  54. package/skills/generated/admin/tenancy/SKILL.md +1 -1
  55. package/skills/generated/admin/ui/SKILL.md +1 -1
  56. package/skills/generated/admin/website-builder/SKILL.md +1 -1
  57. package/skills/generated/api/SKILL.md +1 -1
  58. package/skills/generated/api/aco/SKILL.md +1 -1
  59. package/skills/generated/api/build-params/SKILL.md +1 -1
  60. package/skills/generated/api/cms/SKILL.md +1 -1
  61. package/skills/generated/api/event-publisher/SKILL.md +1 -1
  62. package/skills/generated/api/file-manager/SKILL.md +1 -1
  63. package/skills/generated/api/graphql/SKILL.md +1 -1
  64. package/skills/generated/api/key-value-store/SKILL.md +1 -1
  65. package/skills/generated/api/logger/SKILL.md +1 -1
  66. package/skills/generated/api/opensearch/SKILL.md +1 -1
  67. package/skills/generated/api/scheduler/SKILL.md +1 -1
  68. package/skills/generated/api/security/SKILL.md +1 -1
  69. package/skills/generated/api/system/SKILL.md +1 -1
  70. package/skills/generated/api/tasks/SKILL.md +1 -1
  71. package/skills/generated/api/tenancy/SKILL.md +1 -1
  72. package/skills/generated/api/tenant-manager/SKILL.md +1 -1
  73. package/skills/generated/api/website-builder/SKILL.md +1 -1
  74. package/skills/generated/cli/SKILL.md +1 -1
  75. package/skills/generated/cli/command/SKILL.md +1 -1
  76. package/skills/generated/extensions/SKILL.md +1 -1
  77. package/skills/generated/infra/SKILL.md +1 -1
@@ -2,8 +2,10 @@
2
2
  name: webiny-event-handler-pattern
3
3
  context: webiny-api
4
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).
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 (abstraction). The real logic lives in a dedicated service defined with `createAbstraction` and `createFeature`.
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. Wrapping it in an abstraction makes it injectable, testable, and replaceable.
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
- └── myHandler/ ← thin handler that injects the service
46
+ └── syncOnCreate/ ← thin handler that injects the service
47
47
  ├── feature.ts
48
- └── MyHandler.ts
48
+ └── EntryAfterCreateHandler.ts
49
49
  ```
50
50
 
51
- The EventHandler feature and the service feature are **registered separately** in `Extension.tsx`.
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: always filter by model
67
- // if (entity.modelId !== "myModel") return;
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 your own custom abstractions:
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
- 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.
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
- or inject backend services (identity, tenancy, CMS use-cases) into their GraphQL schema.
9
- Covers the full pattern from simple queries to complex resolvers with dependency injection.
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 the `GraphQLSchemaFactory` builder pattern. Implement `GraphQLSchemaFactory.Interface`, use the builder to add type definitions and resolvers (with per-resolver DI), and export with `GraphQLSchemaFactory.createImplementation()`. Register as `<Api.Extension>`.
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 `builder` (`GraphQLSchemaBuilder.Interface`) and returns it after adding type defs and resolvers.
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 -- it gives each resolver access to request-scoped services like identity and tenant context.
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
- ## Nested Mutation Pattern
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
- For namespaced mutations (e.g. `mutation { tenantManager { installTenant } }`), add a pass-through resolver for the namespace type:
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 { GraphQLSchemaFactory } from "webiny/api/graphql";
139
- import { IdentityContext } from "webiny/api/security";
140
- import { MyService } from "@/extensions/myFeature/MyFeature.js";
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 OrderSchema implements GraphQLSchemaFactory.Interface {
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 Order {
153
+ type EntityResponse {
154
+ data: Entity
155
+ error: Error
156
+ }
157
+
158
+ type Entity {
148
159
  id: ID!
149
- total: Float!
160
+ values: JSON!
150
161
  }
151
162
 
152
- type OrderMutation {
153
- create(total: Float!): Order
163
+ type MyPackageQuery {
164
+ getCurrentEntity: EntityResponse
154
165
  }
155
166
 
156
- extend type Mutation {
157
- orders: OrderMutation
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: "Mutation.orders",
174
+ path: "Query.myPackage",
164
175
  resolver: () => {
165
176
  return () => ({});
166
177
  }
167
178
  });
168
179
 
169
- // Actual mutation with DI
170
- builder.addResolver<{ total: number }>({
171
- path: "OrderMutation.create",
172
- dependencies: [IdentityContext, MyService],
173
- resolver: (identityContext: IdentityContext.Interface, myService: MyService.Interface) => {
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
- if (!identityContext.getPermission("orders.create")) {
176
- throw new Error("Not authorized");
301
+ const result = await createEntity.execute(args.input);
302
+ if (result.isFail()) {
303
+ return new ErrorResponse(result.error);
177
304
  }
178
- return { id: "order-1", total: args.total };
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: OrderSchema,
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
- - `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`
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`