@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,564 @@
|
|
|
1
|
+
# Service Generator Instructions
|
|
2
|
+
|
|
3
|
+
**Generic guidelines for all service types. Framework-specific details are in separate files:**
|
|
4
|
+
- `hono-bun.md` - Hono with Bun runtime
|
|
5
|
+
- `hono-node.md` - Hono with Node.js runtime
|
|
6
|
+
- `nest.md` - NestJS framework
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 🚨 CRITICAL: Port Configuration
|
|
11
|
+
|
|
12
|
+
Services MUST read their port from environment variables using this pattern:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// Convert service name to ENV key format
|
|
16
|
+
// Example: my-hono-service → MY_HONO_SERVICE_PORT
|
|
17
|
+
const port = Number(process.env.MY_HONO_SERVICE_PORT) || 8080
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Port ENV variable naming:**
|
|
21
|
+
- Take service name from package.json `name` field (without scope)
|
|
22
|
+
- Convert to SCREAMING_SNAKE_CASE
|
|
23
|
+
- Append `_PORT`
|
|
24
|
+
|
|
25
|
+
**Examples:**
|
|
26
|
+
- `@my-platform/orders` → `ORDERS_PORT`
|
|
27
|
+
- `@my-platform/my-nest-service` → `MY_NEST_SERVICE_PORT`
|
|
28
|
+
- `@my-platform/api-gateway` → `API_GATEWAY_PORT`
|
|
29
|
+
|
|
30
|
+
**⚠️ CRITICAL: DO NOT hardcode or modify the port!**
|
|
31
|
+
- The `pf new` command automatically assigns a unique port
|
|
32
|
+
- The port is written to `.env.local` (e.g., `MY_SERVICE_PORT=4001`)
|
|
33
|
+
- Service code MUST use the correct env variable name
|
|
34
|
+
- DO NOT change the port in generated files!
|
|
35
|
+
|
|
36
|
+
**When generating service code:**
|
|
37
|
+
1. Determine the correct env variable name from service name
|
|
38
|
+
2. Use ONLY that variable: `process.env.MY_SERVICE_PORT`
|
|
39
|
+
3. DO NOT use generic `PORT` - it causes conflicts!
|
|
40
|
+
4. Keep the `|| 8080` fallback for when `.env.local` is missing
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 🚨 CRITICAL: Stream Architecture
|
|
45
|
+
|
|
46
|
+
> **Services NEVER create streams!**
|
|
47
|
+
>
|
|
48
|
+
> In development, `pf dev` ensures ephemeral streams derived from contracts.
|
|
49
|
+
> In production, streams are materialized explicitly via Pulumi.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Development Workflow
|
|
54
|
+
|
|
55
|
+
### 1️⃣ **Initial Setup** (once per workspace)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pf new workspace my-platform
|
|
59
|
+
cd my-platform
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 2️⃣ **Create Service** (AI or manual)
|
|
63
|
+
|
|
64
|
+
#### Option A: AI Generation (via GitHub Copilot Chat)
|
|
65
|
+
```
|
|
66
|
+
"Create an order processing service that consumes orders.created events"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**What AI generates:**
|
|
70
|
+
1. **Contract** (`packages/contracts/src/events/orders/created.ts`) - With proper Zod schema in domain-grouped structure
|
|
71
|
+
2. **Service** (`services/order-processing/`) - Scaffolded via `pf new hono-micro`
|
|
72
|
+
3. **Event Handler** (`services/order-processing/src/events/orders-created.handler.ts`)
|
|
73
|
+
4. **Use-Cases** (`services/order-processing/src/use-cases/`)
|
|
74
|
+
5. **Tests** (`services/order-processing/src/use-cases/*.test.ts`)
|
|
75
|
+
|
|
76
|
+
**What AI includes in response:**
|
|
77
|
+
```commands
|
|
78
|
+
pf new hono-micro services/order-processing -y
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```post-commands
|
|
82
|
+
pf event add orders.created --service services/order-processing
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**What `pf event add` does:**
|
|
86
|
+
- Creates `packages/contracts/src/events/orders-created.mock.json` (test mock)
|
|
87
|
+
- Adds export to `packages/contracts/src/index.ts`
|
|
88
|
+
- Skips contract creation if already exists (AI's schema is preserved!)
|
|
89
|
+
|
|
90
|
+
#### Option B: Manual Creation
|
|
91
|
+
|
|
92
|
+
**Step 1: Create contract with CLI**
|
|
93
|
+
```bash
|
|
94
|
+
# Create contract with schema
|
|
95
|
+
pf event add orders.created --fields "orderId:string,total:number,customerId:string"
|
|
96
|
+
|
|
97
|
+
# Or with JSON schema
|
|
98
|
+
pf event add orders.created --schema '{"orderId":"string","total":"number"}'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Step 2: Scaffold service**
|
|
102
|
+
```bash
|
|
103
|
+
pf new hono-micro services/order-processing
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Step 3: Register event and create handler**
|
|
107
|
+
```bash
|
|
108
|
+
pf event add orders.created --service services/order-processing
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Step 4: Implement use-cases**
|
|
112
|
+
```bash
|
|
113
|
+
# Edit services/order-processing/src/use-cases/process-order.use-case.ts
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### Option C: Pure Manual (no CLI)
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# 1. Create contract manually (domain-grouped)
|
|
120
|
+
mkdir -p packages/contracts/src/events/orders
|
|
121
|
+
# Edit packages/contracts/src/events/orders/created.ts
|
|
122
|
+
|
|
123
|
+
# 2. Scaffold service
|
|
124
|
+
pf new hono-micro services/order-processing
|
|
125
|
+
|
|
126
|
+
# 3. Create handler manually
|
|
127
|
+
# Edit services/order-processing/src/events/orders-created.handler.ts
|
|
128
|
+
|
|
129
|
+
# 4. Implement use-cases
|
|
130
|
+
# Edit services/order-processing/src/use-cases/
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 3️⃣ **Run Development Environment**
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
pf dev # Starts NATS + creates ephemeral streams from contracts + all services
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**What happens:**
|
|
140
|
+
- **NATS** starts in Docker with JetStream enabled
|
|
141
|
+
- **Ephemeral streams** auto-created from contract channel metadata (memory storage, 1h retention)
|
|
142
|
+
- **Services** start and consume from streams
|
|
143
|
+
- **Hot reload** watches for changes
|
|
144
|
+
|
|
145
|
+
**Stream Creation:** `pf dev` scans `packages/contracts/src/index.ts` for contracts with `channel.stream` metadata and creates ephemeral streams automatically. No manual stream creation needed!
|
|
146
|
+
|
|
147
|
+
### 4️⃣ **Deploy to Production**
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
cd infra
|
|
151
|
+
pulumi up
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**What happens:**
|
|
155
|
+
1. **`infra/streams/`** collects contracts via `collectStreamDefinitions()`
|
|
156
|
+
2. **Streams materialized** via Pulumi with retention policies
|
|
157
|
+
3. **Services deployed** to Kubernetes
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## �🚨 CRITICAL: Stream Architecture
|
|
162
|
+
|
|
163
|
+
> **Services NEVER create streams!**
|
|
164
|
+
> **Dev:** Streams are auto-created (ephemeral, memory)
|
|
165
|
+
> **Prod:** Streams are materialized via Pulumi from contracts
|
|
166
|
+
|
|
167
|
+
### Contracts → Streams → Infra Flow
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
// 1. Contract defines conceptually (packages/contracts)
|
|
171
|
+
export const OrdersCreatedContract = createContract({
|
|
172
|
+
type: 'orders.created',
|
|
173
|
+
channel: { stream: 'ORDERS' }, // Routing metadata
|
|
174
|
+
schema: OrderSchema,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// 2. Services consume (no stream creation!)
|
|
178
|
+
consumeJetStreams({
|
|
179
|
+
streams: ['ORDERS'],
|
|
180
|
+
consumer: 'my-service',
|
|
181
|
+
discover: './src/events/**/*.handler.ts',
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// 3. Infra materializes (infra/streams/)
|
|
185
|
+
import { collectStreamDefinitions } from '@crossdelta/infrastructure'
|
|
186
|
+
import * as contracts from '@workspace/contracts'
|
|
187
|
+
|
|
188
|
+
// Generic logic in library, workspace data injected
|
|
189
|
+
const streams = collectStreamDefinitions(contracts)
|
|
190
|
+
deployStreams(provider, namespace, streams)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Architecture layers:**
|
|
194
|
+
- **@crossdelta/infrastructure:** Generic stream collection logic
|
|
195
|
+
- **Contracts:** Define event semantics + channel routing (workspace data)
|
|
196
|
+
- **`pf dev`:** Ensures ephemeral streams (transient, memory-only)
|
|
197
|
+
- **Platform-Infra:** Materializes streams via Pulumi (persistent, retention)
|
|
198
|
+
- **Services:** Consume streams (never create!)
|
|
199
|
+
|
|
200
|
+
**Separation of concerns:**
|
|
201
|
+
- **Libraries contain logic** (generic, reusable)
|
|
202
|
+
- **Workspaces contain data** (contracts, policies)
|
|
203
|
+
- **No workspace imports inside libraries**
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 🚨 CRITICAL: @crossdelta/cloudevents API
|
|
208
|
+
|
|
209
|
+
**ONLY these exports exist:**
|
|
210
|
+
- `consumeJetStreams` (preferred) - Consume from multiple streams
|
|
211
|
+
- `consumeJetStreamEvents` (legacy) - Consume from single stream
|
|
212
|
+
- `handleEvent`, `publish`, `createContract`
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// Services only consume (DO NOT use ensureJetStreams!)
|
|
216
|
+
consumeJetStreams({
|
|
217
|
+
streams: ['ORDERS'],
|
|
218
|
+
consumer: 'my-service',
|
|
219
|
+
discover: './src/events/**/*.handler.ts',
|
|
220
|
+
})
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**DO NOT USE in services:**
|
|
224
|
+
- ❌ `ensureJetStreams()` - Only for infra materialization
|
|
225
|
+
- ❌ `ensureJetStreamStream()` - Legacy, only for infra
|
|
226
|
+
- ❌ Manual stream creation - Services never create streams!
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## 🚨 CRITICAL: @crossdelta/telemetry Import
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import '@crossdelta/telemetry' // side-effect, MUST be FIRST import
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**DO NOT USE:** `@crossdelta/telemetry/register`, named exports
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 🚨 CRITICAL: Commands Block (REQUIRED - MUST BE FIRST)
|
|
241
|
+
|
|
242
|
+
Your response **MUST START** with a `commands` block that scaffolds the service.
|
|
243
|
+
The exact command depends on the framework - see the framework-specific docs:
|
|
244
|
+
- `hono-bun.md` / `hono-node.md` → `pf new hono-micro`
|
|
245
|
+
- `nest.md` → `pf new nest-micro`
|
|
246
|
+
|
|
247
|
+
**Without this command, the service cannot be built!**
|
|
248
|
+
|
|
249
|
+
**What `pf new` creates automatically:**
|
|
250
|
+
1. Service directory structure (`services/<name>/`)
|
|
251
|
+
2. Infrastructure config (`infra/services/<name>.ts`) with assigned port
|
|
252
|
+
3. Package.json with dependencies
|
|
253
|
+
4. Entry point (`src/index.ts` or `src/main.ts`) with port configuration
|
|
254
|
+
5. Health check endpoint
|
|
255
|
+
6. Dockerfile
|
|
256
|
+
|
|
257
|
+
**⚠️ AI MUST NOT generate `infra/services/<name>.ts`** - This file is automatically created by the CLI with the correct port assignment!
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## 🚨 CRITICAL: Post-Commands Block (REQUIRED for Event Consumers)
|
|
262
|
+
|
|
263
|
+
**For Event Consumer services, include a `post-commands` block with ALL events:**
|
|
264
|
+
|
|
265
|
+
```post-commands
|
|
266
|
+
pf event add orders.created --service services/my-service
|
|
267
|
+
pf event add customers.updated --service services/my-service
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**What `pf event add` creates:**
|
|
271
|
+
- `packages/contracts/src/events/<event>.mock.json` - Test mock data
|
|
272
|
+
- Adds export to `packages/contracts/src/index.ts`
|
|
273
|
+
|
|
274
|
+
**⚠️ IMPORTANT:** `pf event add` skips contract creation if it already exists - so YOUR contract with the correct schema is preserved!
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 🚨 CRITICAL: Event Consumer Workflow
|
|
279
|
+
|
|
280
|
+
**FOR EVENT CONSUMER SERVICES (services that "consume", "listen to", "react to" events):**
|
|
281
|
+
|
|
282
|
+
### What YOU (the AI) generate:
|
|
283
|
+
|
|
284
|
+
1. **`packages/contracts/src/events/<domain>/<event>.ts`** - Contract with correct schema fields in domain-grouped structure (e.g., `events/orders/created.ts`)
|
|
285
|
+
2. **`src/events/<event>.handler.ts`** - Event handler that calls the use-case
|
|
286
|
+
3. **`src/use-cases/*.use-case.ts`** - Business logic (called by handlers)
|
|
287
|
+
4. **`src/use-cases/*.test.ts`** - Tests for use-cases
|
|
288
|
+
5. **`README.md`** - Documentation
|
|
289
|
+
|
|
290
|
+
**⚠️ DO NOT generate `src/index.ts` or `src/main.ts`** - These are created by `pf new` with correct port config!
|
|
291
|
+
|
|
292
|
+
### Handler Location (CRITICAL!)
|
|
293
|
+
|
|
294
|
+
**⚠️ MUST be in `src/events/*.handler.ts` - NEVER in `handlers/` subdirectory**
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
✅ CORRECT:
|
|
298
|
+
services/my-service/src/events/
|
|
299
|
+
├── orders-created.handler.ts
|
|
300
|
+
└── customers-updated.handler.ts
|
|
301
|
+
|
|
302
|
+
❌ WRONG:
|
|
303
|
+
services/my-service/src/events/handlers/ # NEVER create!
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Handler Export Pattern
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
// ✅ CORRECT: Default export
|
|
310
|
+
export default handleEvent(OrdersCreatedContract, async (data) => { ... })
|
|
311
|
+
|
|
312
|
+
// ❌ WRONG: Named export
|
|
313
|
+
export const OrdersCreatedHandler = handleEvent(...)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Handler Logging
|
|
317
|
+
|
|
318
|
+
**Handlers must be thin** - use `console.log` for logging, then delegate to use-case/service:
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
export default handleEvent(OrdersCreatedContract, async (data) => {
|
|
322
|
+
console.log(`[orders.created] Processing orderId=${data.orderId}`) // ✅ console.log
|
|
323
|
+
await processOrder(data)
|
|
324
|
+
})
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Services/use-cases** can use structured logging (NestJS: `new Logger(ServiceName.name)`).
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## 🚨 Response Structure
|
|
332
|
+
|
|
333
|
+
```commands
|
|
334
|
+
pf new hono-micro services/my-service -y
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
#### `packages/contracts/src/events/orders/created.ts`
|
|
338
|
+
```ts
|
|
339
|
+
import { createContract } from '@crossdelta/cloudevents'
|
|
340
|
+
import { z } from 'zod'
|
|
341
|
+
|
|
342
|
+
export const OrdersCreatedSchema = z.object({
|
|
343
|
+
orderId: z.string(),
|
|
344
|
+
total: z.number(),
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
export const OrdersCreatedContract = createContract({
|
|
348
|
+
type: 'orders.created',
|
|
349
|
+
channel: { stream: 'ORDERS' },
|
|
350
|
+
schema: OrdersCreatedSchema,
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
export type OrdersCreatedData = z.infer<typeof OrdersCreatedContract.schema>
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
#### `src/events/orders-created.handler.ts`
|
|
357
|
+
```ts
|
|
358
|
+
import { handleEvent } from '@crossdelta/cloudevents'
|
|
359
|
+
import { OrdersCreatedContract, type OrdersCreatedData } from '{{scope}}/contracts'
|
|
360
|
+
import { processOrder } from '../use-cases/process-order.use-case'
|
|
361
|
+
|
|
362
|
+
export default handleEvent(OrdersCreatedContract, async (data: OrdersCreatedData) => {
|
|
363
|
+
console.log('📦 Processing order:', data.orderId)
|
|
364
|
+
await processOrder(data)
|
|
365
|
+
})
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
#### `src/use-cases/process-order.use-case.ts`
|
|
369
|
+
```ts
|
|
370
|
+
import type { OrdersCreatedData } from '{{scope}}/contracts'
|
|
371
|
+
|
|
372
|
+
export const processOrder = async (data: OrdersCreatedData): Promise<void> => {
|
|
373
|
+
console.log('Processing:', data.orderId)
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
```post-commands
|
|
378
|
+
pf event add orders.created --service services/my-service
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
```dependencies
|
|
382
|
+
@pusher/push-notifications-server
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## 🚨 CRITICAL: Schema Fields Consistency
|
|
388
|
+
|
|
389
|
+
**ONLY use fields that exist in YOUR generated schema!**
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
// If you generate this schema:
|
|
393
|
+
export const DomainCreatedSchema = z.object({
|
|
394
|
+
domainId: z.string(),
|
|
395
|
+
name: z.string(),
|
|
396
|
+
ownerEmail: z.string().email(),
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
// Then ONLY use these fields in service code:
|
|
400
|
+
await sendEmail(data.ownerEmail) // ✅ EXISTS in schema
|
|
401
|
+
await notifyUser(data.userId) // ❌ WRONG - userId not in schema!
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**DO NOT hallucinate fields** like `userId`, `customerId`, etc. unless they are in YOUR schema.
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## Contract Schema (Zod 4 Syntax)
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
export const OrdersCreatedSchema = z.object({
|
|
412
|
+
orderId: z.string(),
|
|
413
|
+
email: z.string().email(), // ✅ Zod 4: no params
|
|
414
|
+
total: z.number(),
|
|
415
|
+
createdAt: z.string().datetime(), // ✅ Zod 4: no params
|
|
416
|
+
})
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Zod 4 Quick Reference:**
|
|
420
|
+
- ✅ `z.string().email()` - no params
|
|
421
|
+
- ✅ `z.string().url()` - no params
|
|
422
|
+
- ❌ `z.string().email('Invalid')` - DEPRECATED
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Naming Convention
|
|
427
|
+
|
|
428
|
+
**Architecture Rule:**
|
|
429
|
+
|
|
430
|
+
| Component | Format | Example | Reason |
|
|
431
|
+
|-----------|--------|---------|--------|
|
|
432
|
+
| **Event Type** | **Singular** | `domain.created` | Describes a single event |
|
|
433
|
+
| **Stream** | **PLURAL** | `DOMAINS` | Collection of events for a domain |
|
|
434
|
+
|
|
435
|
+
**Merksatz:** Events sind singular. Streams sind plural.
|
|
436
|
+
|
|
437
|
+
### Examples
|
|
438
|
+
|
|
439
|
+
| Event Type | Contract | File | Stream |
|
|
440
|
+
|------------|----------|------|--------|
|
|
441
|
+
| `orders.created` | `OrdersCreatedContract` | `orders-created.ts` | `ORDERS` ✅ |
|
|
442
|
+
| `domain.created` | `DomainCreatedContract` | `domain-created.ts` | `DOMAINS` ✅ |
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
// ✅ CORRECT
|
|
446
|
+
export const DomainCreatedContract = createContract({
|
|
447
|
+
type: 'domain.created', // Singular event
|
|
448
|
+
channel: { stream: 'DOMAINS' }, // Plural stream
|
|
449
|
+
schema: DomainCreatedSchema,
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
export const OrdersCreatedContract = createContract({
|
|
453
|
+
type: 'orders.created', // Singular event (but namespace plural!)
|
|
454
|
+
channel: { stream: 'ORDERS' }, // Plural stream
|
|
455
|
+
schema: OrdersCreatedSchema,
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
// ❌ WRONG
|
|
459
|
+
export const DomainCreatedContract = createContract({
|
|
460
|
+
type: 'domain.created',
|
|
461
|
+
channel: { stream: 'DOMAIN' }, // ❌ Must be DOMAINS (plural!)
|
|
462
|
+
schema: DomainCreatedSchema,
|
|
463
|
+
})
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Stream Names in consumeJetStreams
|
|
467
|
+
|
|
468
|
+
**CRITICAL:** When consuming from streams, use the **PLURAL** stream name from the contract's `channel.stream`:
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
// ✅ CORRECT - domain.created event → DOMAINS stream
|
|
472
|
+
consumeJetStreams({
|
|
473
|
+
streams: ['DOMAINS'], // ✅ Plural!
|
|
474
|
+
consumer: 'my-service',
|
|
475
|
+
discover: './src/events/**/*.handler.ts',
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
// ✅ CORRECT - orders.created event → ORDERS stream
|
|
479
|
+
consumeJetStreams({
|
|
480
|
+
streams: ['ORDERS'], // ✅ Plural!
|
|
481
|
+
consumer: 'my-service',
|
|
482
|
+
discover: './src/events/**/*.handler.ts',
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
// ❌ WRONG - Don't use singular!
|
|
486
|
+
consumeJetStreams({
|
|
487
|
+
streams: ['DOMAIN'], // ❌ Wrong! Should be DOMAINS
|
|
488
|
+
consumer: 'my-service',
|
|
489
|
+
discover: './src/events/**/*.handler.ts',
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
// ❌ WRONG - Don't use lowercase!
|
|
493
|
+
consumeJetStreams({
|
|
494
|
+
streams: ['domains'], // ❌ Wrong! Should be DOMAINS (uppercase)
|
|
495
|
+
consumer: 'my-service',
|
|
496
|
+
discover: './src/events/**/*.handler.ts',
|
|
497
|
+
})
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
**Rule:** Extract the stream name from your contract's `channel.stream` field and use it EXACTLY in `consumeJetStreams({ streams: ['...'] })`.
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Block Explanations
|
|
505
|
+
|
|
506
|
+
| Block | Purpose |
|
|
507
|
+
|-------|---------|
|
|
508
|
+
| `commands` | Scaffolds service (REQUIRED FIRST) |
|
|
509
|
+
| `post-commands` | Runs after files created (e.g., `pf event add`) |
|
|
510
|
+
| `dependencies` | Extra npm packages (NOT `@crossdelta/*`) |
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## Service Types
|
|
515
|
+
|
|
516
|
+
### 📤 Event Publisher (REST API)
|
|
517
|
+
**Keywords:** "publishes", "creates", "manages", "REST API"
|
|
518
|
+
|
|
519
|
+
### 📥 Event Consumer (NATS)
|
|
520
|
+
**Keywords:** "consumes", "listens to", "reacts to", "handles events"
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
### 🔄 Hybrid (Both)
|
|
524
|
+
Combines REST endpoints + NATS consumer.
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## Required Files (Hono)
|
|
529
|
+
|
|
530
|
+
```
|
|
531
|
+
services/my-service/src/
|
|
532
|
+
├── index.ts
|
|
533
|
+
├── events/orders-created.handler.ts
|
|
534
|
+
└── use-cases/
|
|
535
|
+
├── process-order.use-case.ts
|
|
536
|
+
└── process-order.test.ts
|
|
537
|
+
|
|
538
|
+
packages/contracts/src/events/
|
|
539
|
+
└── orders-created.ts
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
**Note:** For NestJS structure, see `nest.md` - NestJS uses Services instead of use-cases.
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## Absolute Rules
|
|
547
|
+
|
|
548
|
+
**DO NOT:**
|
|
549
|
+
- ❌ Use raw NATS clients
|
|
550
|
+
- ❌ Put business logic in handlers or entry points
|
|
551
|
+
- ❌ Use `src/handlers/` (use `src/events/`)
|
|
552
|
+
- ❌ Create `src/types/` directory (use contracts)
|
|
553
|
+
- ❌ Insert semicolons
|
|
554
|
+
- ❌ Edit `packages/contracts/src/index.ts` (CLI handles exports)
|
|
555
|
+
- ❌ Create `use-cases/` folder in NestJS (use Services instead)
|
|
556
|
+
|
|
557
|
+
**DO:**
|
|
558
|
+
- ✅ Contracts in `packages/contracts/src/events/`
|
|
559
|
+
- ✅ Handlers in `src/events/*.handler.ts`
|
|
560
|
+
- ✅ Hono: Use-cases in `src/use-cases/*.use-case.ts`
|
|
561
|
+
- ✅ NestJS: Business logic in Services + pure helper functions
|
|
562
|
+
- ✅ Log event type and key identifier in handlers
|
|
563
|
+
- ✅ Keep handlers thin - delegate to use-cases/services
|
|
564
|
+
- ✅ Verify package names on npmjs.com before using
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Testing Guidelines
|
|
2
|
+
|
|
3
|
+
## 🚨 CRITICAL: bun:test Only
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// ✅ CORRECT
|
|
7
|
+
import { describe, expect, it } from 'bun:test'
|
|
8
|
+
|
|
9
|
+
// ❌ WRONG - do NOT use
|
|
10
|
+
jest.mock(...) // Jest doesn't work
|
|
11
|
+
jest.unstable_mockModule() // Jest doesn't work
|
|
12
|
+
vi.mock(...) // Vitest doesn't work
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## What to Test
|
|
18
|
+
|
|
19
|
+
- Test ONLY use-cases (not handlers)
|
|
20
|
+
- No mocking - use dependency injection
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { describe, expect, it } from 'bun:test'
|
|
24
|
+
import { processOrder } from './process-order.use-case'
|
|
25
|
+
|
|
26
|
+
describe('processOrder', () => {
|
|
27
|
+
it('processes order', async () => {
|
|
28
|
+
const store = new Map()
|
|
29
|
+
await processOrder({ orderId: '123', total: 100 }, store)
|
|
30
|
+
expect(store.has('123')).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('throws on missing data', async () => {
|
|
34
|
+
await expect(processOrder(null as any, new Map())).rejects.toThrow()
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## External Services & Config
|
|
42
|
+
|
|
43
|
+
Use dependency injection, not mocking:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// ✅ Use-case accepts dependencies
|
|
47
|
+
export const sendPush = async (
|
|
48
|
+
data: PushData,
|
|
49
|
+
config: { instanceId: string }
|
|
50
|
+
) => {
|
|
51
|
+
if (!config.instanceId) throw new Error('Missing instanceId')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ✅ Test with fake/config
|
|
55
|
+
it('throws on missing config', async () => {
|
|
56
|
+
await expect(sendPush({}, { instanceId: '' })).rejects.toThrow()
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## ❌ WRONG patterns (NEVER use these)
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// process.env manipulation
|
|
66
|
+
beforeEach(() => { process.env.X = 'test' })
|
|
67
|
+
afterEach(() => { process.env = env })
|
|
68
|
+
|
|
69
|
+
// @ts-ignore for mocking
|
|
70
|
+
// @ts-ignore
|
|
71
|
+
svc.beams = new FakeBeams()
|
|
72
|
+
|
|
73
|
+
// Jest/Vitest mocking
|
|
74
|
+
jest.mock(...)
|
|
75
|
+
vi.mock(...)
|
|
76
|
+
|
|
77
|
+
// Dynamic imports for error testing
|
|
78
|
+
await expect(import('./module')).rejects.toThrow()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## NestJS Services
|
|
84
|
+
|
|
85
|
+
For NestJS: extract business logic into pure functions, test those:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// ✅ Extract testable logic from service
|
|
89
|
+
export const validateConfig = (config: { id: string }) => {
|
|
90
|
+
if (!config.id) throw new Error('Missing id')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ✅ Test the extracted function
|
|
94
|
+
it('validates config', () => {
|
|
95
|
+
expect(() => validateConfig({ id: '' })).toThrow()
|
|
96
|
+
})
|
|
97
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "@crossdelta/cloudevents",
|
|
4
|
+
"description": "Type-safe event-driven microservices with NATS and Zod validation",
|
|
5
|
+
"link": "https://www.npmjs.com/package/@crossdelta/cloudevents",
|
|
6
|
+
"install": ["@crossdelta/cloudevents", "zod@^4.0.0"],
|
|
7
|
+
"run": [],
|
|
8
|
+
"initial": true
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "@crossdelta/telemetry",
|
|
12
|
+
"description": "Zero-config OpenTelemetry instrumentation for TypeScript services",
|
|
13
|
+
"link": "https://www.npmjs.com/package/@crossdelta/telemetry",
|
|
14
|
+
"install": "@crossdelta/telemetry",
|
|
15
|
+
"run": [],
|
|
16
|
+
"initial": true
|
|
17
|
+
}
|
|
18
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1.7-labs
|
|
2
|
+
ARG BUN_VERSION={{bunVersion}}
|
|
3
|
+
|
|
4
|
+
FROM oven/bun:${BUN_VERSION}-alpine AS production
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
|
|
7
|
+
COPY bunfig.toml package.json ./
|
|
8
|
+
COPY src ./src
|
|
9
|
+
|
|
10
|
+
RUN --mount=type=secret,id=NPM_TOKEN \
|
|
11
|
+
export NPM_TOKEN="$(cat /run/secrets/NPM_TOKEN)" && \
|
|
12
|
+
bun install --production --omit=optional
|
|
13
|
+
|
|
14
|
+
USER bun
|
|
15
|
+
|
|
16
|
+
CMD ["bun", "run", "src/index.ts"]
|