@crossdelta/platform-sdk 0.11.3 → 0.13.0
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/README.md +17 -4
- package/bin/cli.js +144 -136
- package/bin/docs/generators/hono-bun.md +104 -10
- package/bin/docs/generators/hono-node.md +112 -10
- package/bin/docs/generators/nest.md +40 -9
- package/bin/docs/generators/service.md +302 -25
- package/bin/templates/hono-microservice/src/index.ts.hbs +18 -0
- package/bin/templates/nest-microservice/src/events/events.service.ts.hbs +7 -10
- package/bin/templates/nest-microservice/src/main.ts.hbs +1 -1
- package/bin/templates/workspace/infra/services/.gitkeep +0 -0
- package/bin/templates/workspace/package.json.hbs +2 -2
- package/bin/templates/workspace/packages/contracts/README.md.hbs +40 -8
- package/bin/templates/workspace/packages/contracts/package.json.hbs +2 -1
- package/bin/templates/workspace/packages/contracts/src/events/index.ts +16 -0
- package/bin/templates/workspace/packages/contracts/src/index.ts +9 -0
- package/bin/templates/workspace/packages/contracts/src/stream-policies.ts.hbs +40 -0
- package/package.json +120 -124
- package/bin/templates/workspace/infra/services/nats.ts.hbs +0 -55
- package/bin/templates/workspace/services/nats/README.md +0 -107
- package/bin/templates/workspace/services/nats/nats.conf +0 -31
- package/bin/templates/workspace/services/nats/nats.prod.conf +0 -27
- package/bin/templates/workspace/services/nats/package.json.hbs +0 -7
- package/bin/templates/workspace/services/nats/scripts/start-dev.sh.hbs +0 -55
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Hono (Bun Runtime)
|
|
2
2
|
|
|
3
|
+
## 🚨 CRITICAL: AI Generation Rules
|
|
4
|
+
|
|
5
|
+
**DO NOT create from scratch:**
|
|
6
|
+
- ❌ `infra/services/<name>.ts` - Created by CLI with port assignment
|
|
7
|
+
- ❌ `Dockerfile` - Created by CLI
|
|
8
|
+
- ❌ `package.json` - Created by CLI
|
|
9
|
+
|
|
10
|
+
**Always generate:**
|
|
11
|
+
- ✅ Event handlers (`src/events/*.handler.ts`)
|
|
12
|
+
- ✅ Use-cases (`src/use-cases/*.use-case.ts`)
|
|
13
|
+
- ✅ Tests (`src/**/*.test.ts`)
|
|
14
|
+
- ✅ README.md
|
|
15
|
+
- ✅ Contracts (`packages/contracts/src/events/`)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
3
19
|
## 🚨 CRITICAL: Commands Block (REQUIRED FIRST)
|
|
4
20
|
|
|
5
21
|
```commands
|
|
@@ -20,7 +36,9 @@ pf new hono-micro services/push-notifications -y
|
|
|
20
36
|
import '@crossdelta/telemetry'
|
|
21
37
|
import { Hono } from 'hono'
|
|
22
38
|
|
|
23
|
-
|
|
39
|
+
// Replace MY_SERVICE with actual service name in SCREAMING_SNAKE_CASE
|
|
40
|
+
// Example: my-hono-service → MY_HONO_SERVICE_PORT
|
|
41
|
+
const port = Number(process.env.MY_SERVICE_PORT) || 8080
|
|
24
42
|
const app = new Hono()
|
|
25
43
|
|
|
26
44
|
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
@@ -32,10 +50,11 @@ console.log(`Service running on http://localhost:${port}`)
|
|
|
32
50
|
**Event Consumer:**
|
|
33
51
|
```ts
|
|
34
52
|
import '@crossdelta/telemetry'
|
|
35
|
-
import { consumeJetStreams
|
|
53
|
+
import { consumeJetStreams } from '@crossdelta/cloudevents'
|
|
36
54
|
import { Hono } from 'hono'
|
|
37
55
|
|
|
38
|
-
|
|
56
|
+
// Replace MY_SERVICE with actual service name in SCREAMING_SNAKE_CASE
|
|
57
|
+
const port = Number(process.env.MY_SERVICE_PORT) || 8080
|
|
39
58
|
const app = new Hono()
|
|
40
59
|
|
|
41
60
|
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
@@ -43,24 +62,30 @@ app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
|
43
62
|
Bun.serve({ port, fetch: app.fetch })
|
|
44
63
|
console.log(`Service running on http://localhost:${port}`)
|
|
45
64
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
// Services NEVER create streams!
|
|
66
|
+
// - Development: pf dev auto-creates ephemeral streams from contracts
|
|
67
|
+
// - Production: Pulumi materializes persistent streams
|
|
50
68
|
consumeJetStreams({
|
|
51
|
-
streams: ['ORDERS'],
|
|
69
|
+
streams: ['ORDERS'], // ⚠️ MUST be PLURAL! Extract from contract's channel.stream
|
|
52
70
|
consumer: 'my-service',
|
|
53
|
-
discover: './src/events/**/*.
|
|
71
|
+
discover: './src/events/**/*.handler.ts',
|
|
54
72
|
})
|
|
55
73
|
```
|
|
56
74
|
|
|
75
|
+
**CRITICAL:** Stream names MUST be PLURAL:
|
|
76
|
+
- ✅ `streams: ['ORDERS']` - for orders.created event
|
|
77
|
+
- ✅ `streams: ['DOMAINS']` - for domain.created event
|
|
78
|
+
- ❌ `streams: ['ORDER']` - WRONG (singular)
|
|
79
|
+
- ❌ `streams: ['DOMAIN']` - WRONG (singular)
|
|
80
|
+
|
|
57
81
|
**Event Publisher:**
|
|
58
82
|
```ts
|
|
59
83
|
import '@crossdelta/telemetry'
|
|
60
84
|
import { publish } from '@crossdelta/cloudevents'
|
|
61
85
|
import { Hono } from 'hono'
|
|
62
86
|
|
|
63
|
-
|
|
87
|
+
// Replace MY_SERVICE with actual service name in SCREAMING_SNAKE_CASE
|
|
88
|
+
const port = Number(process.env.MY_SERVICE_PORT) || 8080
|
|
64
89
|
const app = new Hono()
|
|
65
90
|
|
|
66
91
|
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
@@ -77,6 +102,75 @@ console.log(`Service running on http://localhost:${port}`)
|
|
|
77
102
|
|
|
78
103
|
---
|
|
79
104
|
|
|
105
|
+
## Environment Validation (Optional)
|
|
106
|
+
|
|
107
|
+
**For services with required env vars:**
|
|
108
|
+
|
|
109
|
+
### 1️⃣ Create `src/config/env.ts`:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// src/config/env.ts
|
|
113
|
+
import { z } from 'zod'
|
|
114
|
+
|
|
115
|
+
// ⚠️ DO NOT include SERVICE_NAME_PORT here - it's already handled by template!
|
|
116
|
+
// Only validate YOUR custom env vars
|
|
117
|
+
const envSchema = z.object({
|
|
118
|
+
PUSHER_INSTANCE_ID: z.string().min(1, 'PUSHER_INSTANCE_ID is required'),
|
|
119
|
+
PUSHER_SECRET_KEY: z.string().min(1, 'PUSHER_SECRET_KEY is required'),
|
|
120
|
+
NATS_URL: z.string().url().default('nats://localhost:4222'),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
export type Env = z.infer<typeof envSchema>
|
|
124
|
+
|
|
125
|
+
// Validate and crash process if invalid (like @nestjs/config)
|
|
126
|
+
const result = envSchema.safeParse(process.env)
|
|
127
|
+
if (!result.success) {
|
|
128
|
+
console.error('❌ Environment validation failed:')
|
|
129
|
+
console.error(result.error.format())
|
|
130
|
+
process.exit(1) // ← Force crash with exit code 1
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const env = result.data
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 2️⃣ **CRITICAL: Import in `src/index.ts` early!**
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// src/index.ts
|
|
140
|
+
import '@crossdelta/telemetry' // ← MUST be first!
|
|
141
|
+
|
|
142
|
+
// ⚠️ CRITICAL: Import env IMMEDIATELY after telemetry
|
|
143
|
+
// This executes validation and crashes process if env invalid
|
|
144
|
+
import { env } from './config/env' // ← THIS LINE IS REQUIRED!
|
|
145
|
+
|
|
146
|
+
import { Hono } from 'hono'
|
|
147
|
+
|
|
148
|
+
const port = Number(process.env.MY_SERVICE_PORT) || 8080 // ← Handled by template
|
|
149
|
+
const app = new Hono()
|
|
150
|
+
|
|
151
|
+
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
152
|
+
|
|
153
|
+
app.post('/notify', async (c) => {
|
|
154
|
+
const data = await c.req.json()
|
|
155
|
+
// Use validated env vars
|
|
156
|
+
await sendPush(env.PUSHER_INSTANCE_ID, env.PUSHER_SECRET_KEY, data)
|
|
157
|
+
return c.json({ success: true })
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
Bun.serve({ port, fetch: app.fetch })
|
|
161
|
+
console.log(`Service running on http://localhost:${port}`)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**CRITICAL:**
|
|
165
|
+
- ✅ **MUST import `env` in `src/index.ts`** - without import, validation never runs!
|
|
166
|
+
- ✅ Import early (after telemetry, before everything else)
|
|
167
|
+
- ✅ Use `safeParse()` + `process.exit(1)` - Bun doesn't crash on top-level throws
|
|
168
|
+
- ✅ Explicit exit ensures Turbo shows red X (like NestJS)
|
|
169
|
+
- ❌ **DO NOT** include `SERVICE_NAME_PORT` in env schema - already handled by CLI template
|
|
170
|
+
- ❌ **DO NOT** validate inside handlers or use-cases
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
80
174
|
## Rules
|
|
81
175
|
|
|
82
176
|
- ✅ `Bun.serve({ port, fetch: app.fetch })`
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Hono (Node.js Runtime)
|
|
2
2
|
|
|
3
|
+
## 🚨 CRITICAL: AI Generation Rules
|
|
4
|
+
|
|
5
|
+
**DO NOT generate these files** - they are created by `pf new hono-micro`:
|
|
6
|
+
- ❌ `infra/services/<name>.ts` - Infrastructure config with assigned port
|
|
7
|
+
- ❌ `Dockerfile` - Container configuration
|
|
8
|
+
- ❌ `package.json` - Dependencies and scripts
|
|
9
|
+
|
|
10
|
+
**Always generate:**
|
|
11
|
+
- ✅ Event handlers (`src/events/*.handler.ts`)
|
|
12
|
+
- ✅ Use-cases (`src/use-cases/*.use-case.ts`)
|
|
13
|
+
- ✅ Tests (`src/**/*.test.ts`)
|
|
14
|
+
- ✅ README.md
|
|
15
|
+
- ✅ Contracts (`packages/contracts/src/events/`)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
3
19
|
## 🚨 CRITICAL: Commands Block (REQUIRED FIRST)
|
|
4
20
|
|
|
5
21
|
```commands
|
|
@@ -21,7 +37,8 @@ import '@crossdelta/telemetry'
|
|
|
21
37
|
import { serve } from '@hono/node-server'
|
|
22
38
|
import { Hono } from 'hono'
|
|
23
39
|
|
|
24
|
-
|
|
40
|
+
// Replace MY_SERVICE with actual service name in SCREAMING_SNAKE_CASE
|
|
41
|
+
const port = Number(process.env.MY_SERVICE_PORT) || 8080
|
|
25
42
|
const app = new Hono()
|
|
26
43
|
|
|
27
44
|
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
@@ -34,11 +51,12 @@ serve({ fetch: app.fetch, port }, (info) => {
|
|
|
34
51
|
**Event Consumer:**
|
|
35
52
|
```ts
|
|
36
53
|
import '@crossdelta/telemetry'
|
|
37
|
-
import { consumeJetStreams
|
|
54
|
+
import { consumeJetStreams } from '@crossdelta/cloudevents'
|
|
38
55
|
import { serve } from '@hono/node-server'
|
|
39
56
|
import { Hono } from 'hono'
|
|
40
57
|
|
|
41
|
-
|
|
58
|
+
// Replace MY_SERVICE with actual service name in SCREAMING_SNAKE_CASE
|
|
59
|
+
const port = Number(process.env.MY_SERVICE_PORT) || 8080
|
|
42
60
|
const app = new Hono()
|
|
43
61
|
|
|
44
62
|
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
@@ -47,17 +65,22 @@ serve({ fetch: app.fetch, port }, (info) => {
|
|
|
47
65
|
console.log(`Server running on http://localhost:${info.port}`)
|
|
48
66
|
})
|
|
49
67
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
// Services NEVER create streams!
|
|
69
|
+
// - Development: pf dev auto-creates ephemeral streams from contracts
|
|
70
|
+
// - Production: Pulumi materializes persistent streams
|
|
54
71
|
consumeJetStreams({
|
|
55
|
-
streams: ['ORDERS'],
|
|
72
|
+
streams: ['ORDERS'], // ⚠️ MUST be PLURAL! Extract from contract's channel.stream
|
|
56
73
|
consumer: 'my-service',
|
|
57
|
-
discover: './src/events/**/*.
|
|
74
|
+
discover: './src/events/**/*.handler.ts',
|
|
58
75
|
})
|
|
59
76
|
```
|
|
60
77
|
|
|
78
|
+
**CRITICAL:** Stream names MUST be PLURAL:
|
|
79
|
+
- ✅ `streams: ['ORDERS']` - for orders.created event
|
|
80
|
+
- ✅ `streams: ['DOMAINS']` - for domain.created event
|
|
81
|
+
- ❌ `streams: ['ORDER']` - WRONG (singular)
|
|
82
|
+
- ❌ `streams: ['DOMAIN']` - WRONG (singular)
|
|
83
|
+
|
|
61
84
|
**Event Publisher:**
|
|
62
85
|
```ts
|
|
63
86
|
import '@crossdelta/telemetry'
|
|
@@ -65,7 +88,8 @@ import { publish } from '@crossdelta/cloudevents'
|
|
|
65
88
|
import { serve } from '@hono/node-server'
|
|
66
89
|
import { Hono } from 'hono'
|
|
67
90
|
|
|
68
|
-
|
|
91
|
+
// Replace MY_SERVICE with actual service name in SCREAMING_SNAKE_CASE
|
|
92
|
+
const port = Number(process.env.MY_SERVICE_PORT) || 8080
|
|
69
93
|
const app = new Hono()
|
|
70
94
|
|
|
71
95
|
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
@@ -83,6 +107,84 @@ serve({ fetch: app.fetch, port }, (info) => {
|
|
|
83
107
|
|
|
84
108
|
---
|
|
85
109
|
|
|
110
|
+
## Environment Validation (Optional)
|
|
111
|
+
|
|
112
|
+
**For services with required env vars:**
|
|
113
|
+
|
|
114
|
+
### 1️⃣ Create `src/config/env.ts`:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// src/config/env.ts
|
|
118
|
+
import { z } from 'zod'
|
|
119
|
+
|
|
120
|
+
// ⚠️ DO NOT include SERVICE_NAME_PORT here - it's already handled by template!
|
|
121
|
+
// Only validate YOUR custom env vars
|
|
122
|
+
const envSchema = z.object({
|
|
123
|
+
PUSHER_INSTANCE_ID: z.string().min(1, 'PUSHER_INSTANCE_ID is required'),
|
|
124
|
+
PUSHER_SECRET_KEY: z.string().min(1, 'PUSHER_SECRET_KEY is required'),
|
|
125
|
+
NATS_URL: z.string().url().default('nats://localhost:4222'),
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
export type Env = z.infer<typeof envSchema>
|
|
129
|
+
|
|
130
|
+
// Validate and crash process if invalid (like @nestjs/config)
|
|
131
|
+
const result = envSchema.safeParse(process.env)
|
|
132
|
+
if (!result.success) {
|
|
133
|
+
console.error('❌ Environment validation failed:')
|
|
134
|
+
console.error(result.error.format())
|
|
135
|
+
process.exit(1) // ← Force crash with exit code 1
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const env = result.data
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 2️⃣ **CRITICAL: Import in `src/index.ts` early!**
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// src/index.ts
|
|
145
|
+
import '@crossdelta/telemetry' // ← MUST be first!
|
|
146
|
+
|
|
147
|
+
// ⚠️ CRITICAL: Import env IMMEDIATELY after telemetry
|
|
148
|
+
// This executes validation and crashes process if env invalid
|
|
149
|
+
import { env } from './config/env' // ← THIS LINE IS REQUIRED!
|
|
150
|
+
|
|
151
|
+
import { serve } from '@hono/node-server'
|
|
152
|
+
import { Hono } from 'hono'
|
|
153
|
+
import { consumeJetStreams } from '@crossdelta/cloudevents'
|
|
154
|
+
|
|
155
|
+
const port = Number(process.env.NOTIFICATIONS_PORT) || 8080 // ← Handled by template
|
|
156
|
+
|
|
157
|
+
const app = new Hono()
|
|
158
|
+
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
159
|
+
|
|
160
|
+
app.post('/notify', async (c) => {
|
|
161
|
+
const data = await c.req.json()
|
|
162
|
+
// Use validated env vars
|
|
163
|
+
await sendPush(env.PUSHER_INSTANCE_ID, env.PUSHER_SECRET_KEY, data)
|
|
164
|
+
return c.json({ success: true })
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
consumeJetStreams({
|
|
168
|
+
streams: ['DOMAINS'],
|
|
169
|
+
consumer: 'notifications',
|
|
170
|
+
discover: './src/events/**/*.handler.ts',
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
serve({ fetch: app.fetch, port }, (info) => {
|
|
174
|
+
console.log(`Server running on http://localhost:${info.port}`)
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**CRITICAL:**
|
|
179
|
+
- ✅ **MUST import `env` in `src/index.ts`** - without import, validation never runs!
|
|
180
|
+
- ✅ Import early (after telemetry, before everything else)
|
|
181
|
+
- ✅ Use `safeParse()` + `process.exit(1)` - ensures proper crash behavior
|
|
182
|
+
- ✅ Explicit exit ensures Turbo shows red X (like NestJS)
|
|
183
|
+
- ❌ **DO NOT** include `SERVICE_NAME_PORT` in env schema - already handled by CLI template
|
|
184
|
+
- ❌ **DO NOT** validate inside handlers or use-cases
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
86
188
|
## Rules
|
|
87
189
|
|
|
88
190
|
- ✅ `serve()` from `@hono/node-server`
|
|
@@ -4,6 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
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
|
+
|
|
7
24
|
## 🚨 CRITICAL: Use NestJS Built-in Features
|
|
8
25
|
|
|
9
26
|
**NEVER create manual implementations when NestJS provides the feature:**
|
|
@@ -22,9 +39,13 @@
|
|
|
22
39
|
import { z } from 'zod'
|
|
23
40
|
|
|
24
41
|
export const envSchema = z.object({
|
|
42
|
+
PORT: z.string().transform(Number).default('8080'),
|
|
25
43
|
PUSHER_INSTANCE_ID: z.string().min(1),
|
|
26
44
|
PUSHER_SECRET_KEY: z.string().min(1),
|
|
45
|
+
NATS_URL: z.string().url().default('nats://localhost:4222'),
|
|
27
46
|
})
|
|
47
|
+
|
|
48
|
+
export type Env = z.infer<typeof envSchema>
|
|
28
49
|
```
|
|
29
50
|
|
|
30
51
|
```ts
|
|
@@ -48,17 +69,26 @@ export class AppModule {}
|
|
|
48
69
|
// ✅ Service uses ConfigService - already validated at startup
|
|
49
70
|
import { Injectable } from '@nestjs/common'
|
|
50
71
|
import { ConfigService } from '@nestjs/config'
|
|
72
|
+
import type { Env } from './config/env.schema'
|
|
51
73
|
|
|
52
74
|
@Injectable()
|
|
53
75
|
export class MyService {
|
|
54
|
-
constructor(private config: ConfigService) {}
|
|
76
|
+
constructor(private config: ConfigService<Env, true>) {}
|
|
55
77
|
|
|
56
78
|
doSomething() {
|
|
57
|
-
const instanceId = this.config.
|
|
79
|
+
const instanceId = this.config.get('PUSHER_INSTANCE_ID') // Type-safe!
|
|
80
|
+
const port = this.config.get('PORT') // number (auto-transformed)
|
|
58
81
|
}
|
|
59
82
|
}
|
|
60
83
|
```
|
|
61
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
|
+
|
|
62
92
|
---
|
|
63
93
|
|
|
64
94
|
## 🚨 CRITICAL: NestJS Structure (NO use-cases folder!)
|
|
@@ -74,7 +104,7 @@ src/
|
|
|
74
104
|
├── events/
|
|
75
105
|
│ ├── events.module.ts
|
|
76
106
|
│ ├── events.service.ts
|
|
77
|
-
│ └── domain-created.
|
|
107
|
+
│ └── domain-created.handler.ts # Handler → calls service
|
|
78
108
|
└── notifications/
|
|
79
109
|
├── notifications.module.ts
|
|
80
110
|
├── notifications.service.ts # Business logic HERE
|
|
@@ -113,6 +143,7 @@ import { EventsService } from './events/events.service'
|
|
|
113
143
|
|
|
114
144
|
async function bootstrap() {
|
|
115
145
|
const app = await NestFactory.create(AppModule)
|
|
146
|
+
// Replace MY_SERVICE with actual service name in SCREAMING_SNAKE_CASE
|
|
116
147
|
const port = Number(process.env.MY_SERVICE_PORT) || 8080
|
|
117
148
|
|
|
118
149
|
setAppContext(app)
|
|
@@ -148,21 +179,21 @@ export class EventsModule {}
|
|
|
148
179
|
|
|
149
180
|
```ts
|
|
150
181
|
import { Injectable, Logger } from '@nestjs/common'
|
|
151
|
-
import { consumeJetStreams
|
|
182
|
+
import { consumeJetStreams } from '@crossdelta/cloudevents'
|
|
152
183
|
|
|
153
184
|
@Injectable()
|
|
154
185
|
export class EventsService {
|
|
155
186
|
private readonly logger = new Logger(EventsService.name)
|
|
156
187
|
|
|
157
188
|
async startConsumers(): Promise<void> {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
189
|
+
// Services NEVER create streams!
|
|
190
|
+
// - Development: pf dev auto-creates ephemeral streams from contracts
|
|
191
|
+
// - Production: Pulumi materializes persistent streams
|
|
161
192
|
|
|
162
193
|
consumeJetStreams({
|
|
163
194
|
streams: ['ORDERS'],
|
|
164
195
|
consumer: 'my-service',
|
|
165
|
-
discover: './src/events/**/*.
|
|
196
|
+
discover: './src/events/**/*.handler.ts',
|
|
166
197
|
})
|
|
167
198
|
}
|
|
168
199
|
}
|
|
@@ -195,7 +226,7 @@ export const getService = <T>(serviceClass: Type<T>): T => {
|
|
|
195
226
|
|
|
196
227
|
---
|
|
197
228
|
|
|
198
|
-
## Event Handler (src/events/orders-created.
|
|
229
|
+
## Event Handler (src/events/orders-created.handler.ts)
|
|
199
230
|
|
|
200
231
|
**Handlers must be thin** - just log and delegate to a service:
|
|
201
232
|
|