@crossdelta/platform-sdk 0.12.0 → 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.
@@ -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
- const port = Number(process.env.PORT || process.env.MY_SERVICE_PORT) || 8080
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, ensureJetStreams } from '@crossdelta/cloudevents'
53
+ import { consumeJetStreams } from '@crossdelta/cloudevents'
36
54
  import { Hono } from 'hono'
37
55
 
38
- const port = Number(process.env.PORT || process.env.MY_SERVICE_PORT) || 8080
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
- await ensureJetStreams({
47
- streams: [{ stream: 'ORDERS', subjects: ['orders.*'] }]
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/**/*.event.ts',
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
- const port = Number(process.env.PORT || process.env.MY_SERVICE_PORT) || 8080
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' }))
@@ -79,51 +104,70 @@ console.log(`Service running on http://localhost:${port}`)
79
104
 
80
105
  ## Environment Validation (Optional)
81
106
 
82
- For services with required env vars, validate at startup with Zod:
107
+ **For services with required env vars:**
108
+
109
+ ### 1️⃣ Create `src/config/env.ts`:
83
110
 
84
111
  ```ts
85
112
  // src/config/env.ts
86
113
  import { z } from 'zod'
87
114
 
115
+ // ⚠️ DO NOT include SERVICE_NAME_PORT here - it's already handled by template!
116
+ // Only validate YOUR custom env vars
88
117
  const envSchema = z.object({
89
- PORT: z.string().transform(Number).default('8080'),
90
- PUSHER_INSTANCE_ID: z.string().min(1),
91
- PUSHER_SECRET_KEY: z.string().min(1),
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'),
92
120
  NATS_URL: z.string().url().default('nats://localhost:4222'),
93
121
  })
94
122
 
95
123
  export type Env = z.infer<typeof envSchema>
96
124
 
97
- // Validate at module load - crashes immediately if invalid
98
- export const env = envSchema.parse(process.env)
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
99
134
  ```
100
135
 
136
+ ### 2️⃣ **CRITICAL: Import in `src/index.ts` early!**
137
+
101
138
  ```ts
102
139
  // src/index.ts
103
- import '@crossdelta/telemetry'
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
+
104
146
  import { Hono } from 'hono'
105
- import { env } from './config/env'
106
147
 
148
+ const port = Number(process.env.MY_SERVICE_PORT) || 8080 // ← Handled by template
107
149
  const app = new Hono()
108
150
 
109
151
  app.get('/health', (c) => c.json({ status: 'ok' }))
110
152
 
111
153
  app.post('/notify', async (c) => {
112
154
  const data = await c.req.json()
113
- // Use validated env
155
+ // Use validated env vars
114
156
  await sendPush(env.PUSHER_INSTANCE_ID, env.PUSHER_SECRET_KEY, data)
115
157
  return c.json({ success: true })
116
158
  })
117
159
 
118
- Bun.serve({ port: env.PORT, fetch: app.fetch })
119
- console.log(`Service running on http://localhost:${env.PORT}`)
160
+ Bun.serve({ port, fetch: app.fetch })
161
+ console.log(`Service running on http://localhost:${port}`)
120
162
  ```
121
163
 
122
- **Benefits:**
123
- - ✅ Fail-fast at startup (not at runtime)
124
- - ✅ Type-safe env access (`env.PORT` is `number`)
125
- - ✅ Clear error messages for missing vars
126
- - ✅ Defaults for optional vars
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
127
171
 
128
172
  ---
129
173
 
@@ -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
- const port = Number(process.env.PORT || process.env.MY_SERVICE_PORT) || 8080
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, ensureJetStreams } from '@crossdelta/cloudevents'
54
+ import { consumeJetStreams } from '@crossdelta/cloudevents'
38
55
  import { serve } from '@hono/node-server'
39
56
  import { Hono } from 'hono'
40
57
 
41
- const port = Number(process.env.PORT || process.env.MY_SERVICE_PORT) || 8080
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
- await ensureJetStreams({
51
- streams: [{ stream: 'ORDERS', subjects: ['orders.*'] }]
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/**/*.event.ts',
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
- const port = Number(process.env.PORT || process.env.MY_SERVICE_PORT) || 8080
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' }))
@@ -85,53 +109,79 @@ serve({ fetch: app.fetch, port }, (info) => {
85
109
 
86
110
  ## Environment Validation (Optional)
87
111
 
88
- For services with required env vars, validate at startup with Zod:
112
+ **For services with required env vars:**
113
+
114
+ ### 1️⃣ Create `src/config/env.ts`:
89
115
 
90
116
  ```ts
91
117
  // src/config/env.ts
92
118
  import { z } from 'zod'
93
119
 
120
+ // ⚠️ DO NOT include SERVICE_NAME_PORT here - it's already handled by template!
121
+ // Only validate YOUR custom env vars
94
122
  const envSchema = z.object({
95
- PORT: z.string().transform(Number).default('8080'),
96
- PUSHER_INSTANCE_ID: z.string().min(1),
97
- PUSHER_SECRET_KEY: z.string().min(1),
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'),
98
125
  NATS_URL: z.string().url().default('nats://localhost:4222'),
99
126
  })
100
127
 
101
128
  export type Env = z.infer<typeof envSchema>
102
129
 
103
- // Validate at module load - crashes immediately if invalid
104
- export const env = envSchema.parse(process.env)
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
105
139
  ```
106
140
 
141
+ ### 2️⃣ **CRITICAL: Import in `src/index.ts` early!**
142
+
107
143
  ```ts
108
144
  // src/index.ts
109
- import '@crossdelta/telemetry'
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
+
110
151
  import { serve } from '@hono/node-server'
111
152
  import { Hono } from 'hono'
112
- import { env } from './config/env'
153
+ import { consumeJetStreams } from '@crossdelta/cloudevents'
113
154
 
114
- const app = new Hono()
155
+ const port = Number(process.env.NOTIFICATIONS_PORT) || 8080 // ← Handled by template
115
156
 
157
+ const app = new Hono()
116
158
  app.get('/health', (c) => c.json({ status: 'ok' }))
117
159
 
118
160
  app.post('/notify', async (c) => {
119
161
  const data = await c.req.json()
120
- // Use validated env
162
+ // Use validated env vars
121
163
  await sendPush(env.PUSHER_INSTANCE_ID, env.PUSHER_SECRET_KEY, data)
122
164
  return c.json({ success: true })
123
165
  })
124
166
 
125
- serve({ fetch: app.fetch, port: env.PORT }, (info) => {
167
+ consumeJetStreams({
168
+ streams: ['DOMAINS'],
169
+ consumer: 'notifications',
170
+ discover: './src/events/**/*.handler.ts',
171
+ })
172
+
173
+ serve({ fetch: app.fetch, port }, (info) => {
126
174
  console.log(`Server running on http://localhost:${info.port}`)
127
175
  })
128
176
  ```
129
177
 
130
- **Benefits:**
131
- - ✅ Fail-fast at startup (not at runtime)
132
- - ✅ Type-safe env access (`env.PORT` is `number`)
133
- - ✅ Clear error messages for missing vars
134
- - ✅ Defaults for optional vars
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
135
185
 
136
186
  ---
137
187
 
@@ -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:**
@@ -87,7 +104,7 @@ src/
87
104
  ├── events/
88
105
  │ ├── events.module.ts
89
106
  │ ├── events.service.ts
90
- │ └── domain-created.event.ts # Handler → calls service
107
+ │ └── domain-created.handler.ts # Handler → calls service
91
108
  └── notifications/
92
109
  ├── notifications.module.ts
93
110
  ├── notifications.service.ts # Business logic HERE
@@ -126,6 +143,7 @@ import { EventsService } from './events/events.service'
126
143
 
127
144
  async function bootstrap() {
128
145
  const app = await NestFactory.create(AppModule)
146
+ // Replace MY_SERVICE with actual service name in SCREAMING_SNAKE_CASE
129
147
  const port = Number(process.env.MY_SERVICE_PORT) || 8080
130
148
 
131
149
  setAppContext(app)
@@ -161,21 +179,21 @@ export class EventsModule {}
161
179
 
162
180
  ```ts
163
181
  import { Injectable, Logger } from '@nestjs/common'
164
- import { consumeJetStreams, ensureJetStreams } from '@crossdelta/cloudevents'
182
+ import { consumeJetStreams } from '@crossdelta/cloudevents'
165
183
 
166
184
  @Injectable()
167
185
  export class EventsService {
168
186
  private readonly logger = new Logger(EventsService.name)
169
187
 
170
188
  async startConsumers(): Promise<void> {
171
- await ensureJetStreams({
172
- streams: [{ stream: 'ORDERS', subjects: ['orders.*'] }],
173
- })
189
+ // Services NEVER create streams!
190
+ // - Development: pf dev auto-creates ephemeral streams from contracts
191
+ // - Production: Pulumi materializes persistent streams
174
192
 
175
193
  consumeJetStreams({
176
194
  streams: ['ORDERS'],
177
195
  consumer: 'my-service',
178
- discover: './src/events/**/*.event.ts',
196
+ discover: './src/events/**/*.handler.ts',
179
197
  })
180
198
  }
181
199
  }
@@ -208,7 +226,7 @@ export const getService = <T>(serviceClass: Type<T>): T => {
208
226
 
209
227
  ---
210
228
 
211
- ## Event Handler (src/events/orders-created.event.ts)
229
+ ## Event Handler (src/events/orders-created.handler.ts)
212
230
 
213
231
  **Handlers must be thin** - just log and delegate to a service:
214
232