|
| 1 | +# @temporal-contract/client-nestjs |
| 2 | + |
| 3 | +NestJS integration for `@temporal-contract/client` providing a type-safe way to consume Temporal workflows with full dependency injection support. |
| 4 | + |
| 5 | +## Installation |
| 6 | + |
| 7 | +```bash |
| 8 | +pnpm add @temporal-contract/client-nestjs @swan-io/boxed |
| 9 | +``` |
| 10 | + |
| 11 | +## Features |
| 12 | + |
| 13 | +- **ConfigurableModuleBuilder Integration**: Use NestJS's `ConfigurableModuleBuilder` for dynamic module configuration |
| 14 | +- **Type-Safe Client**: Full type safety for workflow execution through the contract system |
| 15 | +- **Full Dependency Injection**: Access the typed client from any NestJS service |
| 16 | +- **Contract Validation**: Automatic validation through the contract system |
| 17 | +- **Global Module**: Available in all modules without re-importing |
| 18 | + |
| 19 | +## Quick Example |
| 20 | + |
| 21 | +```typescript |
| 22 | +import { Module, Injectable } from '@nestjs/common'; |
| 23 | +import { TemporalClientModule, TemporalClientService } from '@temporal-contract/client-nestjs'; |
| 24 | +import { Connection, Client } from '@temporalio/client'; |
| 25 | +import { orderContract } from './contract'; |
| 26 | + |
| 27 | +@Module({ |
| 28 | + imports: [ |
| 29 | + TemporalClientModule.forRootAsync({ |
| 30 | + useFactory: async () => { |
| 31 | + const connection = await Connection.connect({ address: 'localhost:7233' }); |
| 32 | + return { |
| 33 | + contract: orderContract, |
| 34 | + client: new Client({ connection }), |
| 35 | + }; |
| 36 | + }, |
| 37 | + }), |
| 38 | + ], |
| 39 | + providers: [OrderService], |
| 40 | +}) |
| 41 | +export class AppModule {} |
| 42 | + |
| 43 | +@Injectable() |
| 44 | +export class OrderService { |
| 45 | + constructor(private readonly temporalClient: TemporalClientService) {} |
| 46 | + |
| 47 | + async createOrder(orderId: string, customerId: string) { |
| 48 | + const client = this.temporalClient.getClient(); |
| 49 | + |
| 50 | + const result = await client.executeWorkflow('processOrder', { |
| 51 | + workflowId: `order-${orderId}`, |
| 52 | + args: { orderId, customerId }, |
| 53 | + }); |
| 54 | + |
| 55 | + return result.match({ |
| 56 | + Ok: (value) => value, |
| 57 | + Error: (error) => { |
| 58 | + throw new Error(`Order processing failed: ${error.message}`); |
| 59 | + }, |
| 60 | + }); |
| 61 | + } |
| 62 | +} |
| 63 | +``` |
| 64 | + |
| 65 | +## Usage |
| 66 | + |
| 67 | +### Synchronous Configuration |
| 68 | + |
| 69 | +Use `forRoot` for simple, synchronous configuration: |
| 70 | + |
| 71 | +```typescript |
| 72 | +import { Module } from '@nestjs/common'; |
| 73 | +import { TemporalClientModule } from '@temporal-contract/client-nestjs'; |
| 74 | +import { Connection, Client } from '@temporalio/client'; |
| 75 | +import { myContract } from './contract'; |
| 76 | + |
| 77 | +const connection = await Connection.connect({ address: 'localhost:7233' }); |
| 78 | +const client = new Client({ connection }); |
| 79 | + |
| 80 | +@Module({ |
| 81 | + imports: [ |
| 82 | + TemporalClientModule.forRoot({ |
| 83 | + contract: myContract, |
| 84 | + client: client, |
| 85 | + }), |
| 86 | + ], |
| 87 | +}) |
| 88 | +export class AppModule {} |
| 89 | +``` |
| 90 | + |
| 91 | +### Asynchronous Configuration |
| 92 | + |
| 93 | +Use `forRootAsync` for configuration that requires async setup or DI: |
| 94 | + |
| 95 | +```typescript |
| 96 | +import { Module } from '@nestjs/common'; |
| 97 | +import { ConfigModule, ConfigService } from '@nestjs/config'; |
| 98 | +import { TemporalClientModule } from '@temporal-contract/client-nestjs'; |
| 99 | +import { Connection, Client } from '@temporalio/client'; |
| 100 | +import { myContract } from './contract'; |
| 101 | + |
| 102 | +@Module({ |
| 103 | + imports: [ |
| 104 | + ConfigModule.forRoot(), |
| 105 | + TemporalClientModule.forRootAsync({ |
| 106 | + imports: [ConfigModule], |
| 107 | + inject: [ConfigService], |
| 108 | + useFactory: async (config: ConfigService) => { |
| 109 | + const connection = await Connection.connect({ |
| 110 | + address: config.get('TEMPORAL_ADDRESS'), |
| 111 | + }); |
| 112 | + return { |
| 113 | + contract: myContract, |
| 114 | + client: new Client({ |
| 115 | + connection, |
| 116 | + namespace: config.get('TEMPORAL_NAMESPACE'), |
| 117 | + }), |
| 118 | + }; |
| 119 | + }, |
| 120 | + }), |
| 121 | + ], |
| 122 | +}) |
| 123 | +export class AppModule {} |
| 124 | +``` |
| 125 | + |
| 126 | +### Using in Services |
| 127 | + |
| 128 | +Inject the `TemporalClientService` to access the typed client: |
| 129 | + |
| 130 | +```typescript |
| 131 | +import { Injectable } from '@nestjs/common'; |
| 132 | +import { TemporalClientService } from '@temporal-contract/client-nestjs'; |
| 133 | + |
| 134 | +@Injectable() |
| 135 | +export class OrderService { |
| 136 | + constructor(private readonly temporalClient: TemporalClientService) {} |
| 137 | + |
| 138 | + async processOrder(orderId: string, customerId: string) { |
| 139 | + const client = this.temporalClient.getClient(); |
| 140 | + |
| 141 | + const result = await client.executeWorkflow('processOrder', { |
| 142 | + workflowId: `order-${orderId}`, |
| 143 | + args: { orderId, customerId }, |
| 144 | + }); |
| 145 | + |
| 146 | + return result.match({ |
| 147 | + Ok: (value) => ({ success: true, data: value }), |
| 148 | + Error: (error) => ({ success: false, error: error.message }), |
| 149 | + }); |
| 150 | + } |
| 151 | + |
| 152 | + async getOrderStatus(workflowId: string) { |
| 153 | + const client = this.temporalClient.getClient(); |
| 154 | + |
| 155 | + const handleResult = await client.getHandle('processOrder', workflowId); |
| 156 | + |
| 157 | + return handleResult.match({ |
| 158 | + Ok: async (handle) => { |
| 159 | + const queryResult = await handle.queries.getStatus({}); |
| 160 | + return queryResult.match({ |
| 161 | + Ok: (status) => status, |
| 162 | + Error: (error) => { |
| 163 | + throw new Error(`Query failed: ${error.message}`); |
| 164 | + }, |
| 165 | + }); |
| 166 | + }, |
| 167 | + Error: (error) => { |
| 168 | + throw new Error(`Failed to get handle: ${error.message}`); |
| 169 | + }, |
| 170 | + }); |
| 171 | + } |
| 172 | +} |
| 173 | +``` |
| 174 | + |
| 175 | +## Multiple Clients |
| 176 | + |
| 177 | +You can configure multiple clients for different contracts: |
| 178 | + |
| 179 | +```typescript |
| 180 | +@Module({ |
| 181 | + imports: [ |
| 182 | + TemporalClientModule.forRootAsync({ |
| 183 | + useFactory: async () => { |
| 184 | + const connection = await Connection.connect({ address: 'localhost:7233' }); |
| 185 | + return { |
| 186 | + contract: orderContract, |
| 187 | + client: new Client({ connection }), |
| 188 | + }; |
| 189 | + }, |
| 190 | + }), |
| 191 | + // For multiple contracts, create separate modules or use providers |
| 192 | + ], |
| 193 | +}) |
| 194 | +export class AppModule {} |
| 195 | +``` |
| 196 | + |
| 197 | +## Testing |
| 198 | + |
| 199 | +Mock the `TemporalClientService` in your tests: |
| 200 | + |
| 201 | +```typescript |
| 202 | +import { Test } from '@nestjs/testing'; |
| 203 | +import { TemporalClientService } from '@temporal-contract/client-nestjs'; |
| 204 | +import { Result } from '@swan-io/boxed'; |
| 205 | + |
| 206 | +describe('OrderService', () => { |
| 207 | + let service: OrderService; |
| 208 | + let mockTemporalClient: TemporalClientService; |
| 209 | + |
| 210 | + beforeEach(async () => { |
| 211 | + const mockClient = { |
| 212 | + executeWorkflow: vi.fn().mockResolvedValue( |
| 213 | + Result.Ok({ status: 'success', transactionId: 'tx-123' }) |
| 214 | + ), |
| 215 | + }; |
| 216 | + |
| 217 | + mockTemporalClient = { |
| 218 | + getClient: () => mockClient, |
| 219 | + } as any; |
| 220 | + |
| 221 | + const module = await Test.createTestingModule({ |
| 222 | + providers: [ |
| 223 | + OrderService, |
| 224 | + { |
| 225 | + provide: TemporalClientService, |
| 226 | + useValue: mockTemporalClient, |
| 227 | + }, |
| 228 | + ], |
| 229 | + }).compile(); |
| 230 | + |
| 231 | + service = module.get<OrderService>(OrderService); |
| 232 | + }); |
| 233 | + |
| 234 | + it('should process order', async () => { |
| 235 | + const result = await service.processOrder('ORD-123', 'CUST-456'); |
| 236 | + expect(result.success).toBe(true); |
| 237 | + }); |
| 238 | +}); |
| 239 | +``` |
| 240 | + |
| 241 | +## API Reference |
| 242 | + |
| 243 | +See the [API documentation](/api/client-nestjs) for detailed information. |
| 244 | + |
| 245 | +## See Also |
| 246 | + |
| 247 | +- [Client Usage Guide](/guide/client-usage) - General client usage |
| 248 | +- [NestJS Client Usage Guide](/guide/client-nestjs-usage) - Detailed NestJS integration |
| 249 | +- [@temporal-contract/client](/api/client) - Core client package |
| 250 | +- [@temporal-contract/worker-nestjs](/api/worker-nestjs) - NestJS worker integration |
| 251 | + |
| 252 | +## License |
| 253 | + |
| 254 | +MIT |
0 commit comments