@crossdelta/platform-sdk 0.10.1 → 0.11.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.
@@ -1,52 +1,122 @@
1
- # NestJS Service - AI Generator Guidelines
1
+ # NestJS Service
2
2
 
3
- These guidelines are for generating NestJS microservices.
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: Commands Block (REQUIRED - MUST BE FIRST IN YOUR RESPONSE)
7
+ ## 🚨 CRITICAL: Use NestJS Built-in Features
10
8
 
11
- Your response **MUST** start with a commands block that scaffolds the NestJS service structure:
9
+ **NEVER create manual implementations when NestJS provides the feature:**
12
10
 
13
- ```commands
14
- pf new nest-micro <service-path> -y
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
- **Example:** If user asks for "orders service", generate:
18
- ```commands
19
- pf new nest-micro services/orders -y
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
- This command creates: `package.json`, `tsconfig.json`, NestJS structure, and basic files.
23
- **Without this command, the service cannot be built or run!**
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
- - Services MUST be in `services/` directory
26
- - User says "orders" → Generate path: `services/orders`
27
- - Always include `-y` flag to skip prompts
56
+ doSomething() {
57
+ const instanceId = this.config.getOrThrow<string>('PUSHER_INSTANCE_ID')
58
+ }
59
+ }
60
+ ```
28
61
 
29
62
  ---
30
63
 
31
- ## NestJS Event Consumer Architecture
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
- For NestJS event consumers, we use NestJS's application context to bridge
34
- `@crossdelta/cloudevents` handlers with NestJS's dependency injection system.
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
- ### Entry Point (src/main.ts)
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
- // Get EventsService from DI container and start consuming
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 { consumeJetStreamStreams, ensureJetStreamStreams } from '@crossdelta/cloudevents'
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 ensureJetStreamStreams({
89
- streams: [
90
- { stream: 'ORDERS', subjects: ['orders.*'] },
91
- ],
158
+ await ensureJetStreams({
159
+ streams: [{ stream: 'ORDERS', subjects: ['orders.*'] }],
92
160
  })
93
161
 
94
- consumeJetStreamStreams({
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
- ## Accessing NestJS Services in Handlers
173
+ ## App Context (src/app.context.ts)
106
174
 
107
- For handlers that need NestJS DI (database, external services), use the app context:
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
- ### Updated main.ts with App Context
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
- bootstrap()
156
- ```
198
+ ## Event Handler (src/events/orders-created.event.ts)
157
199
 
158
- ### Handler with DI Access (src/events/orders-created.event.ts)
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
- // Get service from NestJS DI container
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
- ### Orders Service (src/orders/orders.service.ts)
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 Structure
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
- ### Unit Test for Service
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/orders/orders.service.spec.ts
229
- import { Test, TestingModule } from '@nestjs/testing'
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
- beforeEach(async () => {
236
- const module: TestingModule = await Test.createTestingModule({
237
- providers: [OrdersService],
238
- }).compile()
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
- await expect(service.processOrder(mockData)).resolves.not.toThrow()
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
- ## Dev Command
293
+ ## 🚨 Use NestJS Best Practices
258
294
 
259
- ```json
260
- {
261
- "scripts": {
262
- "start:dev": "nest start --watch"
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