@webiny/mcp 6.0.0-rc.5 → 6.0.0-rc.7
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/package.json +4 -4
- package/skills/api-custom-feature/SKILL.md +195 -0
- package/skills/configure-auth0/SKILL.md +302 -0
- package/skills/configure-okta/SKILL.md +303 -0
- package/skills/custom-graphql-api/SKILL.md +145 -135
- package/skills/dependency-injection/SKILL.md +277 -149
- package/skills/infrastructure-extensions/SKILL.md +84 -66
- package/skills/lifecycle-events/SKILL.md +151 -6
- package/skills/local-development/SKILL.md +25 -16
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webiny-configure-okta
|
|
3
|
+
description: >
|
|
4
|
+
Configuring Okta as an identity provider (IDP) for Webiny projects.
|
|
5
|
+
Use this skill when the developer asks about Okta authentication, Okta SSO,
|
|
6
|
+
replacing Cognito with Okta, setting up external identity providers, configuring
|
|
7
|
+
OIDC authentication, mapping JWT claims to Webiny identities, or customizing
|
|
8
|
+
the Okta login flow. Also relevant when asking about OKTA_ISSUER, OKTA_CLIENT_ID
|
|
9
|
+
environment variables, OktaIdpConfig, or the MyOktaExtension pattern.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Configure Okta Authentication
|
|
13
|
+
|
|
14
|
+
## TL;DR
|
|
15
|
+
|
|
16
|
+
Webiny supports Okta as an external identity provider (IDP) to replace the default Cognito authentication. First, install the `@webiny/okta` package (using the same version as the `webiny` dependency in `package.json`). Then create two files: an API config class that maps Okta JWT claims to Webiny identity data (`OktaIdpConfig`), and a React extension component (`<Okta />`) that wires issuer URL, client ID, and the API config path. Register the extension in `webiny.config.tsx`, set two environment variables (`OKTA_ISSUER`, `OKTA_CLIENT_ID`), and deploy.
|
|
17
|
+
|
|
18
|
+
## Pattern / Core Concept
|
|
19
|
+
|
|
20
|
+
Okta integration has two parts:
|
|
21
|
+
|
|
22
|
+
1. **API Config** — A class implementing `OktaIdpConfig.Interface` that maps JWT token claims to Webiny's identity structure. Registered via `OktaIdpConfig.createImplementation()` (the universal DI pattern).
|
|
23
|
+
2. **Extension Component** — A React component that renders `<Okta />` from `@webiny/okta`, passing the issuer URL, client ID, and path to the API config file. The `<Okta />` component handles environment variable injection, API extension registration, and Admin login screen setup automatically.
|
|
24
|
+
|
|
25
|
+
### How `<Okta />` Works Internally
|
|
26
|
+
|
|
27
|
+
The `<Okta />` component (from `@webiny/okta`) is a `defineExtension` that:
|
|
28
|
+
|
|
29
|
+
- Sets Lambda env vars: `OKTA_ISSUER`, `OKTA_CLIENT_ID`
|
|
30
|
+
- Sets Admin app env vars: `REACT_APP_IDP_TYPE=okta`, `REACT_APP_OKTA_ISSUER`, `REACT_APP_OKTA_CLIENT_ID`
|
|
31
|
+
- Registers the internal `OktaIdpFeature` API extension (OIDC token verification)
|
|
32
|
+
- Registers your custom API config extension (identity mapping)
|
|
33
|
+
- Registers the Admin Okta login screen extension
|
|
34
|
+
|
|
35
|
+
## Reference Tables
|
|
36
|
+
|
|
37
|
+
### `OktaIdpConfig.Interface`
|
|
38
|
+
|
|
39
|
+
| Method | Signature | Required | Description |
|
|
40
|
+
| ------------------- | -------------------------------------------------------------- | -------- | ----------------------------------------------------- |
|
|
41
|
+
| `getIdentity` | `(token: JwtPayload) => OktaIdentity \| Promise<OktaIdentity>` | Yes | Maps JWT claims to Webiny identity data |
|
|
42
|
+
| `verifyTokenClaims` | `(token: JwtPayload) => void \| Promise<void>` | No | Custom claim verification (throw to reject the token) |
|
|
43
|
+
|
|
44
|
+
### `OktaIdentity` (Return Type of `getIdentity`)
|
|
45
|
+
|
|
46
|
+
| Field | Type | Description |
|
|
47
|
+
| ------------- | -------------------------------- | ------------------------------------------------ |
|
|
48
|
+
| `id` | `string` | Unique user ID (typically `token["sub"]`) |
|
|
49
|
+
| `displayName` | `string` | User's display name |
|
|
50
|
+
| `roles` | `string[]` | Webiny security roles to assign |
|
|
51
|
+
| `teams` | `string[]` | Webiny teams (optional, filter out falsy values) |
|
|
52
|
+
| `profile` | `{ firstName, lastName, email }` | User profile fields |
|
|
53
|
+
| `context` | `object` | Runtime data (not stored in DB) |
|
|
54
|
+
|
|
55
|
+
### `<Okta />` Component Props
|
|
56
|
+
|
|
57
|
+
| Prop | Type | Description |
|
|
58
|
+
| ----------- | -------- | -------------------------------------------------- |
|
|
59
|
+
| `issuer` | `string` | Okta issuer URL (e.g., `https://dev-xxx.okta.com`) |
|
|
60
|
+
| `clientId` | `string` | Okta application client ID |
|
|
61
|
+
| `apiConfig` | `string` | Absolute path to the API config file |
|
|
62
|
+
|
|
63
|
+
### Environment Variables
|
|
64
|
+
|
|
65
|
+
| Variable | Used By | Description |
|
|
66
|
+
| ---------------- | ----------- | -------------------------- |
|
|
67
|
+
| `OKTA_ISSUER` | API + Admin | Okta issuer URL |
|
|
68
|
+
| `OKTA_CLIENT_ID` | API + Admin | Okta application client ID |
|
|
69
|
+
|
|
70
|
+
## Full Examples
|
|
71
|
+
|
|
72
|
+
### Example 1: Basic Okta Configuration
|
|
73
|
+
|
|
74
|
+
**Step 0: Install the `@webiny/okta` dependency**
|
|
75
|
+
|
|
76
|
+
`@webiny/okta` is an optional dependency. Add it to `package.json` using the same version as the `webiny` dependency, then install:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Check the webiny version in package.json, then add @webiny/okta with the same version
|
|
80
|
+
# For example, if "webiny": "^0.0.0-unstable.xxx":
|
|
81
|
+
yarn add @webiny/okta@^0.0.0-unstable.xxx
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
> **Important:** After adding the dependency, tell the user to run `yarn` to install it. Do NOT run `yarn` automatically — let the user do it.
|
|
85
|
+
|
|
86
|
+
**Step 1: Create the API config**
|
|
87
|
+
|
|
88
|
+
Create `extensions/okta/MyOktaConfig.ts`:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { OktaIdpConfig } from "@webiny/okta";
|
|
92
|
+
|
|
93
|
+
class MyIdpConfig implements OktaIdpConfig.Interface {
|
|
94
|
+
getIdentity(token: OktaIdpConfig.JwtPayload) {
|
|
95
|
+
return {
|
|
96
|
+
id: String(token["sub"]),
|
|
97
|
+
displayName: token["name"],
|
|
98
|
+
roles: [token["webiny_group"]],
|
|
99
|
+
teams: [token["team"]].filter(Boolean),
|
|
100
|
+
profile: {
|
|
101
|
+
firstName: token["first_name"],
|
|
102
|
+
lastName: token["last_name"],
|
|
103
|
+
email: token["email"]
|
|
104
|
+
},
|
|
105
|
+
context: {
|
|
106
|
+
canAccessTenant: true,
|
|
107
|
+
defaultTenant: "root"
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const MyOktaConfig = OktaIdpConfig.createImplementation({
|
|
114
|
+
implementation: MyIdpConfig,
|
|
115
|
+
dependencies: []
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export default MyOktaConfig;
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Step 2: Create the extension component**
|
|
122
|
+
|
|
123
|
+
Create `extensions/okta/MyOktaExtension.tsx`:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
import React from "react";
|
|
127
|
+
import { Okta } from "@webiny/okta";
|
|
128
|
+
|
|
129
|
+
export const MyOktaExtension = () => {
|
|
130
|
+
return (
|
|
131
|
+
<Okta
|
|
132
|
+
issuer={String(process.env.OKTA_ISSUER)}
|
|
133
|
+
clientId={String(process.env.OKTA_CLIENT_ID)}
|
|
134
|
+
apiConfig={import.meta.dirname + "/MyOktaConfig.ts"}
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Step 3: Register in `webiny.config.tsx`**
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import React from "react";
|
|
144
|
+
import { MyOktaExtension } from "./extensions/okta/MyOktaExtension.js";
|
|
145
|
+
|
|
146
|
+
export const Extensions = () => {
|
|
147
|
+
return (
|
|
148
|
+
<>
|
|
149
|
+
{/* Replace <Cognito /> with Okta */}
|
|
150
|
+
<MyOktaExtension />
|
|
151
|
+
|
|
152
|
+
{/* ... other extensions ... */}
|
|
153
|
+
</>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Step 4: Set environment variables**
|
|
159
|
+
|
|
160
|
+
Add to your `.env` file (or CI/CD environment):
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
OKTA_ISSUER=https://dev-xxxxx.okta.com/oauth2/default
|
|
164
|
+
OKTA_CLIENT_ID=your-okta-client-id
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Step 5: Deploy**
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
yarn webiny deploy
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Example 2: Custom Claim Verification
|
|
174
|
+
|
|
175
|
+
If your Okta setup uses custom claims that need validation:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { OktaIdpConfig } from "@webiny/okta";
|
|
179
|
+
|
|
180
|
+
class MyIdpConfig implements OktaIdpConfig.Interface {
|
|
181
|
+
getIdentity(token: OktaIdpConfig.JwtPayload) {
|
|
182
|
+
return {
|
|
183
|
+
id: String(token["sub"]),
|
|
184
|
+
displayName: token["name"],
|
|
185
|
+
roles: [token["webiny_role"]],
|
|
186
|
+
profile: {
|
|
187
|
+
firstName: token["given_name"],
|
|
188
|
+
lastName: token["family_name"],
|
|
189
|
+
email: token["email"]
|
|
190
|
+
},
|
|
191
|
+
context: {
|
|
192
|
+
canAccessTenant: true,
|
|
193
|
+
defaultTenant: "root"
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
verifyTokenClaims(token: OktaIdpConfig.JwtPayload) {
|
|
199
|
+
// Reject tokens without the required custom claim
|
|
200
|
+
if (!token["webiny_role"]) {
|
|
201
|
+
throw new Error("Token is missing the 'webiny_role' claim.");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Reject tokens from unauthorized organizations
|
|
205
|
+
const allowedOrgs = ["org_abc123", "org_def456"];
|
|
206
|
+
if (!allowedOrgs.includes(token["org_id"] as string)) {
|
|
207
|
+
throw new Error("User does not belong to an authorized organization.");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const MyOktaConfig = OktaIdpConfig.createImplementation({
|
|
213
|
+
implementation: MyIdpConfig,
|
|
214
|
+
dependencies: []
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
export default MyOktaConfig;
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Example 3: Using DI Dependencies in Config
|
|
221
|
+
|
|
222
|
+
If your config needs access to other Webiny services (e.g., to look up tenant-specific roles):
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import { OktaIdpConfig } from "@webiny/okta";
|
|
226
|
+
import { TenantContext } from "webiny/api/tenancy";
|
|
227
|
+
|
|
228
|
+
class MyIdpConfig implements OktaIdpConfig.Interface {
|
|
229
|
+
constructor(private tenantContext: TenantContext.Interface) {}
|
|
230
|
+
|
|
231
|
+
getIdentity(token: OktaIdpConfig.JwtPayload) {
|
|
232
|
+
const tenant = this.tenantContext.getTenant();
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
id: String(token["sub"]),
|
|
236
|
+
displayName: token["name"],
|
|
237
|
+
roles: [token["webiny_group"]],
|
|
238
|
+
profile: {
|
|
239
|
+
firstName: token["first_name"],
|
|
240
|
+
lastName: token["last_name"],
|
|
241
|
+
email: token["email"]
|
|
242
|
+
},
|
|
243
|
+
context: {
|
|
244
|
+
canAccessTenant: true,
|
|
245
|
+
defaultTenant: tenant?.id ?? "root"
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const MyOktaConfig = OktaIdpConfig.createImplementation({
|
|
252
|
+
implementation: MyIdpConfig,
|
|
253
|
+
dependencies: [TenantContext]
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
export default MyOktaConfig;
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Quick Reference
|
|
260
|
+
|
|
261
|
+
### Imports
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// API config
|
|
265
|
+
import { OktaIdpConfig } from "@webiny/okta";
|
|
266
|
+
|
|
267
|
+
// Extension component
|
|
268
|
+
import { Okta } from "@webiny/okta";
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Key Interfaces
|
|
272
|
+
|
|
273
|
+
| Interface | Package | Purpose |
|
|
274
|
+
| ---------------------------- | -------------- | -------------------------------- |
|
|
275
|
+
| `OktaIdpConfig.Interface` | `@webiny/okta` | API-side JWT-to-identity mapping |
|
|
276
|
+
| `OktaIdpConfig.JwtPayload` | `@webiny/okta` | JWT token payload type |
|
|
277
|
+
| `OktaIdpConfig.IdentityData` | `@webiny/okta` | Identity return type |
|
|
278
|
+
|
|
279
|
+
### File Structure
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
extensions/okta/
|
|
283
|
+
├── MyOktaConfig.ts # API config (JWT claim mapping)
|
|
284
|
+
└── MyOktaExtension.tsx # Extension component (Okta setup)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Registration
|
|
288
|
+
|
|
289
|
+
In `webiny.config.tsx`, replace `<Cognito />` with `<MyOktaExtension />`.
|
|
290
|
+
|
|
291
|
+
### Deploy
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
yarn webiny deploy # Deploy all (Core + API + Admin)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Both API and Admin need to be redeployed since Okta affects both the backend (token verification, identity mapping) and the frontend (login screen).
|
|
298
|
+
|
|
299
|
+
## Related Skills
|
|
300
|
+
|
|
301
|
+
- **dependency-injection** — The universal DI pattern used by `OktaIdpConfig.createImplementation()`
|
|
302
|
+
- **project-structure** — How `webiny.config.tsx` and extensions are organized
|
|
303
|
+
- **local-development** — Deploying and testing your Okta configuration
|
|
@@ -13,187 +13,197 @@ description: >
|
|
|
13
13
|
|
|
14
14
|
## TL;DR
|
|
15
15
|
|
|
16
|
-
Add custom GraphQL queries and mutations using the `GraphQLSchemaFactory` pattern.
|
|
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
17
|
|
|
18
18
|
## The GraphQLSchemaFactory Pattern
|
|
19
19
|
|
|
20
|
+
The `execute` method receives a `builder` (`GraphQLSchemaBuilder.Interface`) and returns it after adding type defs and resolvers.
|
|
21
|
+
|
|
20
22
|
```typescript
|
|
21
|
-
// extensions/MyGraphQLSchema.ts
|
|
23
|
+
// extensions/mySchema/MyGraphQLSchema.ts
|
|
22
24
|
import { GraphQLSchemaFactory } from "webiny/api/graphql";
|
|
23
25
|
|
|
24
|
-
class
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
26
|
+
class MySchema implements GraphQLSchemaFactory.Interface {
|
|
27
|
+
async execute(
|
|
28
|
+
builder: GraphQLSchemaFactory.SchemaBuilder
|
|
29
|
+
): Promise<GraphQLSchemaFactory.SchemaBuilder> {
|
|
30
|
+
builder.addTypeDefs(/* GraphQL */ `
|
|
31
|
+
extend type Query {
|
|
32
|
+
hello: String!
|
|
33
|
+
}
|
|
34
|
+
`);
|
|
35
|
+
|
|
36
|
+
builder.addResolver({
|
|
37
|
+
path: "Query.hello",
|
|
38
|
+
resolver: () => {
|
|
39
|
+
return () => "Hello, World!";
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return builder;
|
|
44
|
+
}
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
export default GraphQLSchemaFactory.createImplementation({
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
implementation: MySchema,
|
|
49
|
+
dependencies: []
|
|
46
50
|
});
|
|
47
51
|
```
|
|
48
52
|
|
|
49
|
-
Register
|
|
53
|
+
Register as an extension:
|
|
50
54
|
|
|
51
55
|
```tsx
|
|
52
|
-
|
|
56
|
+
// extensions/mySchema/Extension.tsx
|
|
57
|
+
import React from "react";
|
|
58
|
+
import { Api } from "webiny/extensions";
|
|
59
|
+
|
|
60
|
+
export const MySchema = () => {
|
|
61
|
+
return <Api.Extension src={"@/extensions/mySchema/MyGraphQLSchema.ts"} />;
|
|
62
|
+
};
|
|
53
63
|
```
|
|
54
64
|
|
|
55
|
-
##
|
|
65
|
+
## Builder API Reference
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
| Method | Description |
|
|
68
|
+
| --------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
69
|
+
| `builder.addTypeDefs(typeDefs: string)` | Add GraphQL type definitions (use `extend type Query/Mutation` to add to existing root types) |
|
|
70
|
+
| `builder.addResolver<TArgs>(config)` | Add a resolver with optional per-resolver DI dependencies |
|
|
71
|
+
|
|
72
|
+
### `addResolver` Config
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
builder.addResolver<TArgs>({
|
|
76
|
+
path: "TypeName.fieldName", // dot-separated path
|
|
77
|
+
dependencies: [SomeAbstraction], // optional: DI tokens resolved at request time
|
|
78
|
+
resolver: (dep1, dep2, ...) => { // factory: receives resolved deps
|
|
79
|
+
return ({ parent, args, context, info }) => {
|
|
80
|
+
// actual resolver logic
|
|
81
|
+
return result;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Key points:
|
|
88
|
+
|
|
89
|
+
- **`path`**: Dot-separated GraphQL type path, e.g. `"Query.hello"`, `"Mutation.createOrder"`, `"OrderMutation.create"`
|
|
90
|
+
- **`dependencies`**: Array of DI abstraction tokens. Resolved **per-request** from `context.container`, not at schema build time
|
|
91
|
+
- **`resolver`**: A factory function that receives resolved dependencies and returns the actual resolver function
|
|
92
|
+
- **Resolver params**: The inner function receives `{ parent, args, context, info }` (named object, not positional)
|
|
93
|
+
|
|
94
|
+
## Per-Resolver Dependency Injection
|
|
95
|
+
|
|
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.
|
|
58
97
|
|
|
59
98
|
```typescript
|
|
60
|
-
// extensions/MyGraphQLSchema.ts
|
|
61
99
|
import { GraphQLSchemaFactory } from "webiny/api/graphql";
|
|
62
100
|
import { IdentityContext } from "webiny/api/security";
|
|
63
101
|
|
|
64
|
-
class
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
102
|
+
class WhoAmISchema implements GraphQLSchemaFactory.Interface {
|
|
103
|
+
async execute(
|
|
104
|
+
builder: GraphQLSchemaFactory.SchemaBuilder
|
|
105
|
+
): Promise<GraphQLSchemaFactory.SchemaBuilder> {
|
|
106
|
+
builder.addTypeDefs(/* GraphQL */ `
|
|
107
|
+
extend type Query {
|
|
108
|
+
whoAmI: String
|
|
109
|
+
}
|
|
110
|
+
`);
|
|
111
|
+
|
|
112
|
+
builder.addResolver({
|
|
113
|
+
path: "Query.whoAmI",
|
|
114
|
+
dependencies: [IdentityContext],
|
|
115
|
+
resolver: (identityContext: IdentityContext.Interface) => {
|
|
116
|
+
return () => {
|
|
117
|
+
const identity = identityContext.getIdentity();
|
|
118
|
+
return `Hello, ${identity.displayName}!`;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return builder;
|
|
124
|
+
}
|
|
86
125
|
}
|
|
87
126
|
|
|
88
127
|
export default GraphQLSchemaFactory.createImplementation({
|
|
89
|
-
|
|
90
|
-
|
|
128
|
+
implementation: WhoAmISchema,
|
|
129
|
+
dependencies: []
|
|
91
130
|
});
|
|
92
131
|
```
|
|
93
132
|
|
|
94
|
-
##
|
|
133
|
+
## Nested Mutation Pattern
|
|
95
134
|
|
|
96
|
-
|
|
135
|
+
For namespaced mutations (e.g. `mutation { tenantManager { installTenant } }`), add a pass-through resolver for the namespace type:
|
|
97
136
|
|
|
98
137
|
```typescript
|
|
99
138
|
import { GraphQLSchemaFactory } from "webiny/api/graphql";
|
|
100
|
-
import {
|
|
101
|
-
import {
|
|
102
|
-
|
|
103
|
-
class
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
139
|
+
import { IdentityContext } from "webiny/api/security";
|
|
140
|
+
import { MyService } from "@/extensions/myFeature/MyFeature.js";
|
|
141
|
+
|
|
142
|
+
class OrderSchema implements GraphQLSchemaFactory.Interface {
|
|
143
|
+
async execute(
|
|
144
|
+
builder: GraphQLSchemaFactory.SchemaBuilder
|
|
145
|
+
): Promise<GraphQLSchemaFactory.SchemaBuilder> {
|
|
146
|
+
builder.addTypeDefs(/* GraphQL */ `
|
|
147
|
+
type Order {
|
|
148
|
+
id: ID!
|
|
149
|
+
total: Float!
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
type OrderMutation {
|
|
153
|
+
create(total: Float!): Order
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
extend type Mutation {
|
|
157
|
+
orders: OrderMutation
|
|
158
|
+
}
|
|
159
|
+
`);
|
|
160
|
+
|
|
161
|
+
// Pass-through resolver for the namespace
|
|
162
|
+
builder.addResolver({
|
|
163
|
+
path: "Mutation.orders",
|
|
164
|
+
resolver: () => {
|
|
165
|
+
return () => ({});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
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) => {
|
|
174
|
+
return async ({ args }) => {
|
|
175
|
+
if (!identityContext.getPermission("orders.create")) {
|
|
176
|
+
throw new Error("Not authorized");
|
|
177
|
+
}
|
|
178
|
+
return { id: "order-1", total: args.total };
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return builder;
|
|
184
|
+
}
|
|
134
185
|
}
|
|
135
186
|
|
|
136
187
|
export default GraphQLSchemaFactory.createImplementation({
|
|
137
|
-
|
|
138
|
-
|
|
188
|
+
implementation: OrderSchema,
|
|
189
|
+
dependencies: []
|
|
139
190
|
});
|
|
140
191
|
```
|
|
141
192
|
|
|
142
|
-
## Available Injectable Services
|
|
143
|
-
|
|
144
|
-
### Core Features
|
|
145
|
-
|
|
146
|
-
| Feature | Import Path | Purpose |
|
|
147
|
-
|---|----------------------------------------------|---|
|
|
148
|
-
| `IdentityContext` | `"webiny/api/security"` | Access current user identity and permissions |
|
|
149
|
-
| `TenantContext` | `"@webiny/api-core/features/TenantContext"` | Access current tenant information |
|
|
150
|
-
| `EventPublisher` | `"@webiny/api-core/features/EventPublisher"` | Publish domain events |
|
|
151
|
-
| `WcpContext` | `"@webiny/api-core/features/WcpContext"` | Webiny Control Panel integration |
|
|
152
|
-
| `Logger` | `"webiny/api/logger"` | Logging (persists to CloudWatch) |
|
|
153
|
-
| `BuildParams` | `"webiny/api/build-params"` | Access build-time parameters |
|
|
154
|
-
|
|
155
|
-
### Headless CMS Use-Cases
|
|
156
|
-
|
|
157
|
-
| Feature | Import Path | Purpose |
|
|
158
|
-
|---|---|---|
|
|
159
|
-
| `GetEntryByIdUseCase` | `"@webiny/api-headless-cms/features/contentEntry/GetEntryById"` | Fetch entry by exact revision ID |
|
|
160
|
-
| `GetEntryUseCase` | `"@webiny/api-headless-cms/features/contentEntry/GetEntry"` | Get entry by query (where + sort) |
|
|
161
|
-
| `ListLatestEntriesUseCase` | `"@webiny/api-headless-cms/features/contentEntry/ListEntries"` | List latest entries |
|
|
162
|
-
| `ListPublishedEntriesUseCase` | `"@webiny/api-headless-cms/features/contentEntry/ListEntries"` | List published entries |
|
|
163
|
-
| `CreateEntryUseCase` | `"@webiny/api-headless-cms/features/contentEntry/CreateEntry"` | Create a new entry |
|
|
164
|
-
| `UpdateEntryUseCase` | `"@webiny/api-headless-cms/features/contentEntry/UpdateEntry"` | Update an existing entry |
|
|
165
|
-
| `DeleteEntryUseCase` | `"@webiny/api-headless-cms/features/contentEntry/DeleteEntry"` | Delete an entry |
|
|
166
|
-
| `GetModelUseCase` | `"@webiny/api-headless-cms/features/contentModel/GetModel"` | Retrieve a content model by ID |
|
|
167
|
-
| `ListModelsUseCase` | `"@webiny/api-headless-cms/features/contentModel/ListModels"` | List all accessible models |
|
|
168
|
-
|
|
169
|
-
### Settings
|
|
170
|
-
|
|
171
|
-
| Feature | Import Path | Purpose |
|
|
172
|
-
|---|---|---|
|
|
173
|
-
| `GetSettings` | `"@webiny/api-core/features/settings/GetSettings"` | Retrieve settings by name |
|
|
174
|
-
| `UpdateSettings` | `"@webiny/api-core/features/settings/UpdateSettings"` | Create or update settings |
|
|
175
|
-
|
|
176
|
-
### Tenancy
|
|
177
|
-
|
|
178
|
-
| Feature | Import Path | Purpose |
|
|
179
|
-
|---|---|---|
|
|
180
|
-
| `GetTenantByIdUseCase` | `"@webiny/api-core/features/tenancy/GetTenantById"` | Fetch a tenant by ID |
|
|
181
|
-
| `CreateTenantUseCase` | `"@webiny/api-core/features/tenancy/CreateTenant"` | Create a new tenant |
|
|
182
|
-
| `UpdateTenantUseCase` | `"@webiny/api-core/features/tenancy/UpdateTenant"` | Update a tenant |
|
|
183
|
-
| `DeleteTenantUseCase` | `"@webiny/api-core/features/tenancy/DeleteTenant"` | Delete a tenant |
|
|
184
|
-
|
|
185
193
|
## Quick Reference
|
|
186
194
|
|
|
187
195
|
```
|
|
188
196
|
Import: import { GraphQLSchemaFactory } from "webiny/api/graphql";
|
|
189
197
|
Interface: GraphQLSchemaFactory.Interface
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
198
|
+
Builder: GraphQLSchemaFactory.SchemaBuilder (param type for execute)
|
|
199
|
+
Return: Promise<GraphQLSchemaFactory.SchemaBuilder>
|
|
200
|
+
Export: GraphQLSchemaFactory.createImplementation({ implementation, dependencies })
|
|
201
|
+
Register: <Api.Extension src={"@/extensions/mySchema/MyGraphQLSchema.ts"} />
|
|
202
|
+
Deploy: yarn webiny deploy api --env=dev
|
|
194
203
|
```
|
|
195
204
|
|
|
196
205
|
## Related Skills
|
|
197
206
|
|
|
207
|
+
- `api-custom-feature` -- Define custom abstractions and services consumed by resolvers
|
|
198
208
|
- `dependency-injection` -- Full DI reference for all injectable services
|
|
199
209
|
- `project-structure` -- How to register extensions in `webiny.config.tsx`
|