@rolandsall24/nest-mediator 0.0.1
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/LICENSE +21 -0
- package/README.md +655 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/lib/decorators/command-handler.decorator.d.ts +19 -0
- package/dist/lib/decorators/command-handler.decorator.d.ts.map +1 -0
- package/dist/lib/decorators/command-handler.decorator.js +20 -0
- package/dist/lib/decorators/index.d.ts +3 -0
- package/dist/lib/decorators/index.d.ts.map +1 -0
- package/dist/lib/decorators/index.js +2 -0
- package/dist/lib/decorators/query-handler.decorator.d.ts +19 -0
- package/dist/lib/decorators/query-handler.decorator.d.ts.map +1 -0
- package/dist/lib/decorators/query-handler.decorator.js +20 -0
- package/dist/lib/interfaces/command-handler.interface.d.ts +14 -0
- package/dist/lib/interfaces/command-handler.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/command-handler.interface.js +1 -0
- package/dist/lib/interfaces/command.interface.d.ts +7 -0
- package/dist/lib/interfaces/command.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/command.interface.js +1 -0
- package/dist/lib/interfaces/index.d.ts +5 -0
- package/dist/lib/interfaces/index.d.ts.map +1 -0
- package/dist/lib/interfaces/index.js +4 -0
- package/dist/lib/interfaces/query-handler.interface.d.ts +15 -0
- package/dist/lib/interfaces/query-handler.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/query-handler.interface.js +1 -0
- package/dist/lib/interfaces/query.interface.d.ts +7 -0
- package/dist/lib/interfaces/query.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/query.interface.js +1 -0
- package/dist/lib/nest-mediator.d.ts +2 -0
- package/dist/lib/nest-mediator.d.ts.map +1 -0
- package/dist/lib/nest-mediator.js +3 -0
- package/dist/lib/nest-mediator.module.d.ts +21 -0
- package/dist/lib/nest-mediator.module.d.ts.map +1 -0
- package/dist/lib/nest-mediator.module.js +58 -0
- package/dist/lib/services/index.d.ts +2 -0
- package/dist/lib/services/index.d.ts.map +1 -0
- package/dist/lib/services/index.js +1 -0
- package/dist/lib/services/mediator.service.d.ts +34 -0
- package/dist/lib/services/mediator.service.d.ts.map +1 -0
- package/dist/lib/services/mediator.service.js +68 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Roland Salloum
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
# NestJS Mediator
|
|
2
|
+
|
|
3
|
+
A lightweight CQRS (Command Query Responsibility Segregation) mediator pattern implementation for NestJS applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Clean separation between Commands and Queries
|
|
8
|
+
- Type-safe handlers with TypeScript
|
|
9
|
+
- Decorator-based handler registration
|
|
10
|
+
- Automatic handler discovery and registration
|
|
11
|
+
- Built on top of NestJS dependency injection
|
|
12
|
+
- Zero runtime dependencies beyond NestJS
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @rolandsall24/nest-mediator
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### 1. Import the Module
|
|
23
|
+
|
|
24
|
+
Import `NestMediatorModule` in your application module and register your handlers:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { Module } from '@nestjs/common';
|
|
28
|
+
import { NestMediatorModule } from '@rolandsall24/nest-mediator';
|
|
29
|
+
import { CreateUserCommandHandler } from './handlers/create-user.handler';
|
|
30
|
+
import { GetUserQueryHandler } from './handlers/get-user-query.handler';
|
|
31
|
+
|
|
32
|
+
@Module({
|
|
33
|
+
imports: [
|
|
34
|
+
NestMediatorModule.forRoot({
|
|
35
|
+
handlers: [
|
|
36
|
+
CreateUserCommandHandler,
|
|
37
|
+
GetUserQueryHandler,
|
|
38
|
+
],
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
providers: [
|
|
42
|
+
// IMPORTANT: You must also add handlers to the providers array
|
|
43
|
+
// so NestJS can inject their dependencies
|
|
44
|
+
CreateUserCommandHandler,
|
|
45
|
+
GetUserQueryHandler,
|
|
46
|
+
],
|
|
47
|
+
})
|
|
48
|
+
export class AppModule {}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Important Note**: Handlers must be registered in **two places**:
|
|
52
|
+
1. In `NestMediatorModule.forRoot()` - for mediator pattern registration
|
|
53
|
+
2. In the module's `providers` array - for NestJS dependency injection
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
### Commands
|
|
58
|
+
|
|
59
|
+
Commands are used for operations that change state (create, update, delete).
|
|
60
|
+
|
|
61
|
+
#### 1. Define a Command
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { ICommand } from '@rolandsall24/nest-mediator';
|
|
65
|
+
|
|
66
|
+
export class CreateUserCommand implements ICommand {
|
|
67
|
+
constructor(
|
|
68
|
+
public readonly email: string,
|
|
69
|
+
public readonly name: string,
|
|
70
|
+
public readonly age: number
|
|
71
|
+
) {}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### 2. Create a Command Handler
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { Injectable } from '@nestjs/common';
|
|
79
|
+
import { CommandHandler, ICommandHandler } from '@rolandsall24/nest-mediator';
|
|
80
|
+
import { CreateUserCommand } from '../commands/create-user.command';
|
|
81
|
+
|
|
82
|
+
@Injectable()
|
|
83
|
+
@CommandHandler(CreateUserCommand)
|
|
84
|
+
export class CreateUserCommandHandler implements ICommandHandler<CreateUserCommand> {
|
|
85
|
+
constructor(
|
|
86
|
+
// Inject your services here
|
|
87
|
+
// private readonly userRepository: UserRepository,
|
|
88
|
+
) {}
|
|
89
|
+
|
|
90
|
+
async execute(command: CreateUserCommand): Promise<void> {
|
|
91
|
+
// Business logic here
|
|
92
|
+
console.log(`Creating user: ${command.email}`);
|
|
93
|
+
|
|
94
|
+
// Example: Save to database
|
|
95
|
+
// await this.userRepository.save({
|
|
96
|
+
// email: command.email,
|
|
97
|
+
// name: command.name,
|
|
98
|
+
// age: command.age,
|
|
99
|
+
// });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### 3. Send a Command from Controller
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { Controller, Post, Body } from '@nestjs/common';
|
|
108
|
+
import { MediatorService } from '@rolandsall24/nest-mediator';
|
|
109
|
+
import { CreateUserCommand } from './commands/create-user.command';
|
|
110
|
+
|
|
111
|
+
@Controller('users')
|
|
112
|
+
export class UserController {
|
|
113
|
+
constructor(private readonly mediator: MediatorService) {}
|
|
114
|
+
|
|
115
|
+
@Post()
|
|
116
|
+
async create(@Body() body: { email: string; name: string; age: number }): Promise<void> {
|
|
117
|
+
const command = new CreateUserCommand(
|
|
118
|
+
body.email,
|
|
119
|
+
body.name,
|
|
120
|
+
body.age
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
await this.mediator.send(command);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Queries
|
|
129
|
+
|
|
130
|
+
Queries are used for operations that read data without changing state.
|
|
131
|
+
|
|
132
|
+
#### 1. Define a Query
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { IQuery } from '@rolandsall24/nest-mediator';
|
|
136
|
+
|
|
137
|
+
export class GetUserByIdQuery implements IQuery {
|
|
138
|
+
constructor(public readonly userId: string) {}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### 2. Define a Query Result Type
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
export interface UserDto {
|
|
146
|
+
id: string;
|
|
147
|
+
email: string;
|
|
148
|
+
name: string;
|
|
149
|
+
age: number;
|
|
150
|
+
createdAt: Date;
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### 3. Create a Query Handler
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { Injectable } from '@nestjs/common';
|
|
158
|
+
import { QueryHandler, IQueryHandler } from '@rolandsall24/nest-mediator';
|
|
159
|
+
import { GetUserByIdQuery } from '../queries/get-user-by-id.query';
|
|
160
|
+
import { UserDto } from '../dtos/user.dto';
|
|
161
|
+
|
|
162
|
+
@Injectable()
|
|
163
|
+
@QueryHandler(GetUserByIdQuery)
|
|
164
|
+
export class GetUserByIdQueryHandler implements IQueryHandler<GetUserByIdQuery, UserDto> {
|
|
165
|
+
constructor(
|
|
166
|
+
// Inject your services here
|
|
167
|
+
// private readonly userRepository: UserRepository,
|
|
168
|
+
) {}
|
|
169
|
+
|
|
170
|
+
async execute(query: GetUserByIdQuery): Promise<UserDto> {
|
|
171
|
+
// Business logic here
|
|
172
|
+
console.log(`Fetching user with ID: ${query.userId}`);
|
|
173
|
+
|
|
174
|
+
// Example: Fetch from database
|
|
175
|
+
// const user = await this.userRepository.findById(query.userId);
|
|
176
|
+
|
|
177
|
+
// Return mock data for demonstration
|
|
178
|
+
return {
|
|
179
|
+
id: query.userId,
|
|
180
|
+
email: 'john.doe@example.com',
|
|
181
|
+
name: 'John Doe',
|
|
182
|
+
age: 30,
|
|
183
|
+
createdAt: new Date(),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### 4. Execute a Query from Controller
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { Controller, Get, Param } from '@nestjs/common';
|
|
193
|
+
import { MediatorService } from '@rolandsall24/nest-mediator';
|
|
194
|
+
import { GetUserByIdQuery } from './queries/get-user-by-id.query';
|
|
195
|
+
import { UserDto } from './dtos/user.dto';
|
|
196
|
+
|
|
197
|
+
@Controller('users')
|
|
198
|
+
export class UserController {
|
|
199
|
+
constructor(private readonly mediator: MediatorService) {}
|
|
200
|
+
|
|
201
|
+
@Get(':id')
|
|
202
|
+
async getById(@Param('id') id: string): Promise<UserDto> {
|
|
203
|
+
const query = new GetUserByIdQuery(id);
|
|
204
|
+
const user = await this.mediator.query<GetUserByIdQuery, UserDto>(query);
|
|
205
|
+
return user;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Complete Example
|
|
211
|
+
|
|
212
|
+
Here's a complete example following Domain-Driven Design principles with proper separation of concerns:
|
|
213
|
+
|
|
214
|
+
### Project Structure
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
src/
|
|
218
|
+
├── domain/
|
|
219
|
+
│ ├── entities/
|
|
220
|
+
│ │ ├── user.ts
|
|
221
|
+
│ │ └── index.ts
|
|
222
|
+
│ └── exceptions/
|
|
223
|
+
│ ├── domain.exception.ts
|
|
224
|
+
│ ├── user-not-found.exception.ts
|
|
225
|
+
│ └── index.ts
|
|
226
|
+
├── application/
|
|
227
|
+
│ └── user/
|
|
228
|
+
│ ├── create-user.command.ts
|
|
229
|
+
│ ├── create-user.handler.ts
|
|
230
|
+
│ ├── get-user.query.ts
|
|
231
|
+
│ ├── get-user.handler.ts
|
|
232
|
+
│ └── user-persistor.port.ts
|
|
233
|
+
├── infrastructure/
|
|
234
|
+
│ └── persistence/
|
|
235
|
+
│ └── user/
|
|
236
|
+
│ └── user-persistence.adapter.ts
|
|
237
|
+
├── presentation/
|
|
238
|
+
│ └── user/
|
|
239
|
+
│ ├── create-user-api.request.ts
|
|
240
|
+
│ ├── user-api.response.ts
|
|
241
|
+
│ └── user.controller.ts
|
|
242
|
+
└── app.module.ts
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Domain Layer
|
|
246
|
+
|
|
247
|
+
#### domain/entities/user.ts
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
export class User {
|
|
251
|
+
constructor(
|
|
252
|
+
public readonly id: string,
|
|
253
|
+
public readonly email: string,
|
|
254
|
+
public readonly name: string,
|
|
255
|
+
public readonly age: number,
|
|
256
|
+
public readonly createdAt: Date
|
|
257
|
+
) {}
|
|
258
|
+
|
|
259
|
+
static create(params: {
|
|
260
|
+
id: string;
|
|
261
|
+
email: string;
|
|
262
|
+
name: string;
|
|
263
|
+
age: number;
|
|
264
|
+
}): User {
|
|
265
|
+
const now = new Date();
|
|
266
|
+
return new User(
|
|
267
|
+
params.id,
|
|
268
|
+
params.email,
|
|
269
|
+
params.name,
|
|
270
|
+
params.age,
|
|
271
|
+
now
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### domain/exceptions/domain.exception.ts
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
export class DomainException extends Error {
|
|
281
|
+
constructor(message: string) {
|
|
282
|
+
super(message);
|
|
283
|
+
this.name = this.constructor.name;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
#### domain/exceptions/user-not-found.exception.ts
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
import { DomainException } from './domain.exception';
|
|
292
|
+
|
|
293
|
+
export class UserNotFoundException extends DomainException {
|
|
294
|
+
constructor(userId: string) {
|
|
295
|
+
super(`User with id ${userId} not found`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Application Layer
|
|
301
|
+
|
|
302
|
+
#### application/user/create-user.command.ts
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
import { ICommand } from '@rolandsall24/nest-mediator';
|
|
306
|
+
|
|
307
|
+
export class CreateUserCommand implements ICommand {
|
|
308
|
+
constructor(
|
|
309
|
+
public readonly email: string,
|
|
310
|
+
public readonly name: string,
|
|
311
|
+
public readonly age: number
|
|
312
|
+
) {}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
#### application/user/user-persistor.port.ts
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
import { User } from '../../domain/entities/user';
|
|
320
|
+
|
|
321
|
+
export interface UserPersistor {
|
|
322
|
+
save(user: User): Promise<User>;
|
|
323
|
+
findById(id: string): Promise<User | null>;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export const USER_PERSISTOR = Symbol('USER_PERSISTOR');
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
#### application/user/create-user.handler.ts
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
import { Injectable, Inject } from '@nestjs/common';
|
|
333
|
+
import { CommandHandler, ICommandHandler } from '@rolandsall24/nest-mediator';
|
|
334
|
+
import { randomUUID } from 'crypto';
|
|
335
|
+
import { CreateUserCommand } from './create-user.command';
|
|
336
|
+
import { User } from '../../domain/entities/user';
|
|
337
|
+
import { UserPersistor, USER_PERSISTOR } from './user-persistor.port';
|
|
338
|
+
|
|
339
|
+
@Injectable()
|
|
340
|
+
@CommandHandler(CreateUserCommand)
|
|
341
|
+
export class CreateUserCommandHandler implements ICommandHandler<CreateUserCommand> {
|
|
342
|
+
constructor(
|
|
343
|
+
@Inject(USER_PERSISTOR)
|
|
344
|
+
private readonly userPersistor: UserPersistor
|
|
345
|
+
) {}
|
|
346
|
+
|
|
347
|
+
async execute(command: CreateUserCommand): Promise<void> {
|
|
348
|
+
const id = randomUUID();
|
|
349
|
+
|
|
350
|
+
const user = User.create({
|
|
351
|
+
id,
|
|
352
|
+
email: command.email,
|
|
353
|
+
name: command.name,
|
|
354
|
+
age: command.age,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
await this.userPersistor.save(user);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### application/user/get-user.query.ts
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { IQuery } from '@rolandsall24/nest-mediator';
|
|
366
|
+
|
|
367
|
+
export class GetUserQuery implements IQuery {
|
|
368
|
+
constructor(public readonly id: string) {}
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
#### application/user/get-user.handler.ts
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import { Injectable, Inject } from '@nestjs/common';
|
|
376
|
+
import { QueryHandler, IQueryHandler } from '@rolandsall24/nest-mediator';
|
|
377
|
+
import { GetUserQuery } from './get-user.query';
|
|
378
|
+
import { User } from '../../domain/entities/user';
|
|
379
|
+
import { UserNotFoundException } from '../../domain/exceptions/user-not-found.exception';
|
|
380
|
+
import { UserPersistor, USER_PERSISTOR } from './user-persistor.port';
|
|
381
|
+
|
|
382
|
+
@Injectable()
|
|
383
|
+
@QueryHandler(GetUserQuery)
|
|
384
|
+
export class GetUserQueryHandler implements IQueryHandler<GetUserQuery, User> {
|
|
385
|
+
constructor(
|
|
386
|
+
@Inject(USER_PERSISTOR)
|
|
387
|
+
private readonly userPersistor: UserPersistor
|
|
388
|
+
) {}
|
|
389
|
+
|
|
390
|
+
async execute(query: GetUserQuery): Promise<User> {
|
|
391
|
+
const user = await this.userPersistor.findById(query.id);
|
|
392
|
+
|
|
393
|
+
if (!user) {
|
|
394
|
+
throw new UserNotFoundException(query.id);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return user;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Infrastructure Layer
|
|
403
|
+
|
|
404
|
+
#### infrastructure/persistence/user/user-persistence.adapter.ts
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
import { Injectable } from '@nestjs/common';
|
|
408
|
+
import { UserPersistor } from '../../../application/user/user-persistor.port';
|
|
409
|
+
import { User } from '../../../domain/entities/user';
|
|
410
|
+
|
|
411
|
+
@Injectable()
|
|
412
|
+
export class UserPersistenceAdapter implements UserPersistor {
|
|
413
|
+
// In-memory storage for demonstration
|
|
414
|
+
private users: Map<string, User> = new Map();
|
|
415
|
+
|
|
416
|
+
async save(user: User): Promise<User> {
|
|
417
|
+
this.users.set(user.id, user);
|
|
418
|
+
return user;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async findById(id: string): Promise<User | null> {
|
|
422
|
+
return this.users.get(id) || null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Presentation Layer
|
|
428
|
+
|
|
429
|
+
#### presentation/user/create-user-api.request.ts
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
export class CreateUserApiRequest {
|
|
433
|
+
email: string;
|
|
434
|
+
name: string;
|
|
435
|
+
age: number;
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
#### presentation/user/user-api.response.ts
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
export class UserApiResponse {
|
|
443
|
+
id: string;
|
|
444
|
+
email: string;
|
|
445
|
+
name: string;
|
|
446
|
+
age: number;
|
|
447
|
+
createdAt: Date;
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
#### presentation/user/user.controller.ts
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
import { Controller, Post, Body, Get, Param } from '@nestjs/common';
|
|
455
|
+
import { MediatorService } from '@rolandsall24/nest-mediator';
|
|
456
|
+
import { CreateUserCommand } from '../../application/user/create-user.command';
|
|
457
|
+
import { GetUserQuery } from '../../application/user/get-user.query';
|
|
458
|
+
import { CreateUserApiRequest } from './create-user-api.request';
|
|
459
|
+
import { UserApiResponse } from './user-api.response';
|
|
460
|
+
|
|
461
|
+
@Controller('users')
|
|
462
|
+
export class UserController {
|
|
463
|
+
constructor(private readonly mediator: MediatorService) {}
|
|
464
|
+
|
|
465
|
+
@Post()
|
|
466
|
+
async create(@Body() request: CreateUserApiRequest): Promise<void> {
|
|
467
|
+
const command = new CreateUserCommand(
|
|
468
|
+
request.email,
|
|
469
|
+
request.name,
|
|
470
|
+
request.age
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
await this.mediator.send(command);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
@Get(':id')
|
|
477
|
+
async getById(@Param('id') id: string): Promise<UserApiResponse> {
|
|
478
|
+
const query = new GetUserQuery(id);
|
|
479
|
+
const user = await this.mediator.query(query);
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
id: user.id,
|
|
483
|
+
email: user.email,
|
|
484
|
+
name: user.name,
|
|
485
|
+
age: user.age,
|
|
486
|
+
createdAt: user.createdAt,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Module Configuration
|
|
493
|
+
|
|
494
|
+
#### app.module.ts
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
import { Module } from '@nestjs/common';
|
|
498
|
+
import { NestMediatorModule } from '@rolandsall24/nest-mediator';
|
|
499
|
+
import { UserController } from './presentation/user/user.controller';
|
|
500
|
+
import { CreateUserCommandHandler } from './application/user/create-user.handler';
|
|
501
|
+
import { GetUserQueryHandler } from './application/user/get-user.handler';
|
|
502
|
+
import { USER_PERSISTOR } from './application/user/user-persistor.port';
|
|
503
|
+
import { UserPersistenceAdapter } from './infrastructure/persistence/user/user-persistence.adapter';
|
|
504
|
+
|
|
505
|
+
@Module({
|
|
506
|
+
imports: [
|
|
507
|
+
NestMediatorModule.forRoot({
|
|
508
|
+
handlers: [
|
|
509
|
+
CreateUserCommandHandler,
|
|
510
|
+
GetUserQueryHandler,
|
|
511
|
+
],
|
|
512
|
+
}),
|
|
513
|
+
],
|
|
514
|
+
controllers: [UserController],
|
|
515
|
+
providers: [
|
|
516
|
+
// Infrastructure
|
|
517
|
+
{
|
|
518
|
+
provide: USER_PERSISTOR,
|
|
519
|
+
useClass: UserPersistenceAdapter,
|
|
520
|
+
},
|
|
521
|
+
// IMPORTANT: Handlers must also be added here for dependency injection
|
|
522
|
+
CreateUserCommandHandler,
|
|
523
|
+
GetUserQueryHandler,
|
|
524
|
+
],
|
|
525
|
+
})
|
|
526
|
+
export class AppModule {}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Key Benefits
|
|
530
|
+
|
|
531
|
+
1. **Domain Layer**: Pure business logic, framework-agnostic
|
|
532
|
+
- Entities contain business rules and invariants
|
|
533
|
+
- Domain exceptions represent business errors
|
|
534
|
+
|
|
535
|
+
2. **Application Layer**: Use cases and business workflows
|
|
536
|
+
- Commands/Queries define application operations
|
|
537
|
+
- Handlers orchestrate domain objects and ports
|
|
538
|
+
- Ports (interfaces) define contracts for infrastructure
|
|
539
|
+
|
|
540
|
+
3. **Infrastructure Layer**: Technical implementations
|
|
541
|
+
- Adapters implement port interfaces
|
|
542
|
+
- Database, external services, file systems, etc.
|
|
543
|
+
|
|
544
|
+
4. **Presentation Layer**: API interface
|
|
545
|
+
- Controllers handle HTTP concerns
|
|
546
|
+
- DTOs for API request/response
|
|
547
|
+
- No business logic
|
|
548
|
+
|
|
549
|
+
This separation enables:
|
|
550
|
+
- Easy testing (mock ports/adapters)
|
|
551
|
+
- Technology independence (swap databases/frameworks)
|
|
552
|
+
- Clear boundaries and responsibilities
|
|
553
|
+
- Scalable architecture for growing applications
|
|
554
|
+
|
|
555
|
+
## API Reference
|
|
556
|
+
|
|
557
|
+
### Interfaces
|
|
558
|
+
|
|
559
|
+
#### `ICommand`
|
|
560
|
+
|
|
561
|
+
Marker interface for commands.
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
export interface ICommand {}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
#### `ICommandHandler<TCommand>`
|
|
568
|
+
|
|
569
|
+
Interface for command handlers.
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
export interface ICommandHandler<TCommand extends ICommand> {
|
|
573
|
+
execute(command: TCommand): Promise<void>;
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
#### `IQuery`
|
|
578
|
+
|
|
579
|
+
Marker interface for queries.
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
export interface IQuery {}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
#### `IQueryHandler<TQuery, TResult>`
|
|
586
|
+
|
|
587
|
+
Interface for query handlers.
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
export interface IQueryHandler<TQuery extends IQuery, TResult = any> {
|
|
591
|
+
execute(query: TQuery): Promise<TResult>;
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### Decorators
|
|
596
|
+
|
|
597
|
+
#### `@CommandHandler(command)`
|
|
598
|
+
|
|
599
|
+
Marks a class as a command handler.
|
|
600
|
+
|
|
601
|
+
- **Parameters**: `command` - The command class this handler handles
|
|
602
|
+
- **Usage**: Apply to handler classes that implement `ICommandHandler`
|
|
603
|
+
|
|
604
|
+
#### `@QueryHandler(query)`
|
|
605
|
+
|
|
606
|
+
Marks a class as a query handler.
|
|
607
|
+
|
|
608
|
+
- **Parameters**: `query` - The query class this handler handles
|
|
609
|
+
- **Usage**: Apply to handler classes that implement `IQueryHandler`
|
|
610
|
+
|
|
611
|
+
### Services
|
|
612
|
+
|
|
613
|
+
#### `MediatorService`
|
|
614
|
+
|
|
615
|
+
The main service for sending commands and queries.
|
|
616
|
+
|
|
617
|
+
##### Methods
|
|
618
|
+
|
|
619
|
+
**`send<TCommand>(command: TCommand): Promise<void>`**
|
|
620
|
+
|
|
621
|
+
Sends a command to its registered handler.
|
|
622
|
+
|
|
623
|
+
- **Parameters**: `command` - The command instance to execute
|
|
624
|
+
- **Returns**: Promise that resolves when the command is executed
|
|
625
|
+
- **Throws**: Error if no handler is registered for the command
|
|
626
|
+
|
|
627
|
+
**`query<TQuery, TResult>(query: TQuery): Promise<TResult>`**
|
|
628
|
+
|
|
629
|
+
Executes a query through its registered handler.
|
|
630
|
+
|
|
631
|
+
- **Parameters**: `query` - The query instance to execute
|
|
632
|
+
- **Returns**: Promise that resolves with the query result
|
|
633
|
+
- **Throws**: Error if no handler is registered for the query
|
|
634
|
+
|
|
635
|
+
## Best Practices
|
|
636
|
+
|
|
637
|
+
1. **Keep Commands and Queries Simple**: They should be simple data containers with minimal logic.
|
|
638
|
+
|
|
639
|
+
2. **One Handler Per Command/Query**: Each command or query should have exactly one handler.
|
|
640
|
+
|
|
641
|
+
3. **Use Dependency Injection**: Inject required services into your handlers through the constructor.
|
|
642
|
+
|
|
643
|
+
4. **Type Safety**: Always specify the return type for queries using the generic parameters.
|
|
644
|
+
|
|
645
|
+
5. **Error Handling**: Implement proper error handling in your handlers.
|
|
646
|
+
|
|
647
|
+
6. **Validation**: Validate command/query data before creating instances or in the handler.
|
|
648
|
+
|
|
649
|
+
## License
|
|
650
|
+
|
|
651
|
+
MIT
|
|
652
|
+
|
|
653
|
+
## Contributing
|
|
654
|
+
|
|
655
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,2BAA2B,CAAC;AAG1C,cAAc,2BAA2B,CAAC;AAG1C,cAAc,yBAAyB,CAAC;AAGxC,cAAc,+BAA+B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ICommand } from '../interfaces/index.js';
|
|
2
|
+
export declare const COMMAND_HANDLER_METADATA = "COMMAND_HANDLER_METADATA";
|
|
3
|
+
/**
|
|
4
|
+
* Decorator to mark a class as a command handler
|
|
5
|
+
* @param command - The command class that this handler handles
|
|
6
|
+
* @returns Class decorator
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* @CommandHandler(AddCategoryCommand)
|
|
11
|
+
* export class AddCategoryCommandHandler implements ICommandHandler<AddCategoryCommand> {
|
|
12
|
+
* async execute(command: AddCategoryCommand): Promise<Category> {
|
|
13
|
+
* // Implementation
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare const CommandHandler: (command: new (...args: any[]) => ICommand) => ClassDecorator;
|
|
19
|
+
//# sourceMappingURL=command-handler.decorator.d.ts.map
|