@crossdelta/platform-sdk 0.9.4 → 0.10.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.
@@ -0,0 +1,351 @@
1
+ # NestJS Service - AI Generator Guidelines
2
+
3
+ These guidelines are for generating NestJS microservices.
4
+
5
+ ---
6
+
7
+ ## 🚨 CRITICAL: Commands Block (REQUIRED - MUST BE FIRST IN YOUR RESPONSE)
8
+
9
+ Your response **MUST** start with a commands block that scaffolds the NestJS service structure:
10
+
11
+ ```commands
12
+ pf new nest-micro <service-path> -y
13
+ ```
14
+
15
+ **Example:** If user asks for "orders service", generate:
16
+ ```commands
17
+ pf new nest-micro services/orders -y
18
+ ```
19
+
20
+ This command creates: `package.json`, `tsconfig.json`, NestJS structure, and basic files.
21
+ **Without this command, the service cannot be built or run!**
22
+
23
+ - Services MUST be in `services/` directory
24
+ - User says "orders" → Generate path: `services/orders`
25
+ - Always include `-y` flag to skip prompts
26
+
27
+ ---
28
+
29
+ ## NestJS Event Consumer Architecture
30
+
31
+ For NestJS event consumers, we use NestJS's application context to bridge
32
+ `@crossdelta/cloudevents` handlers with NestJS's dependency injection system.
33
+
34
+ ### Entry Point (src/main.ts)
35
+
36
+ ```ts
37
+ import '@crossdelta/telemetry'
38
+
39
+ import { NestFactory } from '@nestjs/core'
40
+ import { AppModule } from './app.module'
41
+ import { EventsService } from './events/events.service'
42
+
43
+ async function bootstrap() {
44
+ const app = await NestFactory.create(AppModule)
45
+ const port = Number(process.env.MY_SERVICE_PORT) || 8080
46
+
47
+ // Get EventsService from DI container and start consuming
48
+ const eventsService = app.get(EventsService)
49
+ await eventsService.startConsumers()
50
+
51
+ await app.listen(port)
52
+ console.log(`Service running on http://localhost:${port}`)
53
+ }
54
+
55
+ bootstrap()
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 🚨 CRITICAL: Events Module Pattern (MUST follow EXACTLY)
61
+
62
+ Create a dedicated Events module that manages NATS consumption with auto-discovery.
63
+
64
+ ### Events Module (src/events/events.module.ts) - MUST be like this!
65
+
66
+ ```ts
67
+ import { Module } from '@nestjs/common'
68
+ import { EventsService } from './events.service'
69
+
70
+ @Module({
71
+ providers: [EventsService],
72
+ exports: [EventsService],
73
+ })
74
+ export class EventsModule {}
75
+ ```
76
+
77
+ **⚠️ CRITICAL RULES:**
78
+ - ✅ MUST: Only provide `EventsService`
79
+ - ❌ DO NOT: Create `src/events/handlers/` subdirectory
80
+ - ❌ DO NOT: Import handlers from `handlers/` directory
81
+ - ❌ DO NOT: Register handlers as module providers
82
+ - Handlers are auto-discovered via glob pattern, NOT manually imported
83
+
84
+ ### Events Service (src/events/events.service.ts)
85
+
86
+ ```ts
87
+ import { Injectable, Logger } from '@nestjs/common'
88
+ import { consumeJetStreamStreams, ensureJetStreamStreams } from '@crossdelta/cloudevents'
89
+
90
+ @Injectable()
91
+ export class EventsService {
92
+ private readonly logger = new Logger(EventsService.name)
93
+
94
+ async startConsumers(): Promise<void> {
95
+ await ensureJetStreamStreams({
96
+ streams: [
97
+ { stream: 'ORDERS', subjects: ['orders.*'] },
98
+ ],
99
+ })
100
+
101
+ consumeJetStreamStreams({
102
+ streams: ['ORDERS'],
103
+ consumer: 'my-service',
104
+ discover: './src/events/**/*.event.ts', // ✅ CORRECT: Auto-discovers all handlers
105
+ })
106
+ }
107
+ }
108
+ ```
109
+
110
+ ### Event Handler (src/events/orders-created.event.ts) - MUST be here!
111
+
112
+ Event handlers in `src/events/*.event.ts` use `@crossdelta/cloudevents` for handling.
113
+ Auto-discovery finds them automatically - NO manual imports needed.
114
+
115
+ ```ts
116
+ import { handleEvent } from '@crossdelta/cloudevents'
117
+ import { OrdersCreatedContract, type OrdersCreatedData } from '{{scope}}/contracts'
118
+
119
+ // ✅ CORRECT: Default export, auto-discovered by cloudevents
120
+ export default handleEvent(OrdersCreatedContract, async (data: OrdersCreatedData) => {
121
+ console.log('📦 Processing order:', data.orderId)
122
+
123
+ // For simple handlers, process directly
124
+ // For complex logic with NestJS DI, see "Accessing NestJS Services" below
125
+ })
126
+ ```
127
+
128
+ **⚠️ CRITICAL RULES for Event Handlers:**
129
+ - ✅ MUST: Use `export default handleEvent(...)`
130
+ - ✅ MUST: Be in `src/events/*.event.ts` (e.g., `orders-created.event.ts`)
131
+ - ❌ DO NOT: Create `src/events/handlers/` subdirectory
132
+ - ❌ DO NOT: Use named exports like `export const OrdersCreatedHandler`
133
+ - ❌ DO NOT: Import handlers in events.module.ts
134
+ - ❌ DO NOT: Register handlers as providers
135
+ - Handlers are auto-discovered via `discover: './src/events/**/*.event.ts'`
136
+
137
+ ---
138
+
139
+ ## Accessing NestJS Services in Handlers
140
+
141
+ For handlers that need NestJS DI (database, external services), use the app context:
142
+
143
+ ### App Context Singleton (src/app.context.ts)
144
+
145
+ ```ts
146
+ import type { INestApplication, Type } from '@nestjs/common'
147
+
148
+ let app: INestApplication
149
+
150
+ export const setAppContext = (nestApp: INestApplication): void => {
151
+ app = nestApp
152
+ }
153
+
154
+ export const getAppContext = (): INestApplication => {
155
+ if (!app) throw new Error('App context not initialized')
156
+ return app
157
+ }
158
+
159
+ export const getService = <T>(serviceClass: Type<T>): T => {
160
+ return getAppContext().get(serviceClass)
161
+ }
162
+ ```
163
+
164
+ ### Updated main.ts
165
+
166
+ ```ts
167
+ import '@crossdelta/telemetry'
168
+
169
+ import { NestFactory } from '@nestjs/core'
170
+ import { AppModule } from './app.module'
171
+ import { setAppContext } from './app.context'
172
+ import { EventsService } from './events/events.service'
173
+
174
+ async function bootstrap() {
175
+ const app = await NestFactory.create(AppModule)
176
+ const port = Number(process.env.MY_SERVICE_PORT) || 8080
177
+
178
+ // Make app context available to event handlers
179
+ setAppContext(app)
180
+
181
+ // Start event consumers
182
+ const eventsService = app.get(EventsService)
183
+ await eventsService.startConsumers()
184
+
185
+ await app.listen(port)
186
+ console.log(`Service running on http://localhost:${port}`)
187
+ }
188
+
189
+ bootstrap()
190
+ ```
191
+
192
+ ### Handler with DI Access (src/events/orders-created.event.ts)
193
+
194
+ ```ts
195
+ import { handleEvent } from '@crossdelta/cloudevents'
196
+ import { OrdersCreatedContract, type OrdersCreatedData } from '{{scope}}/contracts'
197
+ import { getService } from '../app.context'
198
+ import { OrdersService } from '../orders/orders.service'
199
+
200
+ // ✅ CORRECT: Default export, auto-discovered
201
+ export default handleEvent(OrdersCreatedContract, async (data: OrdersCreatedData) => {
202
+ // Get service from NestJS DI container
203
+ const ordersService = getService(OrdersService)
204
+
205
+ await ordersService.processOrder(data)
206
+ })
207
+ ```
208
+
209
+ ### Orders Service (src/orders/orders.service.ts)
210
+
211
+ ```ts
212
+ import { Injectable, Logger } from '@nestjs/common'
213
+ import type { OrdersCreatedData } from '{{scope}}/contracts'
214
+
215
+ @Injectable()
216
+ export class OrdersService {
217
+ private readonly logger = new Logger(OrdersService.name)
218
+
219
+ async processOrder(data: OrdersCreatedData): Promise<void> {
220
+ this.logger.log(`Processing order: ${data.orderId}`)
221
+
222
+ // Business logic with full DI access
223
+ // Can inject repositories, HTTP services, etc.
224
+ }
225
+ }
226
+ ```
227
+
228
+ ---
229
+
230
+ ## App Module Structure
231
+
232
+ ```ts
233
+ // src/app.module.ts
234
+ import { Module } from '@nestjs/common'
235
+ import { AppController } from './app.controller'
236
+ import { EventsModule } from './events/events.module'
237
+ import { OrdersModule } from './orders/orders.module'
238
+
239
+ @Module({
240
+ imports: [EventsModule, OrdersModule],
241
+ controllers: [AppController],
242
+ })
243
+ export class AppModule {}
244
+ ```
245
+
246
+ ```ts
247
+ // src/app.controller.ts
248
+ import { Controller, Get } from '@nestjs/common'
249
+
250
+ @Controller()
251
+ export class AppController {
252
+ @Get('health')
253
+ health() {
254
+ return { status: 'ok' }
255
+ }
256
+ }
257
+ ```
258
+
259
+ ---
260
+
261
+ ## Required File Structure for Event Consumer
262
+
263
+ ```
264
+ ✅ CORRECT:
265
+ services/my-service/
266
+ ├── src/
267
+ │ ├── main.ts # Entry point
268
+ │ ├── app.module.ts # Root module
269
+ │ ├── app.controller.ts # Health endpoint
270
+ │ ├── app.context.ts # App context for DI bridge
271
+ │ ├── events/
272
+ │ │ ├── events.module.ts # Events module (ONLY EventsService)
273
+ │ │ ├── events.service.ts # NATS consumer setup
274
+ │ │ ├── orders-created.event.ts # Handler in src/events/*.event.ts
275
+ │ │ └── customers-updated.event.ts # Handler in src/events/*.event.ts
276
+ │ └── orders/
277
+ │ ├── orders.module.ts
278
+ │ └── orders.service.ts
279
+ └── README.md
280
+
281
+ ❌ WRONG (DO NOT CREATE):
282
+ services/my-service/
283
+ ├── src/events/
284
+ │ ├── handlers/ # ❌ NEVER create handlers/ subfolder
285
+ │ │ ├── orders-created.handler.ts # ❌ NEVER put handlers here
286
+ │ │ └── customers-updated.handler.ts # ❌ NEVER put handlers here
287
+ │ └── events.module.ts (with imports) # ❌ NEVER import handlers
288
+
289
+ packages/contracts/src/events/
290
+ ├── orders-created.ts # Contract with schema
291
+ └── customers-updated.ts # Contract with schema
292
+ ```
293
+
294
+ ---
295
+
296
+ ## Testing
297
+
298
+ ### Unit Test for Service
299
+
300
+ ```ts
301
+ // src/orders/orders.service.spec.ts
302
+ import { Test, TestingModule } from '@nestjs/testing'
303
+ import { OrdersService } from './orders.service'
304
+
305
+ describe('OrdersService', () => {
306
+ let service: OrdersService
307
+
308
+ beforeEach(async () => {
309
+ const module: TestingModule = await Test.createTestingModule({
310
+ providers: [OrdersService],
311
+ }).compile()
312
+
313
+ service = module.get<OrdersService>(OrdersService)
314
+ })
315
+
316
+ it('should process order', async () => {
317
+ const mockData = {
318
+ orderId: 'ord_123',
319
+ customerId: 'cust_456',
320
+ total: 99.99,
321
+ }
322
+
323
+ await expect(service.processOrder(mockData)).resolves.not.toThrow()
324
+ })
325
+ })
326
+ ```
327
+
328
+ ---
329
+
330
+ ## Key Differences from Hono
331
+
332
+ | Aspect | Hono | NestJS |
333
+ |--------|------|--------|
334
+ | Entry point | `src/index.ts` | `src/main.ts` |
335
+ | HTTP server | Hono + Bun.serve | NestFactory.create |
336
+ | Event handlers | `handleEvent()` in `src/events/*.event.ts` | `handleEvent()` in `src/events/*.event.ts` |
337
+ | Business logic | Pure use-cases | `@Injectable()` Services |
338
+ | DI | Not used | Full NestJS DI via `getService()` |
339
+ | Testing | Direct function tests | `@nestjs/testing` utilities |
340
+
341
+ ---
342
+
343
+ ## Dev Command
344
+
345
+ ```json
346
+ {
347
+ "scripts": {
348
+ "start:dev": "nest start --watch"
349
+ }
350
+ }
351
+ ```