@crossdelta/platform-sdk 0.13.2 → 0.13.4
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 +312 -0
- package/bin/docs/generators/README.md +56 -0
- package/bin/docs/generators/code-style.md +96 -0
- package/bin/docs/generators/hono-bun.md +181 -0
- package/bin/docs/generators/hono-node.md +194 -0
- package/bin/docs/generators/nest.md +358 -0
- package/bin/docs/generators/service.md +564 -0
- package/bin/docs/generators/testing.md +97 -0
- package/bin/integration.collection.json +18 -0
- package/bin/templates/hono-microservice/Dockerfile.hbs +16 -0
- package/bin/templates/hono-microservice/biome.json.hbs +3 -0
- package/bin/templates/hono-microservice/src/index.ts.hbs +18 -0
- package/bin/templates/hono-microservice/tsconfig.json.hbs +14 -0
- package/bin/templates/nest-microservice/Dockerfile.hbs +37 -0
- package/bin/templates/nest-microservice/biome.json.hbs +3 -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 +22 -0
- package/bin/templates/nest-microservice/src/main.ts.hbs +34 -0
- package/bin/templates/workspace/biome.json.hbs +62 -0
- package/bin/templates/workspace/bunfig.toml.hbs +5 -0
- package/bin/templates/workspace/editorconfig.hbs +9 -0
- package/bin/templates/workspace/gitignore.hbs +15 -0
- package/bin/templates/workspace/infra/Pulumi.dev.yaml.hbs +5 -0
- package/bin/templates/workspace/infra/Pulumi.yaml.hbs +6 -0
- package/bin/templates/workspace/infra/index.ts.hbs +56 -0
- package/bin/templates/workspace/infra/package.json.hbs +23 -0
- package/bin/templates/workspace/infra/tsconfig.json.hbs +15 -0
- package/bin/templates/workspace/npmrc.hbs +2 -0
- package/bin/templates/workspace/package.json.hbs +51 -0
- package/bin/templates/workspace/packages/contracts/README.md.hbs +166 -0
- package/bin/templates/workspace/packages/contracts/package.json.hbs +22 -0
- package/bin/templates/workspace/packages/contracts/src/events/index.ts +16 -0
- package/bin/templates/workspace/packages/contracts/src/index.ts +10 -0
- package/bin/templates/workspace/packages/contracts/src/stream-policies.ts.hbs +40 -0
- package/bin/templates/workspace/packages/contracts/tsconfig.json.hbs +7 -0
- package/bin/templates/workspace/pnpm-workspace.yaml.hbs +5 -0
- package/bin/templates/workspace/turbo.json +38 -0
- package/bin/templates/workspace/turbo.json.hbs +29 -0
- package/install.sh +46 -8
- package/package.json +1 -1
|
@@ -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
|