@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.
@@ -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' }))
@@ -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
- 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' }))
@@ -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.getOrThrow<string>('PUSHER_INSTANCE_ID')
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.event.ts # Handler → calls service
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, ensureJetStreams } from '@crossdelta/cloudevents'
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
- await ensureJetStreams({
159
- streams: [{ stream: 'ORDERS', subjects: ['orders.*'] }],
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/**/*.event.ts',
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.event.ts)
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