Skip to content

Commit 8c4d56a

Browse files
Copilotbtravers
andauthored
feat(client-nestjs): add NestJS integration package for temporal-contract client (#83)
* Initial plan * feat(client-nestjs): add NestJS integration package for temporal-contract client Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> * fix(client-nestjs): update to use changesets and NestJS ^11 peer dependencies Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> * style(changeset): format config with proper line breaks Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> * fix(changeset): format config with multiline arrays and exclude from oxfmt Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> * fix(client-nestjs): update version to 0.0.7, fix knip config, and update docs to use Vitest syntax Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> * refactor(client-nestjs): merge multiple assertions into single toMatchObject calls Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> * fix(changeset): merge NestJS packages into main fixed group for unified versioning Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> * fix(oxfmt): revert changeset ignore pattern as lefthook already excludes it Co-authored-by: btravers <3476441+btravers@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: btravers <3476441+btravers@users.noreply.github.com>
1 parent 592a892 commit 8c4d56a

18 files changed

Lines changed: 1892 additions & 4 deletions

.changeset/config.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"@temporal-contract/client",
1010
"@temporal-contract/testing",
1111
"@temporal-contract/boxed",
12-
"@temporal-contract/tsconfig"
12+
"@temporal-contract/tsconfig",
13+
"@temporal-contract/worker-nestjs",
14+
"@temporal-contract/client-nestjs"
1315
]
1416
],
1517
"linked": [],

knip.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@
2323
"entry": ["src/activity.ts", "src/workflow.ts", "src/__tests__/test.workflows.ts"],
2424
"project": ["src/**/*.ts"]
2525
},
26+
"packages/worker-nestjs": {
27+
"entry": ["src/index.ts"],
28+
"project": ["src/**/*.ts"]
29+
},
30+
"packages/client-nestjs": {
31+
"entry": ["src/index.ts"],
32+
"project": ["src/**/*.ts"]
33+
},
2634
"packages/testing": {
2735
"entry": ["src/extension.ts", "src/global-setup.ts"],
2836
"project": ["src/**/*.ts"]

lefthook.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pre-commit:
33
commands:
44
format:
55
glob: "*.{ts,tsx,js,jsx,json,yaml,yml,md}"
6-
exclude: "*/package.json,pnpm-lock.yaml"
6+
exclude: "*/package.json,pnpm-lock.yaml,.changeset/**"
77
run: pnpm oxfmt {staged_files}
88
stage_fixed: true
99
lint:

packages/client-nestjs/README.md

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
{
2+
"name": "@temporal-contract/client-nestjs",
3+
"version": "0.0.7",
4+
"description": "NestJS integration for temporal-contract client",
5+
"keywords": [
6+
"client",
7+
"contract",
8+
"nestjs",
9+
"temporal",
10+
"typescript"
11+
],
12+
"homepage": "https://github.com/btravers/temporal-contract#readme",
13+
"bugs": {
14+
"url": "https://github.com/btravers/temporal-contract/issues"
15+
},
16+
"repository": {
17+
"type": "git",
18+
"url": "https://github.com/btravers/temporal-contract.git",
19+
"directory": "packages/client-nestjs"
20+
},
21+
"license": "MIT",
22+
"author": "Benoit TRAVERS <benoit.travers.fr@gmail.com>",
23+
"type": "module",
24+
"exports": {
25+
".": {
26+
"import": {
27+
"types": "./dist/index.d.mts",
28+
"default": "./dist/index.mjs"
29+
},
30+
"require": {
31+
"types": "./dist/index.d.cts",
32+
"default": "./dist/index.cjs"
33+
}
34+
},
35+
"./package.json": "./package.json"
36+
},
37+
"main": "./dist/index.cjs",
38+
"module": "./dist/index.mjs",
39+
"types": "./dist/index.d.mts",
40+
"files": [
41+
"dist"
42+
],
43+
"scripts": {
44+
"build": "tsdown src/index.ts --format cjs,esm --dts --clean",
45+
"dev": "tsdown src/index.ts --format cjs,esm --dts --watch",
46+
"test": "vitest run",
47+
"test:watch": "vitest",
48+
"typecheck": "tsc --noEmit"
49+
},
50+
"dependencies": {
51+
"@temporal-contract/client": "workspace:*",
52+
"@temporal-contract/contract": "workspace:*"
53+
},
54+
"devDependencies": {
55+
"@nestjs/common": "catalog:",
56+
"@nestjs/core": "catalog:",
57+
"@nestjs/testing": "catalog:",
58+
"@temporal-contract/tsconfig": "workspace:*",
59+
"@temporalio/client": "catalog:",
60+
"@types/node": "catalog:",
61+
"@vitest/coverage-v8": "catalog:",
62+
"reflect-metadata": "catalog:",
63+
"tsdown": "catalog:",
64+
"typescript": "catalog:",
65+
"vitest": "catalog:",
66+
"zod": "catalog:"
67+
},
68+
"peerDependencies": {
69+
"@nestjs/common": "^11",
70+
"@nestjs/core": "^11",
71+
"@temporalio/client": "^1",
72+
"reflect-metadata": "^0.2.0"
73+
}
74+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @temporal-contract/client-nestjs
3+
*
4+
* NestJS integration for temporal-contract client
5+
*/
6+
7+
export { TemporalClientModule } from "./temporal-client.module.js";
8+
export { TemporalClientService } from "./temporal-client.service.js";
9+
export type { TemporalClientModuleOptions } from "./interfaces.js";
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { ContractDefinition } from "@temporal-contract/contract";
2+
import type { Client } from "@temporalio/client";
3+
4+
/**
5+
* Options for configuring the Temporal client module
6+
*/
7+
export interface TemporalClientModuleOptions<
8+
TContract extends ContractDefinition = ContractDefinition,
9+
> {
10+
/**
11+
* The contract definition for this client
12+
*/
13+
contract: TContract;
14+
15+
/**
16+
* Temporal client instance or configuration to create one
17+
*/
18+
client: Client;
19+
}

0 commit comments

Comments
 (0)