@webiny/mcp 6.2.0 → 6.3.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/package.json +4 -4
- package/skills/admin/admin-architect/SKILL.md +84 -81
- package/skills/api/event-handler-pattern/SKILL.md +56 -60
- package/skills/api/http-route/SKILL.md +0 -1
- package/skills/api/use-case-pattern/SKILL.md +137 -124
- package/skills/dependency-injection/SKILL.md +78 -79
- package/skills/generated/admin/SKILL.md +16 -1
- package/skills/generated/api/SKILL.md +43 -1
- package/skills/webiny-sdk/SKILL.md +148 -29
- package/skills/website-builder/SKILL.md +5 -1
|
@@ -34,24 +34,26 @@ import { SomeUseCase } from "webiny/api/<category>";
|
|
|
34
34
|
import { SomeEventHandler } from "webiny/api/<category>";
|
|
35
35
|
|
|
36
36
|
class MyHandler implements SomeEventHandler.Interface {
|
|
37
|
-
|
|
37
|
+
constructor(private someUseCase: SomeUseCase.Interface) {}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
async handle(event: SomeEventHandler.Event) {
|
|
40
|
+
const result = await this.someUseCase.execute({
|
|
41
|
+
/* input */
|
|
42
|
+
});
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const value = result.value;
|
|
48
|
-
// ... use value
|
|
44
|
+
if (result.isFail()) {
|
|
45
|
+
console.error(result.error.message);
|
|
46
|
+
return;
|
|
49
47
|
}
|
|
48
|
+
|
|
49
|
+
const value = result.value;
|
|
50
|
+
// ... use value
|
|
51
|
+
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export default SomeEventHandler.createImplementation({
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
implementation: MyHandler,
|
|
56
|
+
dependencies: [SomeUseCase]
|
|
55
57
|
});
|
|
56
58
|
```
|
|
57
59
|
|
|
@@ -63,15 +65,15 @@ To replace the default implementation, register your own:
|
|
|
63
65
|
import { SomeUseCase } from "webiny/api/<category>";
|
|
64
66
|
|
|
65
67
|
class CustomImplementation implements SomeUseCase.Interface {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
async execute(input) {
|
|
69
|
+
// Custom logic
|
|
70
|
+
return Result.ok(/* ... */);
|
|
71
|
+
}
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export default SomeUseCase.createImplementation({
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
implementation: CustomImplementation,
|
|
76
|
+
dependencies: []
|
|
75
77
|
});
|
|
76
78
|
```
|
|
77
79
|
|
|
@@ -101,24 +103,24 @@ Every feature defines errors extending `BaseError`. Never use generic `Error` fo
|
|
|
101
103
|
import { BaseError } from "@webiny/feature/api";
|
|
102
104
|
|
|
103
105
|
export class EntityNotFoundError extends BaseError {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
override readonly code = "Entity/NotFound" as const;
|
|
107
|
+
constructor(id: string) {
|
|
108
|
+
super({ message: `Entity with id "${id}" was not found!` });
|
|
109
|
+
}
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
export class EntityPersistenceError extends BaseError<{ error: Error }> {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
override readonly code = "Entity/Persist" as const;
|
|
114
|
+
constructor(error: Error) {
|
|
115
|
+
super({ message: error.message, data: { error } });
|
|
116
|
+
}
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
export class EntityValidationError extends BaseError<{ message: string }> {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
override readonly code = "Entity/Validation" as const;
|
|
121
|
+
constructor(message: string) {
|
|
122
|
+
super({ message, data: { message } });
|
|
123
|
+
}
|
|
122
124
|
}
|
|
123
125
|
```
|
|
124
126
|
|
|
@@ -130,54 +132,58 @@ Define an `IErrors` interface mapping error names to types, then create a union
|
|
|
130
132
|
// features/createEntity/abstractions.ts
|
|
131
133
|
import { createAbstraction, Result } from "@webiny/feature/api";
|
|
132
134
|
import { NotAuthorizedError } from "@webiny/api-core/features/security/shared/errors.js";
|
|
133
|
-
import {
|
|
135
|
+
import {
|
|
136
|
+
EntityPersistenceError,
|
|
137
|
+
EntityModelNotFoundError,
|
|
138
|
+
EntityCreationError
|
|
139
|
+
} from "~/api/domain/errors.js";
|
|
134
140
|
|
|
135
141
|
// REPOSITORY errors
|
|
136
142
|
export interface ICreateEntityRepositoryErrors {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
persistence: EntityPersistenceError;
|
|
144
|
+
modelNotFound: EntityModelNotFoundError;
|
|
145
|
+
creation: EntityCreationError;
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
type RepositoryError = ICreateEntityRepositoryErrors[keyof ICreateEntityRepositoryErrors];
|
|
143
149
|
|
|
144
150
|
export interface ICreateEntityRepository {
|
|
145
|
-
|
|
151
|
+
execute(entity: Entity): Promise<Result<Entity, RepositoryError>>;
|
|
146
152
|
}
|
|
147
153
|
|
|
148
154
|
export const CreateEntityRepository = createAbstraction<ICreateEntityRepository>(
|
|
149
|
-
|
|
155
|
+
"MyExt/CreateEntityRepository"
|
|
150
156
|
);
|
|
151
157
|
|
|
152
158
|
export namespace CreateEntityRepository {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
export type Interface = ICreateEntityRepository;
|
|
160
|
+
export type Error = RepositoryError;
|
|
161
|
+
export type Return = Promise<Result<Entity, RepositoryError>>;
|
|
156
162
|
}
|
|
157
163
|
|
|
158
164
|
// USE CASE errors — superset of repository errors
|
|
159
165
|
export interface ICreateEntityUseCaseErrors {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
persistence: EntityPersistenceError;
|
|
167
|
+
modelNotFound: EntityModelNotFoundError;
|
|
168
|
+
creation: EntityCreationError;
|
|
169
|
+
notAuthorized: NotAuthorizedError;
|
|
164
170
|
}
|
|
165
171
|
|
|
166
172
|
type UseCaseError = ICreateEntityUseCaseErrors[keyof ICreateEntityUseCaseErrors];
|
|
167
173
|
|
|
168
174
|
export interface ICreateEntityUseCase {
|
|
169
|
-
|
|
175
|
+
execute(input: CreateEntityInput): Promise<Result<Entity, UseCaseError>>;
|
|
170
176
|
}
|
|
171
177
|
|
|
172
178
|
export const CreateEntityUseCase = createAbstraction<ICreateEntityUseCase>(
|
|
173
|
-
|
|
179
|
+
"MyExt/CreateEntityUseCase"
|
|
174
180
|
);
|
|
175
181
|
|
|
176
182
|
export namespace CreateEntityUseCase {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
export type Interface = ICreateEntityUseCase;
|
|
184
|
+
export type Input = CreateEntityInput;
|
|
185
|
+
export type Error = UseCaseError;
|
|
186
|
+
export type Return = Promise<Result<Entity, UseCaseError>>;
|
|
181
187
|
}
|
|
182
188
|
```
|
|
183
189
|
|
|
@@ -192,7 +198,7 @@ return Result.fail(new EntityNotFoundError(id));
|
|
|
192
198
|
|
|
193
199
|
// Check result
|
|
194
200
|
if (result.isFail()) {
|
|
195
|
-
|
|
201
|
+
return Result.fail(result.error);
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
// Access value
|
|
@@ -207,7 +213,10 @@ Never use `result.isError()`, `result.getError()`, or `result.getValue()` — th
|
|
|
207
213
|
|
|
208
214
|
```ts
|
|
209
215
|
// features/createEntity/CreateEntityUseCase.ts
|
|
210
|
-
import {
|
|
216
|
+
import {
|
|
217
|
+
CreateEntityUseCase as UseCaseAbstraction,
|
|
218
|
+
CreateEntityRepository
|
|
219
|
+
} from "./abstractions.js";
|
|
211
220
|
import { Result } from "@webiny/feature/api";
|
|
212
221
|
import { IdentityContext } from "@webiny/api-core/exports/api/security.js";
|
|
213
222
|
import { NotAuthorizedError } from "@webiny/api-core/features/security/shared/errors.js";
|
|
@@ -215,37 +224,38 @@ import { Entity } from "~/shared/Entity.js";
|
|
|
215
224
|
import { EntityId } from "~/api/domain/EntityId.js";
|
|
216
225
|
|
|
217
226
|
class CreateEntityUseCase implements UseCaseAbstraction.Interface {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const entity = Entity.from({
|
|
229
|
-
id: EntityId.from(input.id),
|
|
230
|
-
values: { name: input.name, status: "disabled" }
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
const result = await this.repository.execute(entity);
|
|
234
|
-
if (result.isFail()) {
|
|
235
|
-
return Result.fail(result.error);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return Result.ok(result.value);
|
|
227
|
+
constructor(
|
|
228
|
+
private identityContext: IdentityContext.Interface,
|
|
229
|
+
private repository: CreateEntityRepository.Interface
|
|
230
|
+
) {}
|
|
231
|
+
|
|
232
|
+
async execute(input: UseCaseAbstraction.Input): UseCaseAbstraction.Return {
|
|
233
|
+
if (!this.identityContext.getPermission("mypackage.entity")) {
|
|
234
|
+
return Result.fail(new NotAuthorizedError({ message: "Not authorized to create entities!" }));
|
|
239
235
|
}
|
|
236
|
+
|
|
237
|
+
const entity = Entity.from({
|
|
238
|
+
id: EntityId.from(input.id),
|
|
239
|
+
values: { name: input.name, status: "disabled" }
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const result = await this.repository.execute(entity);
|
|
243
|
+
if (result.isFail()) {
|
|
244
|
+
return Result.fail(result.error);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return Result.ok(result.value);
|
|
248
|
+
}
|
|
240
249
|
}
|
|
241
250
|
|
|
242
251
|
export default UseCaseAbstraction.createImplementation({
|
|
243
|
-
|
|
244
|
-
|
|
252
|
+
implementation: CreateEntityUseCase,
|
|
253
|
+
dependencies: [IdentityContext, CreateEntityRepository]
|
|
245
254
|
});
|
|
246
255
|
```
|
|
247
256
|
|
|
248
257
|
**Rules:**
|
|
258
|
+
|
|
249
259
|
- Class implements `UseCaseAbstraction.Interface`
|
|
250
260
|
- Constructor params typed with `.Interface` from their abstractions
|
|
251
261
|
- Return type uses `UseCaseAbstraction.Return`
|
|
@@ -269,36 +279,36 @@ import { GetModelUseCase } from "@webiny/api-headless-cms/exports/api/cms/model"
|
|
|
269
279
|
import { ENTITY_MODEL_ID } from "~/shared/constants.js";
|
|
270
280
|
|
|
271
281
|
class CreateEntityRepository implements RepositoryAbstraction.Interface {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const createResult = await this.createEntryUseCase.execute(modelResult.value, {
|
|
284
|
-
id: entity.id,
|
|
285
|
-
values: {
|
|
286
|
-
name: entity.values.name,
|
|
287
|
-
status: entity.values.status
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
if (createResult.isFail()) {
|
|
292
|
-
return Result.fail(new EntityCreationError(createResult.error));
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
return Result.ok(entity);
|
|
282
|
+
constructor(
|
|
283
|
+
private getModelUseCase: GetModelUseCase.Interface,
|
|
284
|
+
private createEntryUseCase: CreateEntryUseCase.Interface
|
|
285
|
+
) {}
|
|
286
|
+
|
|
287
|
+
async execute(entity: Entity): RepositoryAbstraction.Return {
|
|
288
|
+
const modelResult = await this.getModelUseCase.execute(ENTITY_MODEL_ID);
|
|
289
|
+
if (modelResult.isFail()) {
|
|
290
|
+
return Result.fail(new EntityModelNotFoundError());
|
|
296
291
|
}
|
|
292
|
+
|
|
293
|
+
const createResult = await this.createEntryUseCase.execute(modelResult.value, {
|
|
294
|
+
id: entity.id,
|
|
295
|
+
values: {
|
|
296
|
+
name: entity.values.name,
|
|
297
|
+
status: entity.values.status
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (createResult.isFail()) {
|
|
302
|
+
return Result.fail(new EntityCreationError(createResult.error));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return Result.ok(entity);
|
|
306
|
+
}
|
|
297
307
|
}
|
|
298
308
|
|
|
299
309
|
export default RepositoryAbstraction.createImplementation({
|
|
300
|
-
|
|
301
|
-
|
|
310
|
+
implementation: CreateEntityRepository,
|
|
311
|
+
dependencies: [GetModelUseCase, CreateEntryUseCase]
|
|
302
312
|
});
|
|
303
313
|
```
|
|
304
314
|
|
|
@@ -316,6 +326,7 @@ import { ListModelsUseCase } from "@webiny/api-headless-cms/exports/api/cms/mode
|
|
|
316
326
|
```
|
|
317
327
|
|
|
318
328
|
**Rules:**
|
|
329
|
+
|
|
319
330
|
- Always resolve the CMS model first via `GetModelUseCase`
|
|
320
331
|
- Wrap CMS errors in domain-specific errors
|
|
321
332
|
- Register repositories in **singleton scope**
|
|
@@ -333,12 +344,12 @@ import { Entity as EntityClass } from "~/shared/Entity.js";
|
|
|
333
344
|
import type { Entity, EntityDto, EntityValues } from "~/shared/Entity.js";
|
|
334
345
|
|
|
335
346
|
export class EntryToEntityMapper {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
347
|
+
static toEntity(entry: { entryId: string; values: EntityValues }): Entity {
|
|
348
|
+
return EntityClass.from({
|
|
349
|
+
id: entry.entryId,
|
|
350
|
+
values: entry.values
|
|
351
|
+
});
|
|
352
|
+
}
|
|
342
353
|
}
|
|
343
354
|
```
|
|
344
355
|
|
|
@@ -360,22 +371,22 @@ import { IdentityContext } from "@webiny/api-core/exports/api/security.js";
|
|
|
360
371
|
import { NotAuthorizedError } from "@webiny/api-core/features/security/shared/errors.js";
|
|
361
372
|
|
|
362
373
|
class GetEntityByIdWithAuthorizationImpl implements GetEntityByIdUseCase.Interface {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
return this.decoratee.execute(id);
|
|
374
|
+
constructor(
|
|
375
|
+
private identityContext: IdentityContext.Interface,
|
|
376
|
+
private decoratee: GetEntityByIdUseCase.Interface // decoratee is LAST
|
|
377
|
+
) {}
|
|
378
|
+
|
|
379
|
+
async execute(id: string): GetEntityByIdUseCase.Return {
|
|
380
|
+
if (!this.identityContext.getPermission("mypackage.entity")) {
|
|
381
|
+
return Result.fail(new NotAuthorizedError());
|
|
373
382
|
}
|
|
383
|
+
return this.decoratee.execute(id);
|
|
384
|
+
}
|
|
374
385
|
}
|
|
375
386
|
|
|
376
387
|
export const GetEntityByIdWithAuthorization = GetEntityByIdUseCase.createDecorator({
|
|
377
|
-
|
|
378
|
-
|
|
388
|
+
decorator: GetEntityByIdWithAuthorizationImpl,
|
|
389
|
+
dependencies: [IdentityContext] // does NOT include decoratee
|
|
379
390
|
});
|
|
380
391
|
```
|
|
381
392
|
|
|
@@ -389,16 +400,17 @@ import GetEntityByIdRepository from "./GetEntityByIdRepository.js";
|
|
|
389
400
|
import { GetEntityByIdWithAuthorization } from "./decorators/GetEntityByIdWithAuthorization.js";
|
|
390
401
|
|
|
391
402
|
export const GetEntityByIdFeature = createFeature({
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
403
|
+
name: "GetEntityById",
|
|
404
|
+
register(container) {
|
|
405
|
+
container.register(GetEntityByIdUseCase);
|
|
406
|
+
container.register(GetEntityByIdRepository).inSingletonScope();
|
|
407
|
+
container.registerDecorator(GetEntityByIdWithAuthorization);
|
|
408
|
+
}
|
|
398
409
|
});
|
|
399
410
|
```
|
|
400
411
|
|
|
401
412
|
**Rules:**
|
|
413
|
+
|
|
402
414
|
- Implements the same interface as the use case it decorates
|
|
403
415
|
- Constructor: extra dependencies first, `decoratee` **last**
|
|
404
416
|
- Use `UseCaseAbstraction.createDecorator(...)` — the `dependencies` array does NOT include the decoratee
|
|
@@ -410,6 +422,7 @@ export const GetEntityByIdFeature = createFeature({
|
|
|
410
422
|
## Schema-Based Permissions
|
|
411
423
|
|
|
412
424
|
For implementing authorization in use cases, see the **webiny-api-permissions** skill. It covers:
|
|
425
|
+
|
|
413
426
|
- Permission schema definition with `createPermissions`
|
|
414
427
|
- All permission methods (`canRead`, `canEdit`, `canDelete`, `canPublish`, `onlyOwnRecords`, etc.)
|
|
415
428
|
- Use case patterns for every CRUD operation (get, list, update, delete, publish)
|
|
@@ -154,11 +154,11 @@ export default CorePulumi.createImplementation({
|
|
|
154
154
|
|
|
155
155
|
The `dependencies` array supports three forms per entry:
|
|
156
156
|
|
|
157
|
-
| Form
|
|
158
|
-
|
|
159
|
-
| `Abstraction`
|
|
160
|
-
| `[Abstraction, { optional: true }]`
|
|
161
|
-
| `[Abstraction, { multiple: true }]`
|
|
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
162
|
| `[Abstraction, { multiple: true, optional: true }]` | Multi-injection, optional — injects `undefined` if none registered (vs empty `[]` with just `multiple`) |
|
|
163
163
|
|
|
164
164
|
### Multi-injection (`{ multiple: true }`)
|
|
@@ -169,15 +169,15 @@ Use when a class needs all registered implementations of an abstraction. The con
|
|
|
169
169
|
|
|
170
170
|
```ts
|
|
171
171
|
interface IPageType {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
name: string;
|
|
173
|
+
label: string;
|
|
174
|
+
modify(form: IFormModel): void;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
export const PageType = createAbstraction<IPageType>("PageType");
|
|
178
178
|
|
|
179
179
|
export namespace PageType {
|
|
180
|
-
|
|
180
|
+
export type Interface = IPageType;
|
|
181
181
|
}
|
|
182
182
|
```
|
|
183
183
|
|
|
@@ -186,32 +186,34 @@ export namespace PageType {
|
|
|
186
186
|
```ts
|
|
187
187
|
// StaticPageType.ts
|
|
188
188
|
class StaticPageTypeImpl implements PageType.Interface {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
name = "static";
|
|
190
|
+
label = "Static Page";
|
|
191
|
+
modify(form: IFormModel) {
|
|
192
|
+
/* no-op — base form is sufficient */
|
|
193
|
+
}
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
export const StaticPageType = PageType.createImplementation({
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
implementation: StaticPageTypeImpl,
|
|
198
|
+
dependencies: []
|
|
197
199
|
});
|
|
198
200
|
|
|
199
201
|
// ProductPageType.ts (in another package/extension)
|
|
200
202
|
class ProductPageTypeImpl implements PageType.Interface {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
203
|
+
name = "product";
|
|
204
|
+
label = "Product Page";
|
|
205
|
+
modify(form: IFormModel) {
|
|
206
|
+
form.fields(fields => ({
|
|
207
|
+
product: fields.select().label("Product").required("Product is required")
|
|
208
|
+
}));
|
|
209
|
+
form.field("title").disabled(true);
|
|
210
|
+
form.field("path").disabled(true);
|
|
211
|
+
}
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
export const ProductPageType = PageType.createImplementation({
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
implementation: ProductPageTypeImpl,
|
|
216
|
+
dependencies: []
|
|
215
217
|
});
|
|
216
218
|
```
|
|
217
219
|
|
|
@@ -219,20 +221,20 @@ export const ProductPageType = PageType.createImplementation({
|
|
|
219
221
|
|
|
220
222
|
```ts
|
|
221
223
|
class CreatePagePresenterImpl implements CreatePagePresenter.Interface {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
constructor(
|
|
225
|
+
private factory: FormModelFactory.Interface,
|
|
226
|
+
private pageTypes: PageType.Interface[],
|
|
227
|
+
private modifiers: CreatePageFormModifier.Interface[]
|
|
228
|
+
) {}
|
|
227
229
|
}
|
|
228
230
|
|
|
229
231
|
export const CreatePagePresenter = PresenterAbstraction.createImplementation({
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
232
|
+
implementation: CreatePagePresenterImpl,
|
|
233
|
+
dependencies: [
|
|
234
|
+
FormModelFactory,
|
|
235
|
+
[PageType, { multiple: true }],
|
|
236
|
+
[CreatePageFormModifier, { multiple: true }]
|
|
237
|
+
]
|
|
236
238
|
});
|
|
237
239
|
```
|
|
238
240
|
|
|
@@ -240,12 +242,12 @@ export const CreatePagePresenter = PresenterAbstraction.createImplementation({
|
|
|
240
242
|
|
|
241
243
|
```ts
|
|
242
244
|
export const CreatePageFeature = createFeature({
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
name: "CreatePage",
|
|
246
|
+
register(container) {
|
|
247
|
+
container.register(StaticPageType); // first PageType impl
|
|
248
|
+
container.register(ProductPageType); // second PageType impl
|
|
249
|
+
container.register(CreatePagePresenter);
|
|
250
|
+
}
|
|
249
251
|
});
|
|
250
252
|
```
|
|
251
253
|
|
|
@@ -257,18 +259,15 @@ Use when a dependency may not be registered. The container injects `undefined` i
|
|
|
257
259
|
|
|
258
260
|
```ts
|
|
259
261
|
class MyPresenterImpl {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
constructor(
|
|
263
|
+
private required: RequiredService.Interface,
|
|
264
|
+
private analytics: AnalyticsService.Interface | undefined
|
|
265
|
+
) {}
|
|
264
266
|
}
|
|
265
267
|
|
|
266
268
|
export const MyPresenter = Abstraction.createImplementation({
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
RequiredService,
|
|
270
|
-
[AnalyticsService, { optional: true }]
|
|
271
|
-
]
|
|
269
|
+
implementation: MyPresenterImpl,
|
|
270
|
+
dependencies: [RequiredService, [AnalyticsService, { optional: true }]]
|
|
272
271
|
});
|
|
273
272
|
```
|
|
274
273
|
|
|
@@ -276,21 +275,21 @@ export const MyPresenter = Abstraction.createImplementation({
|
|
|
276
275
|
|
|
277
276
|
### Registration
|
|
278
277
|
|
|
279
|
-
| Method
|
|
280
|
-
|
|
281
|
-
| `container.register(Impl)`
|
|
282
|
-
| `container.registerInstance(Abstraction, instance)` | Register a pre-built instance (no constructor resolution).
|
|
283
|
-
| `container.registerFactory(Abstraction, factory)`
|
|
284
|
-
| `container.registerDecorator(Decorator)`
|
|
285
|
-
| `container.registerComposite(Composite)`
|
|
278
|
+
| Method | Description |
|
|
279
|
+
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
280
|
+
| `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. |
|
|
281
|
+
| `container.registerInstance(Abstraction, instance)` | Register a pre-built instance (no constructor resolution). |
|
|
282
|
+
| `container.registerFactory(Abstraction, factory)` | Register a factory function. Called on every `resolve()`. |
|
|
283
|
+
| `container.registerDecorator(Decorator)` | Register a decorator that wraps resolved instances. Applied in registration order. |
|
|
284
|
+
| `container.registerComposite(Composite)` | Register a composite that aggregates all implementations behind a single `resolve()`. |
|
|
286
285
|
|
|
287
286
|
### Resolution
|
|
288
287
|
|
|
289
|
-
| Method
|
|
290
|
-
|
|
291
|
-
| `container.resolve(Abstraction)`
|
|
288
|
+
| Method | Description |
|
|
289
|
+
| ----------------------------------- | ----------------------------------------------------------------------------- |
|
|
290
|
+
| `container.resolve(Abstraction)` | Resolve single instance (last registered wins). Throws if not registered. |
|
|
292
291
|
| `container.resolveAll(Abstraction)` | Resolve all registered implementations as `T[]`. Returns empty array if none. |
|
|
293
|
-
| `container.createChildContainer()`
|
|
292
|
+
| `container.createChildContainer()` | Create a child container that inherits parent registrations. |
|
|
294
293
|
|
|
295
294
|
### Lifetime Scopes
|
|
296
295
|
|
|
@@ -305,20 +304,20 @@ Decorators wrap resolved instances. The decoratee is always the **last** constru
|
|
|
305
304
|
|
|
306
305
|
```ts
|
|
307
306
|
class LoggingServiceDecorator implements MyService.Interface {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
307
|
+
constructor(
|
|
308
|
+
private logger: Logger.Interface,
|
|
309
|
+
private decoratee: MyService.Interface // LAST param — injected automatically
|
|
310
|
+
) {}
|
|
311
|
+
|
|
312
|
+
execute() {
|
|
313
|
+
this.logger.info("Before");
|
|
314
|
+
this.decoratee.execute();
|
|
315
|
+
}
|
|
317
316
|
}
|
|
318
317
|
|
|
319
318
|
export const MyServiceLoggingDecorator = MyService.createDecorator({
|
|
320
|
-
|
|
321
|
-
|
|
319
|
+
decorator: LoggingServiceDecorator,
|
|
320
|
+
dependencies: [Logger] // decoratee is NOT listed
|
|
322
321
|
});
|
|
323
322
|
|
|
324
323
|
// Registration:
|
|
@@ -331,16 +330,16 @@ Composites aggregate multiple implementations behind a single `resolve()` call.
|
|
|
331
330
|
|
|
332
331
|
```ts
|
|
333
332
|
class AllValidatorsComposite implements Validator.Interface {
|
|
334
|
-
|
|
333
|
+
constructor(private validators: Validator.Interface[]) {}
|
|
335
334
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
335
|
+
validate(input: unknown) {
|
|
336
|
+
for (const v of this.validators) v.validate(input);
|
|
337
|
+
}
|
|
339
338
|
}
|
|
340
339
|
|
|
341
340
|
export const ValidatorComposite = Validator.createComposite({
|
|
342
|
-
|
|
343
|
-
|
|
341
|
+
implementation: AllValidatorsComposite,
|
|
342
|
+
dependencies: [[Validator, { multiple: true }]]
|
|
344
343
|
});
|
|
345
344
|
|
|
346
345
|
// Registration:
|