@webiny/mcp 6.1.0-beta.3 → 6.2.0-beta.0
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/index.d.ts +2 -0
- package/index.js +2 -0
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/skills/admin/admin-permissions/SKILL.md +227 -73
- package/skills/admin/ui-extensions/SKILL.md +11 -42
- package/skills/api/api-architect/SKILL.md +2 -1
- package/skills/api/http-route/SKILL.md +176 -0
- package/skills/api/permissions/SKILL.md +141 -59
- package/skills/dependency-injection/SKILL.md +200 -2
- package/skills/generated/admin/SKILL.md +6 -16
- package/skills/generated/admin/cms/SKILL.md +39 -1
- package/skills/generated/admin/languages/SKILL.md +29 -0
- package/skills/generated/admin/security/SKILL.md +34 -1
- package/skills/generated/admin/ui/SKILL.md +21 -1
- package/skills/generated/admin/website-builder/SKILL.md +16 -1
- package/skills/generated/api/SKILL.md +58 -1
- package/skills/generated/api/cms/SKILL.md +111 -1
- package/skills/generated/api/db/SKILL.md +34 -0
- package/skills/generated/api/languages/SKILL.md +31 -0
- package/skills/generated/api/security/SKILL.md +26 -1
- package/skills/local-development/SKILL.md +1 -1
- package/skills/webiny-sdk/SKILL.md +174 -60
|
@@ -3,73 +3,128 @@ name: webiny-api-permissions
|
|
|
3
3
|
context: webiny-api
|
|
4
4
|
description: >
|
|
5
5
|
Schema-based permission system for API features. Use this skill when implementing
|
|
6
|
-
authorization in use cases, defining permission schemas with
|
|
6
|
+
authorization in use cases, defining permission schemas with createPermissionSchema,
|
|
7
|
+
creating injectable permissions via createPermissionsAbstraction/createPermissionsFeature,
|
|
7
8
|
checking read/write/delete/publish permissions, handling own-record scoping,
|
|
8
9
|
or testing permission scenarios. Covers the full pattern from schema definition
|
|
9
10
|
to use case integration to test matrices.
|
|
10
11
|
---
|
|
11
12
|
|
|
12
|
-
#
|
|
13
|
+
# API Permissions
|
|
13
14
|
|
|
14
15
|
## Overview
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
Permissions follow two layers: **domain** (schema) and **features** (DI abstractions + feature registration). Each package declares a permission schema and gets a typed `Permissions` abstraction injectable into use cases via DI. Methods like `canRead`, `canEdit`, `canDelete`, `canPublish`, `onlyOwnRecords` replace manual `identityContext.getPermission()` calls.
|
|
17
18
|
|
|
18
|
-
## Permission Schema
|
|
19
|
+
## Layer 1: Domain — Permission Schema
|
|
20
|
+
|
|
21
|
+
Define the schema in `src/domain/permissionsSchema.ts`:
|
|
19
22
|
|
|
20
23
|
```ts
|
|
21
|
-
|
|
22
|
-
import { createPermissions } from "@webiny/api-core/features/security/permissions/index.js";
|
|
23
|
-
import type { Permissions } from "@webiny/api-core/features/security/permissions/index.js";
|
|
24
|
+
import { createPermissionSchema } from "webiny/api/security";
|
|
24
25
|
|
|
25
|
-
const
|
|
26
|
-
prefix: "
|
|
27
|
-
fullAccess:
|
|
26
|
+
export const SM_PERMISSIONS_SCHEMA = createPermissionSchema({
|
|
27
|
+
prefix: "sm",
|
|
28
|
+
fullAccess: true,
|
|
28
29
|
entities: [
|
|
29
30
|
{
|
|
30
|
-
id: "
|
|
31
|
-
permission: "
|
|
32
|
-
scopes: ["full", "own"],
|
|
33
|
-
actions: [
|
|
34
|
-
{ name: "rwd" }, // Read/Write/Delete (chars: "r", "w", "d")
|
|
35
|
-
{ name: "pw" } // Publish/Unpublish (chars: "p", "u")
|
|
36
|
-
]
|
|
31
|
+
id: "product",
|
|
32
|
+
permission: "sm.product",
|
|
33
|
+
scopes: ["full", "own"],
|
|
34
|
+
actions: [{ name: "rwd" }, { name: "pw" }]
|
|
37
35
|
},
|
|
38
36
|
{
|
|
39
37
|
id: "settings",
|
|
40
|
-
permission: "
|
|
41
|
-
scopes: ["full"]
|
|
38
|
+
permission: "sm.settings",
|
|
39
|
+
scopes: ["full"]
|
|
42
40
|
}
|
|
43
41
|
]
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
type MySchema = typeof schema;
|
|
47
|
-
|
|
48
|
-
export const MyPermissions = createPermissions(schema);
|
|
49
|
-
|
|
50
|
-
export namespace MyPermissions {
|
|
51
|
-
export type Interface = Permissions<MySchema>;
|
|
52
|
-
}
|
|
42
|
+
});
|
|
53
43
|
```
|
|
54
44
|
|
|
55
|
-
`
|
|
45
|
+
The schema MUST use `as const` inference (handled by `createPermissionSchema`) for TypeScript to narrow entity IDs in method signatures.
|
|
56
46
|
|
|
57
47
|
### Schema Fields
|
|
58
48
|
|
|
59
|
-
| Field | Description
|
|
60
|
-
| ----------------------- |
|
|
61
|
-
| `prefix` | Namespaces the DI abstraction: `${prefix}:Permissions`
|
|
62
|
-
| `fullAccess
|
|
63
|
-
| `entities[].id` | Entity identifier used in method calls: `canRead("
|
|
64
|
-
| `entities[].permission` | Permission name matched against identity permissions
|
|
65
|
-
| `entities[].scopes` | `["full"]` or `["full", "own"]` — determines if own-scope supported
|
|
66
|
-
| `entities[].actions` | Action definitions — built-in: `"rwd"`, `"pw"`; custom: boolean flags
|
|
49
|
+
| Field | Description |
|
|
50
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
51
|
+
| `prefix` | Namespaces the DI abstraction: `${prefix}:Permissions` |
|
|
52
|
+
| `fullAccess` | `true` for standard full access. Pass an object with custom boolean flags for full-access extras (e.g., `{ canForceUnlock: true }`). |
|
|
53
|
+
| `entities[].id` | Entity identifier used in method calls: `canRead("product")` |
|
|
54
|
+
| `entities[].permission` | Permission name matched against identity permissions |
|
|
55
|
+
| `entities[].scopes` | `["full"]` or `["full", "own"]` — determines if own-scope supported |
|
|
56
|
+
| `entities[].actions` | Action definitions — built-in: `"rwd"`, `"pw"`; custom: boolean flags |
|
|
67
57
|
|
|
68
58
|
### Scopes
|
|
69
59
|
|
|
70
60
|
- **`"full"`** — User can access all records (default when no `own` flag on permission object)
|
|
71
61
|
- **`"own"`** — User can only access records where `createdBy.id === identity.id`
|
|
72
62
|
|
|
63
|
+
### Simple Apps (No Entities)
|
|
64
|
+
|
|
65
|
+
Omit `entities` for binary full/no access:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
export const MA_PERMISSIONS_SCHEMA = createPermissionSchema({
|
|
69
|
+
prefix: "ma",
|
|
70
|
+
fullAccess: true
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Layer 2: Features — DI Artifacts + Registration
|
|
77
|
+
|
|
78
|
+
### Abstraction (`src/features/permissions/abstractions.ts`)
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { createPermissionsAbstraction } from "webiny/api/security";
|
|
82
|
+
import type { Permissions } from "webiny/api/security";
|
|
83
|
+
import { SM_PERMISSIONS_SCHEMA } from "~/domain/permissionsSchema.js";
|
|
84
|
+
|
|
85
|
+
export const SmPermissions = createPermissionsAbstraction(SM_PERMISSIONS_SCHEMA);
|
|
86
|
+
|
|
87
|
+
export namespace SmPermissions {
|
|
88
|
+
export type Interface = Permissions<typeof SM_PERMISSIONS_SCHEMA>;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Feature (`src/features/permissions/feature.ts`)
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { createPermissionsFeature } from "webiny/api/security";
|
|
96
|
+
import { SM_PERMISSIONS_SCHEMA } from "~/domain/permissionsSchema.js";
|
|
97
|
+
import { SmPermissions } from "./abstractions.js";
|
|
98
|
+
|
|
99
|
+
export const SmPermissionsFeature = createPermissionsFeature(SM_PERMISSIONS_SCHEMA, SmPermissions);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Registration
|
|
103
|
+
|
|
104
|
+
Register the feature in your context plugin:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { SmPermissionsFeature } from "~/features/permissions/feature.js";
|
|
108
|
+
|
|
109
|
+
// In createContext:
|
|
110
|
+
SmPermissionsFeature.register(container);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## File Structure
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
src/
|
|
119
|
+
├── domain/
|
|
120
|
+
│ └── permissionsSchema.ts # createPermissionSchema()
|
|
121
|
+
├── features/
|
|
122
|
+
│ └── permissions/
|
|
123
|
+
│ ├── abstractions.ts # createPermissionsAbstraction() + namespace type
|
|
124
|
+
│ └── feature.ts # createPermissionsFeature()
|
|
125
|
+
└── index.ts # SmPermissionsFeature.register(container)
|
|
126
|
+
```
|
|
127
|
+
|
|
73
128
|
---
|
|
74
129
|
|
|
75
130
|
## Permission Methods
|
|
@@ -77,7 +132,7 @@ export namespace MyPermissions {
|
|
|
77
132
|
All methods follow a 3-tier bypass:
|
|
78
133
|
|
|
79
134
|
1. `identityContext.hasFullAccess()` → `name: "*"` permission (super admin)
|
|
80
|
-
2. `hasFullSchemaAccess()` → wildcard permission (e.g. `"
|
|
135
|
+
2. `hasFullSchemaAccess()` → wildcard permission (e.g. `"sm.*"`)
|
|
81
136
|
3. Entity-level permission check
|
|
82
137
|
|
|
83
138
|
### Method Reference
|
|
@@ -94,7 +149,7 @@ All methods follow a 3-tier bypass:
|
|
|
94
149
|
| `canUnpublish(entity)` | Unpublish permission | No | Checks `pw` includes `"u"` |
|
|
95
150
|
| `canAction(action, entity)` | Custom boolean action | No | Checks `permission[action] === true` |
|
|
96
151
|
|
|
97
|
-
All return `Promise<boolean>`.
|
|
152
|
+
All return `Promise<boolean>`. Entity IDs are fully typed — `canRead("bogus")` produces a type error.
|
|
98
153
|
|
|
99
154
|
### OwnableItem Interface
|
|
100
155
|
|
|
@@ -113,20 +168,20 @@ interface OwnableItem {
|
|
|
113
168
|
The Get use case is the **central ownership gate** — mutation use cases that delegate to GetById inherit ownership enforcement automatically.
|
|
114
169
|
|
|
115
170
|
```ts
|
|
116
|
-
import { Result } from "
|
|
171
|
+
import { Result } from "webiny/api";
|
|
117
172
|
import { GetByIdUseCase as UseCaseAbstraction, GetByIdRepository } from "./abstractions.js";
|
|
118
|
-
import {
|
|
173
|
+
import { SmPermissions } from "~/features/permissions/abstractions.js";
|
|
119
174
|
import { NotAuthorizedError } from "~/domain/errors.js";
|
|
120
175
|
|
|
121
176
|
class GetByIdUseCaseImpl implements UseCaseAbstraction.Interface {
|
|
122
177
|
constructor(
|
|
123
|
-
private permissions:
|
|
178
|
+
private permissions: SmPermissions.Interface,
|
|
124
179
|
private repository: GetByIdRepository.Interface
|
|
125
180
|
) {}
|
|
126
181
|
|
|
127
182
|
async execute(id: string): UseCaseAbstraction.Return {
|
|
128
183
|
// 1. Entity-level read check
|
|
129
|
-
if (!(await this.permissions.canRead("
|
|
184
|
+
if (!(await this.permissions.canRead("product"))) {
|
|
130
185
|
return Result.fail(new NotAuthorizedError());
|
|
131
186
|
}
|
|
132
187
|
|
|
@@ -137,7 +192,7 @@ class GetByIdUseCaseImpl implements UseCaseAbstraction.Interface {
|
|
|
137
192
|
}
|
|
138
193
|
|
|
139
194
|
// 3. Item-level ownership check
|
|
140
|
-
if (!(await this.permissions.canAccess("
|
|
195
|
+
if (!(await this.permissions.canAccess("product", result.value))) {
|
|
141
196
|
return Result.fail(new NotAuthorizedError());
|
|
142
197
|
}
|
|
143
198
|
|
|
@@ -147,31 +202,31 @@ class GetByIdUseCaseImpl implements UseCaseAbstraction.Interface {
|
|
|
147
202
|
|
|
148
203
|
export const GetByIdUseCase = UseCaseAbstraction.createImplementation({
|
|
149
204
|
implementation: GetByIdUseCaseImpl,
|
|
150
|
-
dependencies: [
|
|
205
|
+
dependencies: [SmPermissions, GetByIdRepository]
|
|
151
206
|
});
|
|
152
207
|
```
|
|
153
208
|
|
|
154
209
|
### List Use Case (Read + Own Records Filter)
|
|
155
210
|
|
|
156
211
|
```ts
|
|
157
|
-
import { IdentityContext } from "
|
|
212
|
+
import { IdentityContext } from "webiny/api/security";
|
|
158
213
|
|
|
159
214
|
class ListUseCaseImpl implements UseCaseAbstraction.Interface {
|
|
160
215
|
constructor(
|
|
161
|
-
private permissions:
|
|
216
|
+
private permissions: SmPermissions.Interface,
|
|
162
217
|
private identityContext: IdentityContext.Interface,
|
|
163
218
|
private repository: ListRepository.Interface
|
|
164
219
|
) {}
|
|
165
220
|
|
|
166
221
|
async execute(params: UseCaseAbstraction.Params): UseCaseAbstraction.Return {
|
|
167
|
-
if (!(await this.permissions.canRead("
|
|
222
|
+
if (!(await this.permissions.canRead("product"))) {
|
|
168
223
|
return Result.fail(new NotAuthorizedError());
|
|
169
224
|
}
|
|
170
225
|
|
|
171
226
|
const where = { ...params.where };
|
|
172
227
|
|
|
173
228
|
// Filter to own records if needed
|
|
174
|
-
if (await this.permissions.onlyOwnRecords("
|
|
229
|
+
if (await this.permissions.onlyOwnRecords("product")) {
|
|
175
230
|
const identity = this.identityContext.getIdentity();
|
|
176
231
|
where.createdBy = identity.id;
|
|
177
232
|
}
|
|
@@ -181,7 +236,7 @@ class ListUseCaseImpl implements UseCaseAbstraction.Interface {
|
|
|
181
236
|
}
|
|
182
237
|
|
|
183
238
|
// Dependencies must include IdentityContext
|
|
184
|
-
dependencies: [
|
|
239
|
+
dependencies: [SmPermissions, IdentityContext, ListRepository];
|
|
185
240
|
```
|
|
186
241
|
|
|
187
242
|
**Important:** The list `where` type must include `createdBy?: string`. For CMS-based entities, `CmsEntryListWhere` already has this.
|
|
@@ -191,14 +246,14 @@ dependencies: [MyPermissions.Abstraction, IdentityContext, ListRepository];
|
|
|
191
246
|
```ts
|
|
192
247
|
class UpdateUseCaseImpl implements UseCaseAbstraction.Interface {
|
|
193
248
|
constructor(
|
|
194
|
-
private permissions:
|
|
195
|
-
private getById: GetByIdUseCase.Interface,
|
|
249
|
+
private permissions: SmPermissions.Interface,
|
|
250
|
+
private getById: GetByIdUseCase.Interface,
|
|
196
251
|
private repository: UpdateRepository.Interface
|
|
197
252
|
) {}
|
|
198
253
|
|
|
199
254
|
async execute(id: string, data: UpdateData): UseCaseAbstraction.Return {
|
|
200
255
|
// 1. Entity-level edit check (no item yet)
|
|
201
|
-
if (!(await this.permissions.canEdit("
|
|
256
|
+
if (!(await this.permissions.canEdit("product"))) {
|
|
202
257
|
return Result.fail(new NotAuthorizedError());
|
|
203
258
|
}
|
|
204
259
|
|
|
@@ -211,7 +266,7 @@ class UpdateUseCaseImpl implements UseCaseAbstraction.Interface {
|
|
|
211
266
|
const original = getResult.value;
|
|
212
267
|
|
|
213
268
|
// 3. Item-level edit check (defense in depth)
|
|
214
|
-
if (!(await this.permissions.canEdit("
|
|
269
|
+
if (!(await this.permissions.canEdit("product", original))) {
|
|
215
270
|
return Result.fail(new NotAuthorizedError());
|
|
216
271
|
}
|
|
217
272
|
|
|
@@ -238,7 +293,7 @@ class DeleteUseCaseImpl implements UseCaseAbstraction.Interface {
|
|
|
238
293
|
const item = getResult.value;
|
|
239
294
|
|
|
240
295
|
// Item-level delete check — MUST pass the item
|
|
241
|
-
if (!(await this.permissions.canDelete("
|
|
296
|
+
if (!(await this.permissions.canDelete("product", item))) {
|
|
242
297
|
return Result.fail(new NotAuthorizedError());
|
|
243
298
|
}
|
|
244
299
|
|
|
@@ -253,7 +308,7 @@ class DeleteUseCaseImpl implements UseCaseAbstraction.Interface {
|
|
|
253
308
|
class PublishUseCaseImpl {
|
|
254
309
|
async execute(params: Params): UseCaseAbstraction.Return {
|
|
255
310
|
// 1. Entity-level publish check
|
|
256
|
-
if (!(await this.permissions.canPublish("
|
|
311
|
+
if (!(await this.permissions.canPublish("product"))) {
|
|
257
312
|
return Result.fail(new NotAuthorizedError());
|
|
258
313
|
}
|
|
259
314
|
|
|
@@ -264,7 +319,7 @@ class PublishUseCaseImpl {
|
|
|
264
319
|
}
|
|
265
320
|
|
|
266
321
|
// 3. Item-level ownership check (defense in depth)
|
|
267
|
-
if (!(await this.permissions.canAccess("
|
|
322
|
+
if (!(await this.permissions.canAccess("product", getResult.value))) {
|
|
268
323
|
return Result.fail(new NotAuthorizedError());
|
|
269
324
|
}
|
|
270
325
|
|
|
@@ -275,17 +330,44 @@ class PublishUseCaseImpl {
|
|
|
275
330
|
|
|
276
331
|
---
|
|
277
332
|
|
|
333
|
+
## DI Injection
|
|
334
|
+
|
|
335
|
+
The permissions abstraction is passed directly as a dependency — it IS the DI key:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
export const MyUseCase = UseCaseAbstraction.createImplementation({
|
|
339
|
+
implementation: MyUseCaseImpl,
|
|
340
|
+
dependencies: [SmPermissions, OtherDep]
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Note:** Use `SmPermissions` directly (not `SmPermissions.Abstraction`). The abstraction returned by `createPermissionsAbstraction` is the DI key itself.
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
278
348
|
## Gotchas
|
|
279
349
|
|
|
280
350
|
1. **`canDelete` without item + `own: true` = `false`** — Always pass the item to `canDelete`. Fetch first, then check.
|
|
281
351
|
2. **`canEdit` without item + `own: true` = `true`** — Intentional: allows editing new/unsaved records.
|
|
282
352
|
3. **`canAccess` without item = `true`** — Only checks entity-level access, not ownership.
|
|
283
353
|
4. **List where type** — Ensure the `where` interface includes `createdBy?: string` for own-scope filtering.
|
|
284
|
-
5.
|
|
285
|
-
6. **
|
|
354
|
+
5. **Dependencies order** — DI constructor params must match the `dependencies` array order exactly.
|
|
355
|
+
6. **Abstraction is the DI key** — Use `SmPermissions` directly in dependencies, not `SmPermissions.Abstraction`.
|
|
356
|
+
|
|
357
|
+
## Matching Admin-Side Permissions
|
|
358
|
+
|
|
359
|
+
The API schema and the admin-side `createPermissionSchema` should use the **same prefix, entity IDs, and action names**. This ensures the permissions emitted by the admin UI are correctly evaluated by the API.
|
|
360
|
+
|
|
361
|
+
```
|
|
362
|
+
API: createPermissionSchema({ prefix: "sm", entities: [{ id: "product", permission: "sm.product", ... }] })
|
|
363
|
+
Admin: createPermissionSchema({ prefix: "sm", entities: [{ id: "product", permission: "sm.product", ... }] })
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
See **webiny-admin-permissions** for the admin-side implementation.
|
|
286
367
|
|
|
287
368
|
## Related Skills
|
|
288
369
|
|
|
370
|
+
- **webiny-admin-permissions** — Admin-side permission UI and DI-backed permission checking
|
|
289
371
|
- **webiny-api-architect** — Architecture overview, Services vs UseCases, feature structure
|
|
290
372
|
- **webiny-use-case-pattern** — UseCase implementation, Result handling, decorators
|
|
291
373
|
- **webiny-dependency-injection** — Injectable services catalog
|
|
@@ -20,8 +20,7 @@ Every Webiny extension type uses the same DI pattern: define a class implementin
|
|
|
20
20
|
|
|
21
21
|
```typescript
|
|
22
22
|
import { SomeFactory } from "webiny/some/path";
|
|
23
|
-
import { Logger } from "webiny/api
|
|
24
|
-
import { BuildParams } from "webiny/api/build-params";
|
|
23
|
+
import { Logger, BuildParams } from "webiny/api";
|
|
25
24
|
|
|
26
25
|
class MyImplementation implements SomeFactory.Interface {
|
|
27
26
|
constructor(
|
|
@@ -151,6 +150,203 @@ export default CorePulumi.createImplementation({
|
|
|
151
150
|
});
|
|
152
151
|
```
|
|
153
152
|
|
|
153
|
+
## Advanced Dependency Options
|
|
154
|
+
|
|
155
|
+
The `dependencies` array supports three forms per entry:
|
|
156
|
+
|
|
157
|
+
| Form | Meaning |
|
|
158
|
+
|------|---------|
|
|
159
|
+
| `Abstraction` | Single required dependency (shorthand) |
|
|
160
|
+
| `[Abstraction, { optional: true }]` | Single optional dependency — injects `undefined` if not registered |
|
|
161
|
+
| `[Abstraction, { multiple: true }]` | Multi-injection — injects **all** registered implementations as `T[]` |
|
|
162
|
+
| `[Abstraction, { multiple: true, optional: true }]` | Multi-injection, optional — injects `undefined` if none registered (vs empty `[]` with just `multiple`) |
|
|
163
|
+
|
|
164
|
+
### Multi-injection (`{ multiple: true }`)
|
|
165
|
+
|
|
166
|
+
Use when a class needs all registered implementations of an abstraction. The container calls `resolveAll()` internally and injects the results as an array.
|
|
167
|
+
|
|
168
|
+
**Abstraction:**
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
interface IPageType {
|
|
172
|
+
name: string;
|
|
173
|
+
label: string;
|
|
174
|
+
modify(form: IFormModel): void;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const PageType = createAbstraction<IPageType>("PageType");
|
|
178
|
+
|
|
179
|
+
export namespace PageType {
|
|
180
|
+
export type Interface = IPageType;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Multiple implementations registered separately:**
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
// StaticPageType.ts
|
|
188
|
+
class StaticPageTypeImpl implements PageType.Interface {
|
|
189
|
+
name = "static";
|
|
190
|
+
label = "Static Page";
|
|
191
|
+
modify(form: IFormModel) { /* no-op — base form is sufficient */ }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export const StaticPageType = PageType.createImplementation({
|
|
195
|
+
implementation: StaticPageTypeImpl,
|
|
196
|
+
dependencies: []
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ProductPageType.ts (in another package/extension)
|
|
200
|
+
class ProductPageTypeImpl implements PageType.Interface {
|
|
201
|
+
name = "product";
|
|
202
|
+
label = "Product Page";
|
|
203
|
+
modify(form: IFormModel) {
|
|
204
|
+
form.fields(fields => ({
|
|
205
|
+
product: fields.select().label("Product").required("Product is required")
|
|
206
|
+
}));
|
|
207
|
+
form.field("title").disabled(true);
|
|
208
|
+
form.field("path").disabled(true);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const ProductPageType = PageType.createImplementation({
|
|
213
|
+
implementation: ProductPageTypeImpl,
|
|
214
|
+
dependencies: []
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Consumer injects the array:**
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
class CreatePagePresenterImpl implements CreatePagePresenter.Interface {
|
|
222
|
+
constructor(
|
|
223
|
+
private factory: FormModelFactory.Interface,
|
|
224
|
+
private pageTypes: PageType.Interface[],
|
|
225
|
+
private modifiers: CreatePageFormModifier.Interface[]
|
|
226
|
+
) {}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const CreatePagePresenter = PresenterAbstraction.createImplementation({
|
|
230
|
+
implementation: CreatePagePresenterImpl,
|
|
231
|
+
dependencies: [
|
|
232
|
+
FormModelFactory,
|
|
233
|
+
[PageType, { multiple: true }],
|
|
234
|
+
[CreatePageFormModifier, { multiple: true }]
|
|
235
|
+
]
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Registration — each implementation is a separate `container.register()` call:**
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
export const CreatePageFeature = createFeature({
|
|
243
|
+
name: "CreatePage",
|
|
244
|
+
register(container) {
|
|
245
|
+
container.register(StaticPageType); // first PageType impl
|
|
246
|
+
container.register(ProductPageType); // second PageType impl
|
|
247
|
+
container.register(CreatePagePresenter);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
When `CreatePagePresenter` is resolved, `pageTypes` receives `[StaticPageTypeImpl, ProductPageTypeImpl]` in registration order.
|
|
253
|
+
|
|
254
|
+
### Optional dependency (`{ optional: true }`)
|
|
255
|
+
|
|
256
|
+
Use when a dependency may not be registered. The container injects `undefined` instead of throwing.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
class MyPresenterImpl {
|
|
260
|
+
constructor(
|
|
261
|
+
private required: RequiredService.Interface,
|
|
262
|
+
private analytics: AnalyticsService.Interface | undefined
|
|
263
|
+
) {}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export const MyPresenter = Abstraction.createImplementation({
|
|
267
|
+
implementation: MyPresenterImpl,
|
|
268
|
+
dependencies: [
|
|
269
|
+
RequiredService,
|
|
270
|
+
[AnalyticsService, { optional: true }]
|
|
271
|
+
]
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Container API Reference
|
|
276
|
+
|
|
277
|
+
### Registration
|
|
278
|
+
|
|
279
|
+
| Method | Description |
|
|
280
|
+
|--------|-------------|
|
|
281
|
+
| `container.register(Impl)` | Register a class implementation. Returns `RegistrationBuilder` with `.inSingletonScope()`. Multiple registrations of the same abstraction accumulate — `resolve()` returns the last, `resolveAll()` returns all. |
|
|
282
|
+
| `container.registerInstance(Abstraction, instance)` | Register a pre-built instance (no constructor resolution). |
|
|
283
|
+
| `container.registerFactory(Abstraction, factory)` | Register a factory function. Called on every `resolve()`. |
|
|
284
|
+
| `container.registerDecorator(Decorator)` | Register a decorator that wraps resolved instances. Applied in registration order. |
|
|
285
|
+
| `container.registerComposite(Composite)` | Register a composite that aggregates all implementations behind a single `resolve()`. |
|
|
286
|
+
|
|
287
|
+
### Resolution
|
|
288
|
+
|
|
289
|
+
| Method | Description |
|
|
290
|
+
|--------|-------------|
|
|
291
|
+
| `container.resolve(Abstraction)` | Resolve single instance (last registered wins). Throws if not registered. |
|
|
292
|
+
| `container.resolveAll(Abstraction)` | Resolve all registered implementations as `T[]`. Returns empty array if none. |
|
|
293
|
+
| `container.createChildContainer()` | Create a child container that inherits parent registrations. |
|
|
294
|
+
|
|
295
|
+
### Lifetime Scopes
|
|
296
|
+
|
|
297
|
+
- **Transient** (default): New instance on every `resolve()`.
|
|
298
|
+
- **Singleton** (`.inSingletonScope()`): Cached after first resolution, one instance per container.
|
|
299
|
+
|
|
300
|
+
**Convention:** Use cases = transient. Repositories, gateways, services, registries = singleton.
|
|
301
|
+
|
|
302
|
+
### Decorators
|
|
303
|
+
|
|
304
|
+
Decorators wrap resolved instances. The decoratee is always the **last** constructor parameter. The `dependencies` array does NOT include the decoratee.
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
class LoggingServiceDecorator implements MyService.Interface {
|
|
308
|
+
constructor(
|
|
309
|
+
private logger: Logger.Interface,
|
|
310
|
+
private decoratee: MyService.Interface // LAST param — injected automatically
|
|
311
|
+
) {}
|
|
312
|
+
|
|
313
|
+
execute() {
|
|
314
|
+
this.logger.info("Before");
|
|
315
|
+
this.decoratee.execute();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export const MyServiceLoggingDecorator = MyService.createDecorator({
|
|
320
|
+
decorator: LoggingServiceDecorator,
|
|
321
|
+
dependencies: [Logger] // decoratee is NOT listed
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Registration:
|
|
325
|
+
container.registerDecorator(MyServiceLoggingDecorator);
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Composites
|
|
329
|
+
|
|
330
|
+
Composites aggregate multiple implementations behind a single `resolve()` call. Created via `Abstraction.createComposite()`:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
class AllValidatorsComposite implements Validator.Interface {
|
|
334
|
+
constructor(private validators: Validator.Interface[]) {}
|
|
335
|
+
|
|
336
|
+
validate(input: unknown) {
|
|
337
|
+
for (const v of this.validators) v.validate(input);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export const ValidatorComposite = Validator.createComposite({
|
|
342
|
+
implementation: AllValidatorsComposite,
|
|
343
|
+
dependencies: [[Validator, { multiple: true }]]
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Registration:
|
|
347
|
+
container.registerComposite(ValidatorComposite);
|
|
348
|
+
```
|
|
349
|
+
|
|
154
350
|
## Key Rules
|
|
155
351
|
|
|
156
352
|
1. Always import from the **feature path**, not the package root.
|
|
@@ -160,6 +356,8 @@ export default CorePulumi.createImplementation({
|
|
|
160
356
|
5. Extensions with no dependencies use `dependencies: []`.
|
|
161
357
|
6. `BuildParams.get<T>(name)` returns `T | null` — always type the receiving property/variable as nullable (e.g. `string | null`) and handle the `null` case.
|
|
162
358
|
7. **BuildParam declarations belong inside the extension's `Extension.tsx`**, not in `webiny.config.tsx`. Expose required params as React props on the extension component so the consumer decides where values come from (see `webiny-full-stack-architect` skill for the full pattern).
|
|
359
|
+
8. For multi-injection, type the constructor param as `T[]` and use `[Abstraction, { multiple: true }]` in the dependencies array.
|
|
360
|
+
9. Each implementation of a multi-bound abstraction is a separate `container.register()` call — they accumulate.
|
|
163
361
|
|
|
164
362
|
## Related Skills
|
|
165
363
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: webiny-admin-catalog
|
|
3
3
|
context: webiny-api
|
|
4
4
|
description: >
|
|
5
|
-
admin —
|
|
5
|
+
admin — 16 abstractions.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# admin
|
|
@@ -45,16 +45,6 @@ description: >
|
|
|
45
45
|
**Import:** `import { createFeature } from "webiny/admin"`
|
|
46
46
|
**Source:** `@webiny/feature/admin/index.ts`
|
|
47
47
|
|
|
48
|
-
---
|
|
49
|
-
**Name:** `createHasPermission`
|
|
50
|
-
**Import:** `import { createHasPermission } from "webiny/admin"`
|
|
51
|
-
**Source:** `@webiny/app-admin/permissions/index.ts`
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
**Name:** `createPermissionSchema`
|
|
55
|
-
**Import:** `import { createPermissionSchema } from "webiny/admin"`
|
|
56
|
-
**Source:** `@webiny/app-admin/permissions/index.ts`
|
|
57
|
-
|
|
58
48
|
---
|
|
59
49
|
**Name:** `createProvider`
|
|
60
50
|
**Import:** `import { createProvider } from "webiny/admin"`
|
|
@@ -70,11 +60,6 @@ This is mostly useful for adding React Context providers.
|
|
|
70
60
|
This is particularly useful for wrapping the entire app with custom React Context providers.
|
|
71
61
|
For more information, visit https://www.webiny.com/docs/admin-area/basics/framework.
|
|
72
62
|
|
|
73
|
-
---
|
|
74
|
-
**Name:** `createUsePermissions`
|
|
75
|
-
**Import:** `import { createUsePermissions } from "webiny/admin"`
|
|
76
|
-
**Source:** `@webiny/app-admin/permissions/index.ts`
|
|
77
|
-
|
|
78
63
|
---
|
|
79
64
|
**Name:** `DevToolsSection`
|
|
80
65
|
**Import:** `import { DevToolsSection } from "webiny/admin"`
|
|
@@ -85,6 +70,11 @@ Renders nothing — purely a data registration side-effect.
|
|
|
85
70
|
When the component unmounts (e.g., route change), the section
|
|
86
71
|
is automatically removed from DevTools.
|
|
87
72
|
|
|
73
|
+
---
|
|
74
|
+
**Name:** `MainGraphQLClient`
|
|
75
|
+
**Import:** `import { MainGraphQLClient } from "webiny/admin"`
|
|
76
|
+
**Source:** `@webiny/app/features/mainGraphQLClient/index.ts`
|
|
77
|
+
|
|
88
78
|
---
|
|
89
79
|
**Name:** `NetworkErrorEventHandler`
|
|
90
80
|
**Import:** `import { NetworkErrorEventHandler } from "webiny/admin"`
|