@jdevel/tnest 0.0.2 → 0.0.4
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/README.md +260 -86
- package/dist/__tests__/constants.spec.d.ts +1 -0
- package/dist/__tests__/constants.spec.js +21 -0
- package/dist/__tests__/constants.spec.js.map +1 -0
- package/dist/__tests__/tnest-module.spec.d.ts +1 -0
- package/dist/__tests__/tnest-module.spec.js +88 -0
- package/dist/__tests__/tnest-module.spec.js.map +1 -0
- package/dist/client/__tests__/typed-client-factory.spec.d.ts +1 -0
- package/dist/client/__tests__/typed-client-factory.spec.js +35 -0
- package/dist/client/__tests__/typed-client-factory.spec.js.map +1 -0
- package/dist/client/__tests__/typed-client-types.spec.d.ts +1 -0
- package/dist/client/__tests__/typed-client-types.spec.js +18 -0
- package/dist/client/__tests__/typed-client-types.spec.js.map +1 -0
- package/dist/client/__tests__/typed-client.spec.d.ts +1 -0
- package/dist/client/__tests__/typed-client.spec.js +59 -0
- package/dist/client/__tests__/typed-client.spec.js.map +1 -0
- package/dist/contracts/__tests__/contract-types.spec.d.ts +1 -0
- package/dist/contracts/__tests__/contract-types.spec.js +91 -0
- package/dist/contracts/__tests__/contract-types.spec.js.map +1 -0
- package/dist/contracts/__tests__/define-helpers.spec.d.ts +1 -0
- package/dist/contracts/__tests__/define-helpers.spec.js +52 -0
- package/dist/contracts/__tests__/define-helpers.spec.js.map +1 -0
- package/dist/contracts/define-helpers.js.map +1 -1
- package/dist/handlers/__tests__/handler-types.spec.d.ts +1 -0
- package/dist/handlers/__tests__/handler-types.spec.js +17 -0
- package/dist/handlers/__tests__/handler-types.spec.js.map +1 -0
- package/dist/handlers/__tests__/typed-event-pattern.spec.d.ts +1 -0
- package/dist/handlers/__tests__/typed-event-pattern.spec.js +61 -0
- package/dist/handlers/__tests__/typed-event-pattern.spec.js.map +1 -0
- package/dist/handlers/__tests__/typed-message-pattern.spec.d.ts +1 -0
- package/dist/handlers/__tests__/typed-message-pattern.spec.js +94 -0
- package/dist/handlers/__tests__/typed-message-pattern.spec.js.map +1 -0
- package/dist/handlers/typed-event-pattern.decorator.d.ts +4 -2
- package/dist/handlers/typed-event-pattern.decorator.js.map +1 -1
- package/dist/handlers/typed-message-pattern.decorator.d.ts +5 -2
- package/dist/handlers/typed-message-pattern.decorator.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/module-options.d.ts +1 -0
- package/dist/serialization/__tests__/default-serializer.spec.d.ts +1 -0
- package/dist/serialization/__tests__/default-serializer.spec.js +32 -0
- package/dist/serialization/__tests__/default-serializer.spec.js.map +1 -0
- package/dist/testing/__tests__/mock-typed-client.spec.d.ts +1 -0
- package/dist/testing/__tests__/mock-typed-client.spec.js +70 -0
- package/dist/testing/__tests__/mock-typed-client.spec.js.map +1 -0
- package/dist/testing/__tests__/test-contract-module.spec.d.ts +1 -0
- package/dist/testing/__tests__/test-contract-module.spec.js +30 -0
- package/dist/testing/__tests__/test-contract-module.spec.js.map +1 -0
- package/dist/testing/mock-typed-client.js.map +1 -1
- package/dist/tnest.module.d.ts +1 -0
- package/dist/tnest.module.js +21 -20
- package/dist/tnest.module.js.map +1 -1
- package/dist/validation/__tests__/validate-contract.spec.d.ts +1 -0
- package/dist/validation/__tests__/validate-contract.spec.js +102 -0
- package/dist/validation/__tests__/validate-contract.spec.js.map +1 -0
- package/dist/validation/validate-contract.decorator.js.map +1 -1
- package/package.json +30 -5
package/README.md
CHANGED
|
@@ -30,11 +30,11 @@ npm install @nestjs/common @nestjs/microservices reflect-metadata rxjs
|
|
|
30
30
|
|
|
31
31
|
Contracts describe the messages exchanged between services. There are three kinds:
|
|
32
32
|
|
|
33
|
-
| Type
|
|
34
|
-
|
|
35
|
-
| `Command` | Write operation (request/response) | Yes
|
|
36
|
-
| `Query`
|
|
37
|
-
| `Event`
|
|
33
|
+
| Type | Purpose | Has response? |
|
|
34
|
+
| --------- | ---------------------------------- | ------------- |
|
|
35
|
+
| `Command` | Write operation (request/response) | Yes |
|
|
36
|
+
| `Query` | Read operation (request/response) | Yes |
|
|
37
|
+
| `Event` | Notification (fire-and-forget) | No |
|
|
38
38
|
|
|
39
39
|
```ts
|
|
40
40
|
// contracts/user.contracts.ts
|
|
@@ -63,9 +63,9 @@ export type UserContracts = typeof userContracts;
|
|
|
63
63
|
You can also define contracts with explicit interfaces if you prefer:
|
|
64
64
|
|
|
65
65
|
```ts
|
|
66
|
-
import type { Command, Event, Query } from '@jdevel/tnest';
|
|
66
|
+
import type { Command, Event, Query, ContractRegistry } from '@jdevel/tnest';
|
|
67
67
|
|
|
68
|
-
export interface UserContracts {
|
|
68
|
+
export interface UserContracts extends ContractRegistry {
|
|
69
69
|
'user.create': Command<'user.create', CreateUserDto, User>;
|
|
70
70
|
'user.created': Event<'user.created', { userId: string; email: string }>;
|
|
71
71
|
'user.get': Query<'user.get', { id: string }, User>;
|
|
@@ -74,6 +74,10 @@ export interface UserContracts {
|
|
|
74
74
|
|
|
75
75
|
### 2. Register the module
|
|
76
76
|
|
|
77
|
+
#### Static configuration with `forRoot`
|
|
78
|
+
|
|
79
|
+
Use `forRoot` when your transport configuration is known at compile time:
|
|
80
|
+
|
|
77
81
|
```ts
|
|
78
82
|
// app.module.ts
|
|
79
83
|
import { Module } from '@nestjs/common';
|
|
@@ -91,6 +95,13 @@ import { TnestModule } from '@jdevel/tnest';
|
|
|
91
95
|
options: { host: 'localhost', port: 3001 },
|
|
92
96
|
},
|
|
93
97
|
},
|
|
98
|
+
{
|
|
99
|
+
name: 'NOTIFICATION_SERVICE',
|
|
100
|
+
options: {
|
|
101
|
+
transport: Transport.REDIS,
|
|
102
|
+
options: { host: 'localhost', port: 6379 },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
94
105
|
],
|
|
95
106
|
}),
|
|
96
107
|
],
|
|
@@ -98,6 +109,107 @@ import { TnestModule } from '@jdevel/tnest';
|
|
|
98
109
|
export class AppModule {}
|
|
99
110
|
```
|
|
100
111
|
|
|
112
|
+
#### Async configuration with `forRootAsync`
|
|
113
|
+
|
|
114
|
+
Use `forRootAsync` when transport configuration comes from `ConfigService`, environment variables, or another async source.
|
|
115
|
+
|
|
116
|
+
**With `useFactory`:**
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { Module } from '@nestjs/common';
|
|
120
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
121
|
+
import { Transport } from '@nestjs/microservices';
|
|
122
|
+
import { TnestModule } from '@jdevel/tnest';
|
|
123
|
+
|
|
124
|
+
@Module({
|
|
125
|
+
imports: [
|
|
126
|
+
TnestModule.forRootAsync({
|
|
127
|
+
imports: [ConfigModule],
|
|
128
|
+
// Declare client names upfront so they can be injected before the factory resolves
|
|
129
|
+
clientNames: ['USER_SERVICE', 'NOTIFICATION_SERVICE'],
|
|
130
|
+
useFactory: (config: ConfigService) => ({
|
|
131
|
+
clients: [
|
|
132
|
+
{
|
|
133
|
+
name: 'USER_SERVICE',
|
|
134
|
+
options: {
|
|
135
|
+
transport: Transport.TCP,
|
|
136
|
+
options: {
|
|
137
|
+
host: config.get('USER_SERVICE_HOST'),
|
|
138
|
+
port: config.get<number>('USER_SERVICE_PORT'),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'NOTIFICATION_SERVICE',
|
|
144
|
+
options: {
|
|
145
|
+
transport: Transport.REDIS,
|
|
146
|
+
options: {
|
|
147
|
+
host: config.get('REDIS_HOST'),
|
|
148
|
+
port: config.get<number>('REDIS_PORT'),
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
}),
|
|
154
|
+
inject: [ConfigService],
|
|
155
|
+
}),
|
|
156
|
+
],
|
|
157
|
+
})
|
|
158
|
+
export class AppModule {}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**With `useClass`:**
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { Injectable } from '@nestjs/common';
|
|
165
|
+
import { Transport } from '@nestjs/microservices';
|
|
166
|
+
import { TnestModule, type TnestOptionsFactory, type TnestModuleOptions } from '@jdevel/tnest';
|
|
167
|
+
|
|
168
|
+
@Injectable()
|
|
169
|
+
class TnestConfigService implements TnestOptionsFactory {
|
|
170
|
+
createTnestOptions(): TnestModuleOptions {
|
|
171
|
+
return {
|
|
172
|
+
clients: [
|
|
173
|
+
{
|
|
174
|
+
name: 'USER_SERVICE',
|
|
175
|
+
options: {
|
|
176
|
+
transport: Transport.TCP,
|
|
177
|
+
options: { host: 'localhost', port: 3001 },
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@Module({
|
|
186
|
+
imports: [
|
|
187
|
+
TnestModule.forRootAsync({
|
|
188
|
+
clientNames: ['USER_SERVICE'],
|
|
189
|
+
useClass: TnestConfigService,
|
|
190
|
+
}),
|
|
191
|
+
],
|
|
192
|
+
})
|
|
193
|
+
export class AppModule {}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**With `useExisting`:**
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
@Module({
|
|
200
|
+
imports: [
|
|
201
|
+
TnestModule.forRootAsync({
|
|
202
|
+
clientNames: ['USER_SERVICE'],
|
|
203
|
+
useExisting: TnestConfigService, // reuse an already-registered provider
|
|
204
|
+
imports: [ConfigModule],
|
|
205
|
+
}),
|
|
206
|
+
],
|
|
207
|
+
})
|
|
208
|
+
export class AppModule {}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
> **Note:** The `clientNames` array tells the module which client tokens to register upfront. Each name must match a `name` in the `clients` array returned by the factory. Without `clientNames`, async clients won't be injectable by token.
|
|
212
|
+
|
|
101
213
|
### 3. Send messages (producer)
|
|
102
214
|
|
|
103
215
|
```ts
|
|
@@ -120,10 +232,10 @@ export class OrderService {
|
|
|
120
232
|
}
|
|
121
233
|
|
|
122
234
|
async createOrder(userId: string) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
);
|
|
235
|
+
// send() for commands and queries — type-safe pattern, payload, and response
|
|
236
|
+
const user = await firstValueFrom(this.users.send('user.get', { id: userId }));
|
|
126
237
|
|
|
238
|
+
// emit() for events — type-safe pattern and payload, no response
|
|
127
239
|
this.users.emit('user.created', {
|
|
128
240
|
userId: user.id,
|
|
129
241
|
email: user.email,
|
|
@@ -147,8 +259,28 @@ this.users.send('user.created', { userId: '1', email: 'a@b.com' });
|
|
|
147
259
|
// ~~~~~~~~~~~~~~ ERROR: 'user.created' is an event, use emit()
|
|
148
260
|
```
|
|
149
261
|
|
|
262
|
+
When using the second type parameter on handler decorators, the compiler also catches handler signature mismatches:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
@TypedMessagePattern<UserContracts, 'user.create'>('user.create')
|
|
266
|
+
async create(payload: { wrong: number }) {
|
|
267
|
+
// ~~~~~~~~~~~~~~~~ ERROR: expects { email: string; name: string }
|
|
268
|
+
return { id: '1' };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@TypedMessagePattern<UserContracts, 'user.get'>('user.get')
|
|
272
|
+
async get(payload: { id: string }): Promise<{ wrong: boolean }> {
|
|
273
|
+
// ~~~~~~~~~~~~~~~~~ ERROR: must return User
|
|
274
|
+
return { wrong: true };
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
> **Note:** Omitting the second type parameter preserves the existing behavior — the decorator validates the pattern string but does not constrain the method signature.
|
|
279
|
+
|
|
150
280
|
### 4. Handle messages (consumer)
|
|
151
281
|
|
|
282
|
+
#### Using decorators
|
|
283
|
+
|
|
152
284
|
```ts
|
|
153
285
|
// user.controller.ts
|
|
154
286
|
import { Controller } from '@nestjs/common';
|
|
@@ -157,25 +289,56 @@ import type { UserContracts } from './contracts/user.contracts';
|
|
|
157
289
|
|
|
158
290
|
@Controller()
|
|
159
291
|
export class UserController {
|
|
160
|
-
|
|
292
|
+
// Pass the pattern as a second type parameter to enforce the method signature.
|
|
293
|
+
// The compiler will error if the payload or return type doesn't match the contract.
|
|
294
|
+
@TypedMessagePattern<UserContracts, 'user.create'>('user.create')
|
|
161
295
|
async create(payload: { email: string; name: string }) {
|
|
162
296
|
return { id: crypto.randomUUID(), ...payload };
|
|
163
297
|
}
|
|
164
298
|
|
|
165
|
-
@TypedMessagePattern<UserContracts>('user.get')
|
|
299
|
+
@TypedMessagePattern<UserContracts, 'user.get'>('user.get')
|
|
166
300
|
async get(payload: { id: string }) {
|
|
167
301
|
return { id: payload.id, email: 'user@example.com', name: 'Example' };
|
|
168
302
|
}
|
|
169
303
|
|
|
170
|
-
@TypedEventPattern<UserContracts>('user.created')
|
|
304
|
+
@TypedEventPattern<UserContracts, 'user.created'>('user.created')
|
|
171
305
|
async handleCreated(payload: { userId: string; email: string }) {
|
|
172
306
|
console.log(`User created: ${payload.userId}`);
|
|
173
307
|
}
|
|
174
308
|
}
|
|
175
309
|
```
|
|
176
310
|
|
|
311
|
+
#### Using handler type helpers
|
|
312
|
+
|
|
313
|
+
If you prefer explicit typing without decorators, use `TypedMessageHandler` and `TypedEventHandler`:
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
import type { TypedMessageHandler, TypedEventHandler } from '@jdevel/tnest';
|
|
317
|
+
import { MessagePattern, EventPattern } from '@nestjs/microservices';
|
|
318
|
+
import type { UserContracts } from './contracts/user.contracts';
|
|
319
|
+
|
|
320
|
+
@Controller()
|
|
321
|
+
export class UserController {
|
|
322
|
+
@MessagePattern('user.create')
|
|
323
|
+
create: TypedMessageHandler<UserContracts, 'user.create'> = async (payload) => {
|
|
324
|
+
// payload is typed as { email: string; name: string }
|
|
325
|
+
// return type is enforced as User | Promise<User> | Observable<User>
|
|
326
|
+
return { id: crypto.randomUUID(), ...payload };
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
@EventPattern('user.created')
|
|
330
|
+
handleCreated: TypedEventHandler<UserContracts, 'user.created'> = async (payload) => {
|
|
331
|
+
// payload is typed as { userId: string; email: string }
|
|
332
|
+
// return type is enforced as void | Promise<void>
|
|
333
|
+
console.log(`User created: ${payload.userId}`);
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
177
338
|
## Testing
|
|
178
339
|
|
|
340
|
+
### MockTypedClient
|
|
341
|
+
|
|
179
342
|
`MockTypedClient` records every message and returns configurable canned responses:
|
|
180
343
|
|
|
181
344
|
```ts
|
|
@@ -185,66 +348,45 @@ import type { UserContracts } from './contracts/user.contracts';
|
|
|
185
348
|
|
|
186
349
|
const mock = new MockTypedClient<UserContracts>();
|
|
187
350
|
|
|
351
|
+
// Set up canned responses
|
|
188
352
|
mock.setResponse('user.get', {
|
|
189
353
|
id: '42',
|
|
190
354
|
email: 'test@example.com',
|
|
191
355
|
name: 'Test User',
|
|
192
356
|
});
|
|
193
357
|
|
|
358
|
+
// Use the mock exactly like a real TypedClient
|
|
194
359
|
const user = await firstValueFrom(mock.send('user.get', { id: '42' }));
|
|
195
360
|
// user.id === '42'
|
|
196
361
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
362
|
+
// Assert on recorded messages
|
|
363
|
+
expect(mock.messages).toEqual([{ type: 'send', pattern: 'user.get', payload: { id: '42' } }]);
|
|
364
|
+
|
|
365
|
+
// Reset between tests
|
|
366
|
+
mock.reset();
|
|
200
367
|
```
|
|
201
368
|
|
|
369
|
+
### TestContractModule
|
|
370
|
+
|
|
202
371
|
For integration tests, `TestContractModule` swaps real clients for mocks:
|
|
203
372
|
|
|
204
373
|
```ts
|
|
205
374
|
import { Test } from '@nestjs/testing';
|
|
206
|
-
import { MockTypedClient, TestContractModule } from '@jdevel/tnest';
|
|
375
|
+
import { MockTypedClient, TestContractModule, getClientToken } from '@jdevel/tnest';
|
|
207
376
|
import type { UserContracts } from './contracts/user.contracts';
|
|
208
377
|
|
|
209
|
-
const
|
|
378
|
+
const mockUserClient = new MockTypedClient<UserContracts>();
|
|
210
379
|
|
|
211
380
|
const module = await Test.createTestingModule({
|
|
212
|
-
imports: [
|
|
213
|
-
TestContractModule.register([
|
|
214
|
-
{ name: 'USER_SERVICE', mock },
|
|
215
|
-
]),
|
|
216
|
-
],
|
|
381
|
+
imports: [TestContractModule.register([{ name: 'USER_SERVICE', mock: mockUserClient }])],
|
|
217
382
|
providers: [OrderService],
|
|
218
383
|
}).compile();
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
## Async Module Configuration
|
|
222
384
|
|
|
223
|
-
|
|
385
|
+
const service = module.get(OrderService);
|
|
224
386
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
import { Transport } from '@nestjs/microservices';
|
|
229
|
-
|
|
230
|
-
TnestModule.forRootAsync({
|
|
231
|
-
imports: [ConfigModule],
|
|
232
|
-
useFactory: (config: ConfigService) => ({
|
|
233
|
-
clients: [
|
|
234
|
-
{
|
|
235
|
-
name: 'USER_SERVICE',
|
|
236
|
-
options: {
|
|
237
|
-
transport: Transport.TCP,
|
|
238
|
-
options: {
|
|
239
|
-
host: config.get('USER_SERVICE_HOST'),
|
|
240
|
-
port: config.get('USER_SERVICE_PORT'),
|
|
241
|
-
},
|
|
242
|
-
},
|
|
243
|
-
},
|
|
244
|
-
],
|
|
245
|
-
}),
|
|
246
|
-
inject: [ConfigService],
|
|
247
|
-
});
|
|
387
|
+
// The mock is also retrievable by token
|
|
388
|
+
const client = module.get(getClientToken('USER_SERVICE'));
|
|
389
|
+
// client === mockUserClient
|
|
248
390
|
```
|
|
249
391
|
|
|
250
392
|
## Runtime Validation (Optional)
|
|
@@ -261,17 +403,18 @@ import {
|
|
|
261
403
|
} from '@jdevel/tnest';
|
|
262
404
|
|
|
263
405
|
@Injectable()
|
|
264
|
-
class
|
|
406
|
+
class ZodValidator implements ContractValidator {
|
|
265
407
|
validate(payload: unknown): void {
|
|
266
408
|
// your validation logic (zod, class-validator, joi, etc.)
|
|
409
|
+
// throw an error if validation fails
|
|
267
410
|
}
|
|
268
411
|
}
|
|
269
412
|
|
|
270
413
|
// Register in module providers
|
|
271
|
-
{ provide: CONTRACT_VALIDATOR, useClass:
|
|
414
|
+
{ provide: CONTRACT_VALIDATOR, useClass: ZodValidator }
|
|
272
415
|
|
|
273
|
-
// Apply to handlers
|
|
274
|
-
@TypedMessagePattern<UserContracts>('user.create')
|
|
416
|
+
// Apply to handlers — validation runs before the handler method
|
|
417
|
+
@TypedMessagePattern<UserContracts, 'user.create'>('user.create')
|
|
275
418
|
@ValidateContract()
|
|
276
419
|
async create(payload: CreateUserDto) {
|
|
277
420
|
// payload has been validated at runtime
|
|
@@ -283,6 +426,7 @@ async create(payload: CreateUserDto) {
|
|
|
283
426
|
Provide custom payload serialization (protobuf, msgpack, etc.) by implementing `PayloadSerializer` and `PayloadDeserializer`:
|
|
284
427
|
|
|
285
428
|
```ts
|
|
429
|
+
import { Injectable } from '@nestjs/common';
|
|
286
430
|
import {
|
|
287
431
|
PAYLOAD_SERIALIZER,
|
|
288
432
|
PAYLOAD_DESERIALIZER,
|
|
@@ -292,8 +436,12 @@ import {
|
|
|
292
436
|
|
|
293
437
|
@Injectable()
|
|
294
438
|
class MsgpackSerializer implements PayloadSerializer, PayloadDeserializer {
|
|
295
|
-
serialize(payload: unknown) {
|
|
296
|
-
|
|
439
|
+
serialize(payload: unknown) {
|
|
440
|
+
return msgpack.encode(payload);
|
|
441
|
+
}
|
|
442
|
+
deserialize(data: unknown) {
|
|
443
|
+
return msgpack.decode(data as Buffer);
|
|
444
|
+
}
|
|
297
445
|
}
|
|
298
446
|
|
|
299
447
|
// Register in module providers
|
|
@@ -301,12 +449,15 @@ class MsgpackSerializer implements PayloadSerializer, PayloadDeserializer {
|
|
|
301
449
|
{ provide: PAYLOAD_DESERIALIZER, useClass: MsgpackSerializer }
|
|
302
450
|
```
|
|
303
451
|
|
|
452
|
+
A `DefaultPayloadSerializer` (pass-through) is included and used when no custom serializer is registered.
|
|
453
|
+
|
|
304
454
|
## Utility Types
|
|
305
455
|
|
|
306
456
|
Extract type information from your contract registry:
|
|
307
457
|
|
|
308
458
|
```ts
|
|
309
459
|
import type {
|
|
460
|
+
PatternOf,
|
|
310
461
|
PayloadOf,
|
|
311
462
|
ResponseOf,
|
|
312
463
|
CommandPatterns,
|
|
@@ -316,42 +467,65 @@ import type {
|
|
|
316
467
|
CommandsOf,
|
|
317
468
|
EventsOf,
|
|
318
469
|
QueriesOf,
|
|
470
|
+
ValidateRegistry,
|
|
319
471
|
} from '@jdevel/tnest';
|
|
320
472
|
|
|
321
|
-
|
|
322
|
-
type
|
|
323
|
-
type
|
|
324
|
-
type
|
|
325
|
-
|
|
326
|
-
|
|
473
|
+
// Extract parts of a single contract
|
|
474
|
+
type CreatePayload = PayloadOf<UserContracts['user.create']>; // CreateUserDto
|
|
475
|
+
type CreateResponse = ResponseOf<UserContracts['user.create']>; // User
|
|
476
|
+
type CreatePattern = PatternOf<UserContracts['user.create']>; // 'user.create'
|
|
477
|
+
|
|
478
|
+
// Pattern string unions by contract kind
|
|
479
|
+
type Cmds = CommandPatterns<UserContracts>; // 'user.create'
|
|
480
|
+
type Evts = EventPatterns<UserContracts>; // 'user.created'
|
|
481
|
+
type Qrys = QueryPatterns<UserContracts>; // 'user.get'
|
|
482
|
+
type Sendable = SendablePatterns<UserContracts>; // 'user.create' | 'user.get'
|
|
483
|
+
|
|
484
|
+
// Filter a registry to contracts of a specific kind
|
|
485
|
+
type OnlyCommands = CommandsOf<UserContracts>; // { 'user.create': Command<...> }
|
|
486
|
+
type OnlyEvents = EventsOf<UserContracts>; // { 'user.created': Event<...> }
|
|
487
|
+
type OnlyQueries = QueriesOf<UserContracts>; // { 'user.get': Query<...> }
|
|
488
|
+
|
|
489
|
+
// Validate that a registry's keys match its pattern type parameters
|
|
490
|
+
type Validated = ValidateRegistry<UserContracts>;
|
|
491
|
+
// Produces an error type if any key doesn't match its contract's pattern
|
|
327
492
|
```
|
|
328
493
|
|
|
329
494
|
## API Reference
|
|
330
495
|
|
|
331
|
-
| Export
|
|
332
|
-
|
|
333
|
-
| `TnestModule`
|
|
334
|
-
| `TypedClient`
|
|
335
|
-
| `TypedClientFactory`
|
|
336
|
-
| `TypedMessagePattern`
|
|
337
|
-
| `TypedEventPattern`
|
|
338
|
-
| `
|
|
339
|
-
| `
|
|
340
|
-
| `
|
|
341
|
-
| `getClientToken`
|
|
342
|
-
| `defineRegistry`
|
|
343
|
-
| `command` / `event` / `query`
|
|
344
|
-
| `Command` / `Event` / `Query`
|
|
345
|
-
| `ContractRegistry`
|
|
346
|
-
| `
|
|
347
|
-
| `
|
|
348
|
-
| `
|
|
349
|
-
| `
|
|
350
|
-
| `
|
|
351
|
-
| `
|
|
352
|
-
| `
|
|
353
|
-
| `
|
|
496
|
+
| Export | Kind | Description |
|
|
497
|
+
| ----------------------------------------------------- | --------- | --------------------------------------------------- |
|
|
498
|
+
| `TnestModule` | Module | Dynamic module with `forRoot` / `forRootAsync` |
|
|
499
|
+
| `TypedClient` | Class | Type-safe wrapper around `ClientProxy` |
|
|
500
|
+
| `TypedClientFactory` | Service | Creates `TypedClient` instances |
|
|
501
|
+
| `TypedMessagePattern` | Decorator | Typed `@MessagePattern` for commands/queries |
|
|
502
|
+
| `TypedEventPattern` | Decorator | Typed `@EventPattern` for events |
|
|
503
|
+
| `ValidateContract` | Decorator | Opt-in runtime payload validation |
|
|
504
|
+
| `MockTypedClient` | Class | Test double that records messages |
|
|
505
|
+
| `TestContractModule` | Module | Registers mock clients for testing |
|
|
506
|
+
| `getClientToken` | Function | Returns injection token for a named client |
|
|
507
|
+
| `defineRegistry` | Function | Builder helper for defining contract registries |
|
|
508
|
+
| `command` / `event` / `query` | Functions | Builder helpers for individual contracts |
|
|
509
|
+
| `Command` / `Event` / `Query` | Type | Contract type interfaces |
|
|
510
|
+
| `ContractRegistry` | Type | Base type for a registry of contracts |
|
|
511
|
+
| `ValidateRegistry` | Type | Validates registry keys match contract patterns |
|
|
512
|
+
| `PayloadOf` / `ResponseOf` / `PatternOf` | Type | Extract parts of a contract |
|
|
513
|
+
| `CommandsOf` / `EventsOf` / `QueriesOf` | Type | Filter registry by contract kind |
|
|
514
|
+
| `CommandPatterns` / `EventPatterns` / `QueryPatterns` | Type | Pattern string unions by kind |
|
|
515
|
+
| `SendablePatterns` | Type | Union of command + query patterns |
|
|
516
|
+
| `TypedMessageHandler` | Type | Function signature for command/query handlers |
|
|
517
|
+
| `TypedEventHandler` | Type | Function signature for event handlers |
|
|
518
|
+
| `TnestModuleOptions` | Interface | Configuration for `forRoot` |
|
|
519
|
+
| `TnestModuleAsyncOptions` | Interface | Configuration for `forRootAsync` |
|
|
520
|
+
| `TnestOptionsFactory` | Interface | Implement for `useClass`/`useExisting` async config |
|
|
521
|
+
| `TnestClientDefinition` | Interface | Client name + transport options pair |
|
|
522
|
+
| `ContractValidator` | Interface | Implement for runtime validation |
|
|
523
|
+
| `PayloadSerializer` / `PayloadDeserializer` | Interface | Implement for custom serialization |
|
|
524
|
+
| `DefaultPayloadSerializer` | Class | Pass-through serializer (default) |
|
|
525
|
+
| `CONTRACT_VALIDATOR` | Token | Injection token for validator |
|
|
526
|
+
| `PAYLOAD_SERIALIZER` / `PAYLOAD_DESERIALIZER` | Token | Injection tokens for serialization |
|
|
527
|
+
| `TNEST_OPTIONS` | Token | Injection token for module options |
|
|
354
528
|
|
|
355
529
|
## License
|
|
356
530
|
|
|
357
|
-
MIT
|
|
531
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const constants_1 = require("../constants");
|
|
4
|
+
describe('TNEST_OPTIONS', () => {
|
|
5
|
+
it('is a Symbol', () => {
|
|
6
|
+
expect(typeof constants_1.TNEST_OPTIONS).toBe('symbol');
|
|
7
|
+
});
|
|
8
|
+
});
|
|
9
|
+
describe('getClientToken()', () => {
|
|
10
|
+
it('returns a string prefixed with TNEST_CLIENT_PREFIX', () => {
|
|
11
|
+
const token = (0, constants_1.getClientToken)('USER_SERVICE');
|
|
12
|
+
expect(token).toBe(`${constants_1.TNEST_CLIENT_PREFIX}USER_SERVICE`);
|
|
13
|
+
});
|
|
14
|
+
it('returns deterministic tokens for the same name', () => {
|
|
15
|
+
expect((0, constants_1.getClientToken)('SVC')).toBe((0, constants_1.getClientToken)('SVC'));
|
|
16
|
+
});
|
|
17
|
+
it('returns different tokens for different names', () => {
|
|
18
|
+
expect((0, constants_1.getClientToken)('A')).not.toBe((0, constants_1.getClientToken)('B'));
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
//# sourceMappingURL=constants.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.spec.js","sourceRoot":"","sources":["../../src/__tests__/constants.spec.ts"],"names":[],"mappings":";;AAAA,4CAAkF;AAElF,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,aAAa,EAAE,GAAG,EAAE;QACrB,MAAM,CAAC,OAAO,yBAAa,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,KAAK,GAAG,IAAA,0BAAc,EAAC,cAAc,CAAC,CAAC;QAC7C,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,+BAAmB,cAAc,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,CAAC,IAAA,0BAAc,EAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAA,0BAAc,EAAC,KAAK,CAAC,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,CAAC,IAAA,0BAAc,EAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAA,0BAAc,EAAC,GAAG,CAAC,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const testing_1 = require("@nestjs/testing");
|
|
4
|
+
const tnest_module_1 = require("../tnest.module");
|
|
5
|
+
const client_1 = require("../client");
|
|
6
|
+
const constants_1 = require("../constants");
|
|
7
|
+
describe('TnestModule', () => {
|
|
8
|
+
describe('forRoot()', () => {
|
|
9
|
+
it('provides TypedClientFactory', async () => {
|
|
10
|
+
const module = await testing_1.Test.createTestingModule({
|
|
11
|
+
imports: [tnest_module_1.TnestModule.forRoot()],
|
|
12
|
+
}).compile();
|
|
13
|
+
const factory = module.get(client_1.TypedClientFactory);
|
|
14
|
+
expect(factory).toBeInstanceOf(client_1.TypedClientFactory);
|
|
15
|
+
});
|
|
16
|
+
it('provides TNEST_OPTIONS', async () => {
|
|
17
|
+
const options = { clients: [] };
|
|
18
|
+
const module = await testing_1.Test.createTestingModule({
|
|
19
|
+
imports: [tnest_module_1.TnestModule.forRoot(options)],
|
|
20
|
+
}).compile();
|
|
21
|
+
const resolved = module.get(constants_1.TNEST_OPTIONS);
|
|
22
|
+
expect(resolved).toBe(options);
|
|
23
|
+
});
|
|
24
|
+
it('registers named client tokens from client definitions', async () => {
|
|
25
|
+
const module = await testing_1.Test.createTestingModule({
|
|
26
|
+
imports: [
|
|
27
|
+
tnest_module_1.TnestModule.forRoot({
|
|
28
|
+
clients: [{ name: 'USER_SERVICE', options: { transport: 0 } }],
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
}).compile();
|
|
32
|
+
const token = (0, constants_1.getClientToken)('USER_SERVICE');
|
|
33
|
+
const client = module.get(token);
|
|
34
|
+
expect(client).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('forRootAsync()', () => {
|
|
38
|
+
it('provides TypedClientFactory', async () => {
|
|
39
|
+
const module = await testing_1.Test.createTestingModule({
|
|
40
|
+
imports: [
|
|
41
|
+
tnest_module_1.TnestModule.forRootAsync({
|
|
42
|
+
useFactory: () => ({ clients: [] }),
|
|
43
|
+
}),
|
|
44
|
+
],
|
|
45
|
+
}).compile();
|
|
46
|
+
const factory = module.get(client_1.TypedClientFactory);
|
|
47
|
+
expect(factory).toBeInstanceOf(client_1.TypedClientFactory);
|
|
48
|
+
});
|
|
49
|
+
it('resolves TNEST_OPTIONS from factory', async () => {
|
|
50
|
+
const options = { clients: [] };
|
|
51
|
+
const module = await testing_1.Test.createTestingModule({
|
|
52
|
+
imports: [
|
|
53
|
+
tnest_module_1.TnestModule.forRootAsync({
|
|
54
|
+
useFactory: () => options,
|
|
55
|
+
}),
|
|
56
|
+
],
|
|
57
|
+
}).compile();
|
|
58
|
+
const resolved = module.get(constants_1.TNEST_OPTIONS);
|
|
59
|
+
expect(resolved).toBe(options);
|
|
60
|
+
});
|
|
61
|
+
it('registers named client tokens when clientNames are provided', async () => {
|
|
62
|
+
const module = await testing_1.Test.createTestingModule({
|
|
63
|
+
imports: [
|
|
64
|
+
tnest_module_1.TnestModule.forRootAsync({
|
|
65
|
+
clientNames: ['USER_SERVICE'],
|
|
66
|
+
useFactory: () => ({
|
|
67
|
+
clients: [{ name: 'USER_SERVICE', options: { transport: 0 } }],
|
|
68
|
+
}),
|
|
69
|
+
}),
|
|
70
|
+
],
|
|
71
|
+
}).compile();
|
|
72
|
+
const token = (0, constants_1.getClientToken)('USER_SERVICE');
|
|
73
|
+
const client = module.get(token);
|
|
74
|
+
expect(client).toBeDefined();
|
|
75
|
+
});
|
|
76
|
+
it('throws when clientName is not found in resolved options', async () => {
|
|
77
|
+
await expect(testing_1.Test.createTestingModule({
|
|
78
|
+
imports: [
|
|
79
|
+
tnest_module_1.TnestModule.forRootAsync({
|
|
80
|
+
clientNames: ['MISSING_SERVICE'],
|
|
81
|
+
useFactory: () => ({ clients: [] }),
|
|
82
|
+
}),
|
|
83
|
+
],
|
|
84
|
+
}).compile()).rejects.toThrow(/client "MISSING_SERVICE" was declared in clientNames but not found/);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
//# sourceMappingURL=tnest-module.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tnest-module.spec.js","sourceRoot":"","sources":["../../src/__tests__/tnest-module.spec.ts"],"names":[],"mappings":";;AAAA,6CAAuC;AACvC,kDAA8C;AAC9C,sCAA+C;AAC/C,4CAA6D;AAG7D,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;QACzB,EAAE,CAAC,6BAA6B,EAAE,KAAK,IAAI,EAAE;YAC3C,MAAM,MAAM,GAAG,MAAM,cAAI,CAAC,mBAAmB,CAAC;gBAC5C,OAAO,EAAE,CAAC,0BAAW,CAAC,OAAO,EAAE,CAAC;aACjC,CAAC,CAAC,OAAO,EAAE,CAAC;YAEb,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,2BAAkB,CAAC,CAAC;YAC/C,MAAM,CAAC,OAAO,CAAC,CAAC,cAAc,CAAC,2BAAkB,CAAC,CAAC;QACrD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wBAAwB,EAAE,KAAK,IAAI,EAAE;YACtC,MAAM,OAAO,GAAuB,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;YACpD,MAAM,MAAM,GAAG,MAAM,cAAI,CAAC,mBAAmB,CAAC;gBAC5C,OAAO,EAAE,CAAC,0BAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;aACxC,CAAC,CAAC,OAAO,EAAE,CAAC;YAEb,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAqB,yBAAa,CAAC,CAAC;YAC/D,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;YACrE,MAAM,MAAM,GAAG,MAAM,cAAI,CAAC,mBAAmB,CAAC;gBAC5C,OAAO,EAAE;oBACP,0BAAW,CAAC,OAAO,CAAC;wBAClB,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC;qBAC/D,CAAC;iBACH;aACF,CAAC,CAAC,OAAO,EAAE,CAAC;YAEb,MAAM,KAAK,GAAG,IAAA,0BAAc,EAAC,cAAc,CAAC,CAAC;YAC7C,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAU,KAAK,CAAC,CAAC;YAC1C,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;QAC/B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC9B,EAAE,CAAC,6BAA6B,EAAE,KAAK,IAAI,EAAE;YAC3C,MAAM,MAAM,GAAG,MAAM,cAAI,CAAC,mBAAmB,CAAC;gBAC5C,OAAO,EAAE;oBACP,0BAAW,CAAC,YAAY,CAAC;wBACvB,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;qBACpC,CAAC;iBACH;aACF,CAAC,CAAC,OAAO,EAAE,CAAC;YAEb,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,2BAAkB,CAAC,CAAC;YAC/C,MAAM,CAAC,OAAO,CAAC,CAAC,cAAc,CAAC,2BAAkB,CAAC,CAAC;QACrD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;YACnD,MAAM,OAAO,GAAuB,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;YACpD,MAAM,MAAM,GAAG,MAAM,cAAI,CAAC,mBAAmB,CAAC;gBAC5C,OAAO,EAAE;oBACP,0BAAW,CAAC,YAAY,CAAC;wBACvB,UAAU,EAAE,GAAG,EAAE,CAAC,OAAO;qBAC1B,CAAC;iBACH;aACF,CAAC,CAAC,OAAO,EAAE,CAAC;YAEb,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAqB,yBAAa,CAAC,CAAC;YAC/D,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;YAC3E,MAAM,MAAM,GAAG,MAAM,cAAI,CAAC,mBAAmB,CAAC;gBAC5C,OAAO,EAAE;oBACP,0BAAW,CAAC,YAAY,CAAC;wBACvB,WAAW,EAAE,CAAC,cAAc,CAAC;wBAC7B,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC;4BACjB,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC;yBAC/D,CAAC;qBACH,CAAC;iBACH;aACF,CAAC,CAAC,OAAO,EAAE,CAAC;YAEb,MAAM,KAAK,GAAG,IAAA,0BAAc,EAAC,cAAc,CAAC,CAAC;YAC7C,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAU,KAAK,CAAC,CAAC;YAC1C,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;QAC/B,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,yDAAyD,EAAE,KAAK,IAAI,EAAE;YACvE,MAAM,MAAM,CACV,cAAI,CAAC,mBAAmB,CAAC;gBACvB,OAAO,EAAE;oBACP,0BAAW,CAAC,YAAY,CAAC;wBACvB,WAAW,EAAE,CAAC,iBAAiB,CAAC;wBAChC,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;qBACpC,CAAC;iBACH;aACF,CAAC,CAAC,OAAO,EAAE,CACb,CAAC,OAAO,CAAC,OAAO,CAAC,oEAAoE,CAAC,CAAC;QAC1F,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|