@crossdelta/platform-sdk 0.13.3 → 0.13.6

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