@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.
- package/bin/cli.js +185 -279
- package/bin/docs/generators/hono-bun.md +101 -0
- package/bin/docs/generators/hono-node.md +116 -0
- package/bin/docs/generators/nest.md +265 -0
- package/bin/docs/generators/nest.md.new +351 -0
- package/bin/docs/generators/service.md +501 -78
- package/bin/templates/hono-microservice/biome.json.hbs +3 -0
- package/bin/templates/hono-microservice/tsconfig.json.hbs +1 -0
- package/bin/templates/nest-microservice/biome.json.hbs +10 -0
- package/bin/templates/nest-microservice/src/app.context.ts.hbs +17 -0
- package/bin/templates/nest-microservice/src/events/events.module.ts.hbs +8 -0
- package/bin/templates/nest-microservice/src/events/events.service.ts.hbs +25 -0
- package/bin/templates/nest-microservice/src/main.ts.hbs +9 -0
- package/bin/templates/workspace/.github/copilot-instructions.md.hbs +1 -0
- package/bin/templates/workspace/biome.json.hbs +1 -1
- package/bin/templates/workspace/package.json.hbs +2 -6
- package/bin/templates/workspace/packages/contracts/package.json.hbs +3 -1
- package/bin/templates/workspace/packages/contracts/tsconfig.json.hbs +0 -1
- package/package.json +30 -4
- package/schemas/service-types.schema.json +33 -0
- package/bin/templates/hono-microservice/src/index.ts.hbs +0 -18
|
@@ -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
|
+
```
|