@crossdelta/platform-sdk 0.10.1 → 0.11.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/README.md +40 -3
- package/bin/cli.js +109 -109
- package/bin/docs/generators/README.md +29 -20
- package/bin/docs/generators/code-style.md +96 -0
- package/bin/docs/generators/hono-bun.md +31 -45
- package/bin/docs/generators/hono-node.md +33 -57
- package/bin/docs/generators/nest.md +169 -107
- package/bin/docs/generators/service.md +133 -528
- package/bin/docs/generators/testing.md +97 -0
- package/bin/templates/nest-microservice/biome.json.hbs +1 -8
- package/bin/templates/workspace/biome.json.hbs +2 -1
- package/bin/templates/workspace/packages/contracts/package.json.hbs +1 -1
- package/package.json +128 -118
- package/bin/docs/generators/nest.md.new +0 -351
|
@@ -1,52 +1,122 @@
|
|
|
1
|
-
# NestJS Service
|
|
1
|
+
# NestJS Service
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
**For handler location and structure rules (shared for all frameworks), see:** [Service Generator - Handler Location & Structure](./service.md#handler-location--structure-critical)
|
|
3
|
+
**For handler location and structure rules, see:** [service.md](./service.md)
|
|
6
4
|
|
|
7
5
|
---
|
|
8
6
|
|
|
9
|
-
## 🚨 CRITICAL:
|
|
7
|
+
## 🚨 CRITICAL: Use NestJS Built-in Features
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
**NEVER create manual implementations when NestJS provides the feature:**
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
| ❌ WRONG (Manual) | ✅ CORRECT (NestJS) |
|
|
12
|
+
|-------------------|---------------------|
|
|
13
|
+
| `validateConfig()` function | `ConfigModule` + Zod |
|
|
14
|
+
| `process.env.X` direct access | `ConfigService.get()` |
|
|
15
|
+
| `console.log()` | `Logger` from `@nestjs/common` |
|
|
16
|
+
| Manual error throwing | `HttpException` classes |
|
|
17
|
+
| Custom auth checks | `@UseGuards()` decorator |
|
|
18
|
+
|
|
19
|
+
**Env validation with `@nestjs/config` + Zod:**
|
|
20
|
+
```ts
|
|
21
|
+
// src/config/env.schema.ts
|
|
22
|
+
import { z } from 'zod'
|
|
23
|
+
|
|
24
|
+
export const envSchema = z.object({
|
|
25
|
+
PUSHER_INSTANCE_ID: z.string().min(1),
|
|
26
|
+
PUSHER_SECRET_KEY: z.string().min(1),
|
|
27
|
+
})
|
|
15
28
|
```
|
|
16
29
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
30
|
+
```ts
|
|
31
|
+
// src/app.module.ts
|
|
32
|
+
import { Module } from '@nestjs/common'
|
|
33
|
+
import { ConfigModule } from '@nestjs/config'
|
|
34
|
+
import { envSchema } from './config/env.schema'
|
|
35
|
+
|
|
36
|
+
@Module({
|
|
37
|
+
imports: [
|
|
38
|
+
ConfigModule.forRoot({
|
|
39
|
+
validate: (config) => envSchema.parse(config),
|
|
40
|
+
isGlobal: true,
|
|
41
|
+
}),
|
|
42
|
+
],
|
|
43
|
+
})
|
|
44
|
+
export class AppModule {}
|
|
20
45
|
```
|
|
21
46
|
|
|
22
|
-
|
|
23
|
-
|
|
47
|
+
```ts
|
|
48
|
+
// ✅ Service uses ConfigService - already validated at startup
|
|
49
|
+
import { Injectable } from '@nestjs/common'
|
|
50
|
+
import { ConfigService } from '@nestjs/config'
|
|
51
|
+
|
|
52
|
+
@Injectable()
|
|
53
|
+
export class MyService {
|
|
54
|
+
constructor(private config: ConfigService) {}
|
|
24
55
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
56
|
+
doSomething() {
|
|
57
|
+
const instanceId = this.config.getOrThrow<string>('PUSHER_INSTANCE_ID')
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
28
61
|
|
|
29
62
|
---
|
|
30
63
|
|
|
31
|
-
## NestJS
|
|
64
|
+
## 🚨 CRITICAL: NestJS Structure (NO use-cases folder!)
|
|
65
|
+
|
|
66
|
+
NestJS has Services - they ARE the use-cases. Don't create a separate `use-cases/` folder:
|
|
32
67
|
|
|
33
|
-
|
|
34
|
-
|
|
68
|
+
```
|
|
69
|
+
✅ CORRECT NestJS structure:
|
|
70
|
+
src/
|
|
71
|
+
├── main.ts
|
|
72
|
+
├── app.module.ts
|
|
73
|
+
├── app.context.ts
|
|
74
|
+
├── events/
|
|
75
|
+
│ ├── events.module.ts
|
|
76
|
+
│ ├── events.service.ts
|
|
77
|
+
│ └── domain-created.event.ts # Handler → calls service
|
|
78
|
+
└── notifications/
|
|
79
|
+
├── notifications.module.ts
|
|
80
|
+
├── notifications.service.ts # Business logic HERE
|
|
81
|
+
└── send-notification.ts # Pure helper functions (testable)
|
|
82
|
+
└── send-notification.test.ts # Tests for pure functions
|
|
83
|
+
|
|
84
|
+
❌ WRONG - don't create use-cases folder in NestJS:
|
|
85
|
+
src/
|
|
86
|
+
├── use-cases/ # NOT needed in NestJS!
|
|
87
|
+
│ └── send-notification.use-case.ts
|
|
88
|
+
```
|
|
35
89
|
|
|
36
|
-
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 🚨 CRITICAL: Commands Block (REQUIRED FIRST)
|
|
93
|
+
|
|
94
|
+
```commands
|
|
95
|
+
pf new nest-micro services/<service-name> -y
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Example:** User asks for "orders service" → generate:
|
|
99
|
+
```commands
|
|
100
|
+
pf new nest-micro services/orders -y
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Entry Point (src/main.ts)
|
|
37
106
|
|
|
38
107
|
```ts
|
|
39
108
|
import '@crossdelta/telemetry'
|
|
40
|
-
|
|
41
109
|
import { NestFactory } from '@nestjs/core'
|
|
42
110
|
import { AppModule } from './app.module'
|
|
111
|
+
import { setAppContext } from './app.context'
|
|
43
112
|
import { EventsService } from './events/events.service'
|
|
44
113
|
|
|
45
114
|
async function bootstrap() {
|
|
46
115
|
const app = await NestFactory.create(AppModule)
|
|
47
116
|
const port = Number(process.env.MY_SERVICE_PORT) || 8080
|
|
48
117
|
|
|
49
|
-
|
|
118
|
+
setAppContext(app)
|
|
119
|
+
|
|
50
120
|
const eventsService = app.get(EventsService)
|
|
51
121
|
await eventsService.startConsumers()
|
|
52
122
|
|
|
@@ -78,20 +148,18 @@ export class EventsModule {}
|
|
|
78
148
|
|
|
79
149
|
```ts
|
|
80
150
|
import { Injectable, Logger } from '@nestjs/common'
|
|
81
|
-
import {
|
|
151
|
+
import { consumeJetStreams, ensureJetStreams } from '@crossdelta/cloudevents'
|
|
82
152
|
|
|
83
153
|
@Injectable()
|
|
84
154
|
export class EventsService {
|
|
85
155
|
private readonly logger = new Logger(EventsService.name)
|
|
86
156
|
|
|
87
157
|
async startConsumers(): Promise<void> {
|
|
88
|
-
await
|
|
89
|
-
streams: [
|
|
90
|
-
{ stream: 'ORDERS', subjects: ['orders.*'] },
|
|
91
|
-
],
|
|
158
|
+
await ensureJetStreams({
|
|
159
|
+
streams: [{ stream: 'ORDERS', subjects: ['orders.*'] }],
|
|
92
160
|
})
|
|
93
161
|
|
|
94
|
-
|
|
162
|
+
consumeJetStreams({
|
|
95
163
|
streams: ['ORDERS'],
|
|
96
164
|
consumer: 'my-service',
|
|
97
165
|
discover: './src/events/**/*.event.ts',
|
|
@@ -102,11 +170,9 @@ export class EventsService {
|
|
|
102
170
|
|
|
103
171
|
---
|
|
104
172
|
|
|
105
|
-
##
|
|
173
|
+
## App Context (src/app.context.ts)
|
|
106
174
|
|
|
107
|
-
For handlers
|
|
108
|
-
|
|
109
|
-
### App Context Singleton (src/app.context.ts)
|
|
175
|
+
For handlers to access NestJS DI:
|
|
110
176
|
|
|
111
177
|
```ts
|
|
112
178
|
import type { INestApplication, Type } from '@nestjs/common'
|
|
@@ -127,35 +193,11 @@ export const getService = <T>(serviceClass: Type<T>): T => {
|
|
|
127
193
|
}
|
|
128
194
|
```
|
|
129
195
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
```ts
|
|
133
|
-
import '@crossdelta/telemetry'
|
|
134
|
-
|
|
135
|
-
import { NestFactory } from '@nestjs/core'
|
|
136
|
-
import { AppModule } from './app.module'
|
|
137
|
-
import { setAppContext } from './app.context'
|
|
138
|
-
import { EventsService } from './events/events.service'
|
|
139
|
-
|
|
140
|
-
async function bootstrap() {
|
|
141
|
-
const app = await NestFactory.create(AppModule)
|
|
142
|
-
const port = Number(process.env.MY_SERVICE_PORT) || 8080
|
|
143
|
-
|
|
144
|
-
// Make app context available to event handlers
|
|
145
|
-
setAppContext(app)
|
|
146
|
-
|
|
147
|
-
// Start event consumers
|
|
148
|
-
const eventsService = app.get(EventsService)
|
|
149
|
-
await eventsService.startConsumers()
|
|
150
|
-
|
|
151
|
-
await app.listen(port)
|
|
152
|
-
console.log(`Service running on http://localhost:${port}`)
|
|
153
|
-
}
|
|
196
|
+
---
|
|
154
197
|
|
|
155
|
-
|
|
156
|
-
```
|
|
198
|
+
## Event Handler (src/events/orders-created.event.ts)
|
|
157
199
|
|
|
158
|
-
|
|
200
|
+
**Handlers must be thin** - just log and delegate to a service:
|
|
159
201
|
|
|
160
202
|
```ts
|
|
161
203
|
import { handleEvent } from '@crossdelta/cloudevents'
|
|
@@ -164,35 +206,19 @@ import { getService } from '../app.context'
|
|
|
164
206
|
import { OrdersService } from '../orders/orders.service'
|
|
165
207
|
|
|
166
208
|
export default handleEvent(OrdersCreatedContract, async (data: OrdersCreatedData) => {
|
|
167
|
-
|
|
209
|
+
console.log(`[orders.created] Processing orderId=${data.orderId}`)
|
|
168
210
|
const ordersService = getService(OrdersService)
|
|
169
211
|
await ordersService.processOrder(data)
|
|
170
212
|
})
|
|
171
213
|
```
|
|
172
214
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
```ts
|
|
176
|
-
import { Injectable, Logger } from '@nestjs/common'
|
|
177
|
-
import type { OrdersCreatedData } from '{{scope}}/contracts'
|
|
178
|
-
|
|
179
|
-
@Injectable()
|
|
180
|
-
export class OrdersService {
|
|
181
|
-
private readonly logger = new Logger(OrdersService.name)
|
|
182
|
-
|
|
183
|
-
async processOrder(data: OrdersCreatedData): Promise<void> {
|
|
184
|
-
this.logger.log(`Processing order: ${data.orderId}`)
|
|
185
|
-
// Business logic with full DI access
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
```
|
|
215
|
+
**⚠️ CRITICAL:** Use `console.log` in handlers for logging. Do NOT use `getService(Logger)` - Logger is not injectable via app context!
|
|
189
216
|
|
|
190
217
|
---
|
|
191
218
|
|
|
192
|
-
## App Module
|
|
219
|
+
## App Module (src/app.module.ts)
|
|
193
220
|
|
|
194
221
|
```ts
|
|
195
|
-
// src/app.module.ts
|
|
196
222
|
import { Module } from '@nestjs/common'
|
|
197
223
|
import { AppController } from './app.controller'
|
|
198
224
|
import { EventsModule } from './events/events.module'
|
|
@@ -205,8 +231,11 @@ import { OrdersModule } from './orders/orders.module'
|
|
|
205
231
|
export class AppModule {}
|
|
206
232
|
```
|
|
207
233
|
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## App Controller (src/app.controller.ts)
|
|
237
|
+
|
|
208
238
|
```ts
|
|
209
|
-
// src/app.controller.ts
|
|
210
239
|
import { Controller, Get } from '@nestjs/common'
|
|
211
240
|
|
|
212
241
|
@Controller()
|
|
@@ -222,44 +251,77 @@ export class AppController {
|
|
|
222
251
|
|
|
223
252
|
## Testing
|
|
224
253
|
|
|
225
|
-
|
|
254
|
+
**NestJS Services mit externen Dependencies (Pusher, DB, etc.):**
|
|
255
|
+
- Extrahiere Business-Logik in testbare Funktionen
|
|
256
|
+
- Service-Methoden sind thin wrappers
|
|
257
|
+
- Teste die extrahierte Logik mit Dependency Injection
|
|
226
258
|
|
|
227
259
|
```ts
|
|
228
|
-
// src/
|
|
229
|
-
import {
|
|
230
|
-
import { OrdersService } from './orders.service'
|
|
231
|
-
|
|
232
|
-
describe('OrdersService', () => {
|
|
233
|
-
let service: OrdersService
|
|
260
|
+
// src/notifications/send-notification.ts (pure function)
|
|
261
|
+
import type { DomainCreatedData } from '{{scope}}/contracts'
|
|
234
262
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
service = module.get<OrdersService>(OrdersService)
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
it('should process order', async () => {
|
|
244
|
-
const mockData = {
|
|
245
|
-
orderId: 'ord_123',
|
|
246
|
-
customerId: 'cust_456',
|
|
247
|
-
total: 99.99,
|
|
248
|
-
}
|
|
263
|
+
export const buildNotificationPayload = (data: DomainCreatedData) => ({
|
|
264
|
+
title: 'Domain Created',
|
|
265
|
+
body: `Domain ${data.domainId} was created`,
|
|
266
|
+
})
|
|
267
|
+
```
|
|
249
268
|
|
|
250
|
-
|
|
269
|
+
```ts
|
|
270
|
+
// src/notifications/send-notification.test.ts
|
|
271
|
+
import { describe, expect, it } from 'bun:test'
|
|
272
|
+
import { buildNotificationPayload } from './send-notification'
|
|
273
|
+
|
|
274
|
+
describe('buildNotificationPayload', () => {
|
|
275
|
+
it('builds payload with domain info', () => {
|
|
276
|
+
const payload = buildNotificationPayload({ domainId: 'dom_1', name: 'Test' })
|
|
277
|
+
expect(payload.title).toBe('Domain Created')
|
|
278
|
+
expect(payload.body).toContain('dom_1')
|
|
251
279
|
})
|
|
252
280
|
})
|
|
253
281
|
```
|
|
254
282
|
|
|
283
|
+
**❌ WRONG NestJS Testing patterns:**
|
|
284
|
+
```ts
|
|
285
|
+
// Don't do this:
|
|
286
|
+
beforeEach(() => { process.env.X = 'test' }) // fragile
|
|
287
|
+
// @ts-ignore // code smell
|
|
288
|
+
svc.beams = new FakeBeams() // mocking
|
|
289
|
+
```
|
|
290
|
+
|
|
255
291
|
---
|
|
256
292
|
|
|
257
|
-
##
|
|
293
|
+
## 🚨 Use NestJS Best Practices
|
|
258
294
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
295
|
+
**Always use NestJS built-in features instead of manual implementations:**
|
|
296
|
+
|
|
297
|
+
| ❌ Manual Implementation | ✅ NestJS Way |
|
|
298
|
+
|--------------------------|---------------|
|
|
299
|
+
| Manual config validation functions | `ConfigModule` with Zod validation |
|
|
300
|
+
| Manual environment parsing | `ConfigService` injection |
|
|
301
|
+
| Manual logging | `Logger` from `@nestjs/common` |
|
|
302
|
+
| Manual HTTP exceptions | `HttpException` classes |
|
|
303
|
+
| Manual guards/interceptors | NestJS decorators (`@UseGuards`, etc.) |
|
|
304
|
+
|
|
305
|
+
**Example - Config validation:**
|
|
306
|
+
```ts
|
|
307
|
+
// ❌ WRONG: Manual validator function
|
|
308
|
+
export const validateConfig = (c: { key?: string }) => {
|
|
309
|
+
if (!c.key) throw new Error('Missing KEY')
|
|
264
310
|
}
|
|
311
|
+
|
|
312
|
+
// ✅ CORRECT: ConfigModule with validation at startup
|
|
313
|
+
ConfigModule.forRoot({ validate: zodValidator, isGlobal: true })
|
|
314
|
+
// Then inject ConfigService in your service
|
|
265
315
|
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Rules
|
|
320
|
+
|
|
321
|
+
- ✅ Import telemetry FIRST in main.ts
|
|
322
|
+
- ✅ Use `setAppContext(app)` before starting consumers
|
|
323
|
+
- ✅ Handlers use `getService()` for DI access
|
|
324
|
+
- ✅ Always include `/health` endpoint (AppController)
|
|
325
|
+
- ✅ EventsModule only provides EventsService
|
|
326
|
+
- ❌ Don't import NestJS services directly in handlers
|
|
327
|
+
- ❌ Don't register handlers as module providers
|