@crossdelta/platform-sdk 0.16.3 → 0.16.5
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 +6 -6
- package/bin/cli.js +137 -135
- package/bin/docs/generators/code-style.md +5 -5
- package/bin/docs/generators/hono-bun.md +2 -2
- package/bin/docs/generators/hono-node.md +2 -2
- package/bin/docs/generators/nest.md +4 -4
- package/bin/docs/generators/service.md +123 -95
- package/bin/templates/workspace/infra/package.json.hbs +1 -1
- package/bin/templates/workspace/packages/contracts/README.md.hbs +17 -17
- package/bin/templates/workspace/packages/contracts/package.json.hbs +1 -1
- package/package.json +1 -1
|
@@ -14,17 +14,17 @@
|
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
16
|
// ✅ CORRECT - sorted alphabetically, type imports first
|
|
17
|
-
import type { DomainCreatedData } from '
|
|
18
|
-
import type {
|
|
17
|
+
import type { DomainCreatedData } from '{workspaceScope}/contracts'
|
|
18
|
+
import type { OrderCreatedData } from '@scope/contracts'
|
|
19
19
|
import { handleEvent } from '@crossdelta/cloudevents'
|
|
20
20
|
import PusherPushNotifications from '@pusher/push-notifications-server'
|
|
21
21
|
|
|
22
22
|
// ❌ WRONG - unsorted
|
|
23
23
|
import PusherPushNotifications from '@pusher/push-notifications-server'
|
|
24
|
-
import type { DomainCreatedData } from '
|
|
24
|
+
import type { DomainCreatedData } from '{workspaceScope}/contracts'
|
|
25
25
|
|
|
26
26
|
// ❌ WRONG - missing 'type' keyword
|
|
27
|
-
import {
|
|
27
|
+
import { OrderCreatedData } from '@scope/contracts'
|
|
28
28
|
|
|
29
29
|
// ❌ WRONG - unused imports
|
|
30
30
|
import { handleEvent, publish } from '@crossdelta/cloudevents'
|
|
@@ -51,7 +51,7 @@ const apiKey = process.env.API_KEY! // no assertions
|
|
|
51
51
|
|------|------------|---------|
|
|
52
52
|
| Files | kebab-case | `send-notification.use-case.ts` |
|
|
53
53
|
| Contracts | PascalCase | `OrdersCreatedContract` |
|
|
54
|
-
| Types | PascalCase + Data | `
|
|
54
|
+
| Types | PascalCase + Data | `OrderCreatedData` |
|
|
55
55
|
|
|
56
56
|
---
|
|
57
57
|
|
|
@@ -83,7 +83,7 @@ consumeJetStreams({
|
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
**CRITICAL:** Stream names MUST be PLURAL:
|
|
86
|
-
- ✅ `streams: ['ORDERS']` - for
|
|
86
|
+
- ✅ `streams: ['ORDERS']` - for order.created event
|
|
87
87
|
- ✅ `streams: ['DOMAINS']` - for domain.created event
|
|
88
88
|
- ❌ `streams: ['ORDER']` - WRONG (singular)
|
|
89
89
|
- ❌ `streams: ['DOMAIN']` - WRONG (singular)
|
|
@@ -104,7 +104,7 @@ app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
|
104
104
|
|
|
105
105
|
app.post('/orders', async (c) => {
|
|
106
106
|
const data = await c.req.json()
|
|
107
|
-
await publish('
|
|
107
|
+
await publish('order.created', data)
|
|
108
108
|
return c.json({ success: true })
|
|
109
109
|
})
|
|
110
110
|
|
|
@@ -86,7 +86,7 @@ consumeJetStreams({
|
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
**CRITICAL:** Stream names MUST be PLURAL:
|
|
89
|
-
- ✅ `streams: ['ORDERS']` - for
|
|
89
|
+
- ✅ `streams: ['ORDERS']` - for order.created event
|
|
90
90
|
- ✅ `streams: ['DOMAINS']` - for domain.created event
|
|
91
91
|
- ❌ `streams: ['ORDER']` - WRONG (singular)
|
|
92
92
|
- ❌ `streams: ['DOMAIN']` - WRONG (singular)
|
|
@@ -108,7 +108,7 @@ app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
|
108
108
|
|
|
109
109
|
app.post('/orders', async (c) => {
|
|
110
110
|
const data = await c.req.json()
|
|
111
|
-
await publish('
|
|
111
|
+
await publish('order.created', data)
|
|
112
112
|
return c.json({ success: true })
|
|
113
113
|
})
|
|
114
114
|
|
|
@@ -242,12 +242,12 @@ export const getService = <T>(serviceClass: Type<T>): T => {
|
|
|
242
242
|
|
|
243
243
|
```ts
|
|
244
244
|
import { handleEvent } from '@crossdelta/cloudevents'
|
|
245
|
-
import { OrdersCreatedContract, type
|
|
245
|
+
import { OrdersCreatedContract, type OrderCreatedData } from '{workspaceScope}/contracts'
|
|
246
246
|
import { getService } from '../app.context'
|
|
247
247
|
import { OrdersService } from '../orders/orders.service'
|
|
248
248
|
|
|
249
|
-
export default handleEvent(OrdersCreatedContract, async (data:
|
|
250
|
-
console.log(`[
|
|
249
|
+
export default handleEvent(OrdersCreatedContract, async (data: OrderCreatedData) => {
|
|
250
|
+
console.log(`[order.created] Processing orderId=${data.orderId}`)
|
|
251
251
|
const ordersService = getService(OrdersService)
|
|
252
252
|
await ordersService.processOrder(data)
|
|
253
253
|
})
|
|
@@ -299,7 +299,7 @@ export class AppController {
|
|
|
299
299
|
|
|
300
300
|
```ts
|
|
301
301
|
// src/notifications/send-notification.ts (pure function)
|
|
302
|
-
import type { DomainCreatedData } from '{
|
|
302
|
+
import type { DomainCreatedData } from '{workspaceScope}/contracts'
|
|
303
303
|
|
|
304
304
|
export const buildNotificationPayload = (data: DomainCreatedData) => ({
|
|
305
305
|
title: 'Domain Created',
|
|
@@ -47,8 +47,33 @@ const port = Number(process.env.MY_HONO_SERVICE_PORT) || 8080
|
|
|
47
47
|
|
|
48
48
|
> **Services NEVER create streams!**
|
|
49
49
|
>
|
|
50
|
-
>
|
|
51
|
-
>
|
|
50
|
+
> **Dev:** `pf dev` auto-creates ephemeral streams from contracts (memory, 1h retention)
|
|
51
|
+
> **Prod:** Streams materialized via Pulumi with retention policies
|
|
52
|
+
|
|
53
|
+
### Architecture Flow
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// 1. Contract defines routing
|
|
57
|
+
export const OrdersCreatedContract = createContract({
|
|
58
|
+
type: 'order.created',
|
|
59
|
+
channel: { stream: 'ORDERS' },
|
|
60
|
+
schema: OrderSchema,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// 2. Services consume (never create!)
|
|
64
|
+
consumeJetStreams({
|
|
65
|
+
streams: ['ORDERS'],
|
|
66
|
+
consumer: 'my-service',
|
|
67
|
+
discover: './src/events/**/*.handler.ts',
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// 3. Infra materializes (infra/streams/)
|
|
71
|
+
import { collectStreamDefinitions } from '@crossdelta/infrastructure'
|
|
72
|
+
const streams = collectStreamDefinitions(contracts)
|
|
73
|
+
deployStreams(provider, namespace, streams)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Key Principle:** Contracts define **what** (routing), infrastructure defines **how** (retention).
|
|
52
77
|
|
|
53
78
|
---
|
|
54
79
|
|
|
@@ -65,7 +90,7 @@ cd my-platform
|
|
|
65
90
|
|
|
66
91
|
#### Option A: AI Generation (via GitHub Copilot Chat)
|
|
67
92
|
```
|
|
68
|
-
"Create an order processing service that consumes
|
|
93
|
+
"Create an order processing service that consumes order.created events"
|
|
69
94
|
```
|
|
70
95
|
|
|
71
96
|
**What AI generates:**
|
|
@@ -81,7 +106,7 @@ pf new hono-micro services/order-processing -y
|
|
|
81
106
|
```
|
|
82
107
|
|
|
83
108
|
```post-commands
|
|
84
|
-
pf event add
|
|
109
|
+
pf event add order.created --service services/order-processing
|
|
85
110
|
```
|
|
86
111
|
|
|
87
112
|
**What `pf event add` does:**
|
|
@@ -94,10 +119,10 @@ pf event add orders.created --service services/order-processing
|
|
|
94
119
|
**Step 1: Create contract with CLI**
|
|
95
120
|
```bash
|
|
96
121
|
# Create contract with schema
|
|
97
|
-
pf event add
|
|
122
|
+
pf event add order.created --fields "orderId:string,total:number,customerId:string"
|
|
98
123
|
|
|
99
124
|
# Or with JSON schema
|
|
100
|
-
pf event add
|
|
125
|
+
pf event add order.created --schema '{"orderId":"string","total":"number"}'
|
|
101
126
|
```
|
|
102
127
|
|
|
103
128
|
**Step 2: Scaffold service**
|
|
@@ -107,7 +132,7 @@ pf new hono-micro services/order-processing
|
|
|
107
132
|
|
|
108
133
|
**Step 3: Register event and create handler**
|
|
109
134
|
```bash
|
|
110
|
-
pf event add
|
|
135
|
+
pf event add order.created --service services/order-processing
|
|
111
136
|
```
|
|
112
137
|
|
|
113
138
|
**Step 4: Implement use-cases**
|
|
@@ -160,52 +185,6 @@ pulumi up
|
|
|
160
185
|
|
|
161
186
|
---
|
|
162
187
|
|
|
163
|
-
## �🚨 CRITICAL: Stream Architecture
|
|
164
|
-
|
|
165
|
-
> **Services NEVER create streams!**
|
|
166
|
-
> **Dev:** Streams are auto-created (ephemeral, memory)
|
|
167
|
-
> **Prod:** Streams are materialized via Pulumi from contracts
|
|
168
|
-
|
|
169
|
-
### Contracts → Streams → Infra Flow
|
|
170
|
-
|
|
171
|
-
```typescript
|
|
172
|
-
// 1. Contract defines conceptually (packages/contracts)
|
|
173
|
-
export const OrdersCreatedContract = createContract({
|
|
174
|
-
type: 'orders.created',
|
|
175
|
-
channel: { stream: 'ORDERS' }, // Routing metadata
|
|
176
|
-
schema: OrderSchema,
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
// 2. Services consume (no stream creation!)
|
|
180
|
-
consumeJetStreams({
|
|
181
|
-
streams: ['ORDERS'],
|
|
182
|
-
consumer: 'my-service',
|
|
183
|
-
discover: './src/events/**/*.handler.ts',
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
// 3. Infra materializes (infra/streams/)
|
|
187
|
-
import { collectStreamDefinitions } from '@crossdelta/infrastructure'
|
|
188
|
-
import * as contracts from '@workspace/contracts'
|
|
189
|
-
|
|
190
|
-
// Generic logic in library, workspace data injected
|
|
191
|
-
const streams = collectStreamDefinitions(contracts)
|
|
192
|
-
deployStreams(provider, namespace, streams)
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
**Architecture layers:**
|
|
196
|
-
- **@crossdelta/infrastructure:** Generic stream collection logic
|
|
197
|
-
- **Contracts:** Define event semantics + channel routing (workspace data)
|
|
198
|
-
- **`pf dev`:** Ensures ephemeral streams (transient, memory-only)
|
|
199
|
-
- **Platform-Infra:** Materializes streams via Pulumi (persistent, retention)
|
|
200
|
-
- **Services:** Consume streams (never create!)
|
|
201
|
-
|
|
202
|
-
**Separation of concerns:**
|
|
203
|
-
- **Libraries contain logic** (generic, reusable)
|
|
204
|
-
- **Workspaces contain data** (contracts, policies)
|
|
205
|
-
- **No workspace imports inside libraries**
|
|
206
|
-
|
|
207
|
-
---
|
|
208
|
-
|
|
209
188
|
## 🚨 CRITICAL: @crossdelta/cloudevents API
|
|
210
189
|
|
|
211
190
|
**ONLY these exports exist:**
|
|
@@ -269,8 +248,8 @@ The exact command depends on the framework - see the framework-specific docs:
|
|
|
269
248
|
**For Event Consumer services, include a `post-commands` block with ALL events:**
|
|
270
249
|
|
|
271
250
|
```post-commands
|
|
272
|
-
pf event add
|
|
273
|
-
pf event add
|
|
251
|
+
pf event add order.created --service services/my-service
|
|
252
|
+
pf event add customer.updated --service services/my-service
|
|
274
253
|
```
|
|
275
254
|
|
|
276
255
|
**What `pf event add` creates:**
|
|
@@ -285,13 +264,30 @@ pf event add customers.updated --service services/my-service
|
|
|
285
264
|
|
|
286
265
|
**FOR EVENT CONSUMER SERVICES (services that "consume", "listen to", "react to" events):**
|
|
287
266
|
|
|
267
|
+
### ⚠️ CRITICAL: Contract Exports (MUST DO FIRST!)
|
|
268
|
+
|
|
269
|
+
**Before generating ANY files, check if the contract export exists:**
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// packages/contracts/src/index.ts
|
|
273
|
+
export * from './events/orders/created' // ← MUST exist!
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**If missing, ADD IT before generating handler!** Services import from `{workspaceScope}/contracts`, not individual files.
|
|
277
|
+
|
|
278
|
+
**Common Error if missing:**
|
|
279
|
+
```
|
|
280
|
+
Export named 'OrderCreatedContract' not found in module 'packages/contracts/src/index.ts'
|
|
281
|
+
```
|
|
282
|
+
|
|
288
283
|
### What YOU (the AI) generate:
|
|
289
284
|
|
|
290
285
|
1. **`packages/contracts/src/events/<domain>/<event>.ts`** - Contract with correct schema fields in domain-grouped structure (e.g., `events/orders/created.ts`)
|
|
291
|
-
2. **`src/
|
|
292
|
-
3. **`src/
|
|
293
|
-
4. **`src/use-cases/*.
|
|
294
|
-
5. **`
|
|
286
|
+
2. **`packages/contracts/src/index.ts`** - ⚠️ **ADD EXPORT** for the contract (CRITICAL!)
|
|
287
|
+
3. **`src/events/<event>.handler.ts`** - Event handler that calls the use-case
|
|
288
|
+
4. **`src/use-cases/*.use-case.ts`** - Business logic (called by handlers)
|
|
289
|
+
5. **`src/use-cases/*.test.ts`** - Tests for use-cases
|
|
290
|
+
6. **`README.md`** - Documentation
|
|
295
291
|
|
|
296
292
|
**⚠️ DO NOT generate `src/index.ts` or `src/main.ts`** - These are created by `pf new` with correct port config!
|
|
297
293
|
|
|
@@ -325,7 +321,7 @@ export const OrdersCreatedHandler = handleEvent(...)
|
|
|
325
321
|
|
|
326
322
|
```ts
|
|
327
323
|
export default handleEvent(OrdersCreatedContract, async (data) => {
|
|
328
|
-
console.log(`[
|
|
324
|
+
console.log(`[order.created] Processing orderId=${data.orderId}`) // ✅ console.log
|
|
329
325
|
await processOrder(data)
|
|
330
326
|
})
|
|
331
327
|
```
|
|
@@ -345,27 +341,33 @@ pf new hono-micro services/my-service -y
|
|
|
345
341
|
import { createContract } from '@crossdelta/cloudevents'
|
|
346
342
|
import { z } from 'zod'
|
|
347
343
|
|
|
348
|
-
export const
|
|
344
|
+
export const OrderCreatedSchema = z.object({
|
|
349
345
|
orderId: z.string(),
|
|
350
346
|
total: z.number(),
|
|
351
347
|
})
|
|
352
348
|
|
|
349
|
+
export type OrderCreatedData = z.infer<typeof OrderCreatedSchema>
|
|
350
|
+
|
|
353
351
|
export const OrdersCreatedContract = createContract({
|
|
354
|
-
type: '
|
|
352
|
+
type: 'order.created',
|
|
355
353
|
channel: { stream: 'ORDERS' },
|
|
356
|
-
schema:
|
|
354
|
+
schema: OrderCreatedSchema,
|
|
357
355
|
})
|
|
356
|
+
```
|
|
358
357
|
|
|
359
|
-
|
|
358
|
+
#### `packages/contracts/src/index.ts`
|
|
359
|
+
```ts
|
|
360
|
+
// Export all contracts (REQUIRED - handlers import from here!)
|
|
361
|
+
export * from './events/orders/created'
|
|
360
362
|
```
|
|
361
363
|
|
|
362
364
|
#### `src/events/orders-created.handler.ts`
|
|
363
365
|
```ts
|
|
364
366
|
import { handleEvent } from '@crossdelta/cloudevents'
|
|
365
|
-
import { OrdersCreatedContract, type
|
|
367
|
+
import { OrdersCreatedContract, type OrderCreatedData } from '{workspaceScope}/contracts'
|
|
366
368
|
import { processOrder } from '../use-cases/process-order.use-case'
|
|
367
369
|
|
|
368
|
-
export default handleEvent(OrdersCreatedContract, async (data:
|
|
370
|
+
export default handleEvent(OrdersCreatedContract, async (data: OrderCreatedData) => {
|
|
369
371
|
console.log('📦 Processing order:', data.orderId)
|
|
370
372
|
await processOrder(data)
|
|
371
373
|
})
|
|
@@ -373,15 +375,15 @@ export default handleEvent(OrdersCreatedContract, async (data: OrdersCreatedData
|
|
|
373
375
|
|
|
374
376
|
#### `src/use-cases/process-order.use-case.ts`
|
|
375
377
|
```ts
|
|
376
|
-
import type {
|
|
378
|
+
import type { OrderCreatedData } from '{workspaceScope}/contracts'
|
|
377
379
|
|
|
378
|
-
export const processOrder = async (data:
|
|
380
|
+
export const processOrder = async (data: OrderCreatedData): Promise<void> => {
|
|
379
381
|
console.log('Processing:', data.orderId)
|
|
380
382
|
}
|
|
381
383
|
```
|
|
382
384
|
|
|
383
385
|
```post-commands
|
|
384
|
-
pf event add
|
|
386
|
+
pf event add order.created --service services/my-service
|
|
385
387
|
```
|
|
386
388
|
|
|
387
389
|
```dependencies
|
|
@@ -416,14 +418,24 @@ await notifyUser(data.userId) // ❌ WRONG - userId not in schema!
|
|
|
416
418
|
## Contract Schema (Zod 4 Syntax)
|
|
417
419
|
|
|
418
420
|
```ts
|
|
419
|
-
export const
|
|
421
|
+
export const OrderCreatedSchema = z.object({
|
|
420
422
|
orderId: z.string(),
|
|
421
423
|
email: z.string().email(), // ✅ Zod 4: no params
|
|
422
424
|
total: z.number(),
|
|
423
425
|
createdAt: z.string().datetime(), // ✅ Zod 4: no params
|
|
424
426
|
})
|
|
427
|
+
|
|
428
|
+
export type OrderCreatedData = z.infer<typeof OrderCreatedSchema>
|
|
429
|
+
|
|
430
|
+
export const OrdersCreatedContract = createContract({
|
|
431
|
+
type: 'order.created',
|
|
432
|
+
channel: { stream: 'ORDERS' },
|
|
433
|
+
schema: OrderCreatedSchema, // ← Singular schema
|
|
434
|
+
})
|
|
425
435
|
```
|
|
426
436
|
|
|
437
|
+
**Pattern:** Schema is singular (describes one object), Contract is plural (represents stream)
|
|
438
|
+
|
|
427
439
|
**Zod 4 Quick Reference:**
|
|
428
440
|
- ✅ `z.string().email()` - no params
|
|
429
441
|
- ✅ `z.string().url()` - no params
|
|
@@ -437,14 +449,23 @@ export const OrdersCreatedSchema = z.object({
|
|
|
437
449
|
|
|
438
450
|
| Component | Format | Example | Reason |
|
|
439
451
|
|-----------|--------|---------|--------|
|
|
440
|
-
| **Event Type** | **Singular** | `customer.created` |
|
|
441
|
-
| **
|
|
442
|
-
| **
|
|
443
|
-
| **
|
|
452
|
+
| **Event Type** | **Singular** | `customer.created` | A single fact |
|
|
453
|
+
| **Schema** | **Singular** | `CustomerCreatedSchema` | One event object |
|
|
454
|
+
| **Type** | **Singular** | `CustomerCreatedData` | One event object |
|
|
455
|
+
| **Contract** | **PLURAL** | `CustomersCreatedContract` | Category of events |
|
|
456
|
+
| **Event Folder** | **PLURAL** | `customers/` | Domain (collection) |
|
|
457
|
+
| **Stream** | **PLURAL** | `CUSTOMERS` | Collection of events |
|
|
458
|
+
| **Mock Files** | **PLURAL folder** | `customers/created.mock.json` | Matches event folder |
|
|
459
|
+
|
|
460
|
+
**The final rule:**
|
|
461
|
+
```
|
|
462
|
+
Schema / Data → singular (one event instance)
|
|
463
|
+
Event Type → singular (one fact)
|
|
464
|
+
Contract → plural (category of events)
|
|
465
|
+
Stream / Domain → plural (collection)
|
|
466
|
+
```
|
|
444
467
|
|
|
445
|
-
**
|
|
446
|
-
- **Singular** describes an event
|
|
447
|
-
- **Plural** describes a domain
|
|
468
|
+
**Key principle:** Contracts represent event collections, Schemas represent event instances.
|
|
448
469
|
|
|
449
470
|
### File Structure Example
|
|
450
471
|
|
|
@@ -466,36 +487,42 @@ packages/contracts/src/events/
|
|
|
466
487
|
|
|
467
488
|
### Contract Examples
|
|
468
489
|
|
|
469
|
-
|
|
490
|
+
| Event Type | Schema | Type | Contract | Folder | Stream |
|
|
491
|
+
|------------|--------|------|----------|--------|--------|
|
|
492
|
+
| `customer.created` | `CustomerCreatedSchema` | `CustomerCreatedData` | `CustomersCreatedContract` | `customers/` | `CUSTOMERS` |
|
|
493
|
+
| `order.created` | `OrderCreatedSchema` | `OrderCreatedData` | `OrdersCreatedContract` | `orders/` | `ORDERS` |
|
|
494
|
+
| `domain.created` | `DomainCreatedSchema` | `DomainCreatedData` | `DomainsCreatedContract` | `domains/` | `DOMAINS` |
|
|
470
495
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
| `order.created` | `OrderCreatedContract` | `orders/` | `ORDERS` ✅ |
|
|
475
|
-
| `domain.created` | `DomainCreatedContract` | `domains/` | `DOMAINS` ✅ |
|
|
496
|
+
**Naming Patterns:**
|
|
497
|
+
- Schema/Type: `{SingularDomain}{Action}Schema/Data` - describes one object
|
|
498
|
+
- Contract: `{PluralDomain}{Action}Contract` - matches folder and stream names
|
|
476
499
|
|
|
477
500
|
```typescript
|
|
478
501
|
// ✅ CORRECT
|
|
479
|
-
export const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
502
|
+
export const CustomerCreatedSchema = z.object({ ... })
|
|
503
|
+
export type CustomerCreatedData = z.infer<typeof CustomerCreatedSchema>
|
|
504
|
+
|
|
505
|
+
export const CustomersCreatedContract = createContract({
|
|
506
|
+
type: 'customer.created', // Singular event type
|
|
507
|
+
channel: { stream: 'CUSTOMERS' }, // Plural stream (matches contract name prefix)
|
|
508
|
+
schema: CustomerCreatedSchema, // Singular schema
|
|
483
509
|
})
|
|
484
510
|
|
|
485
511
|
// Folder: packages/contracts/src/events/customers/created.ts ✅ PLURAL
|
|
486
512
|
// Mock: packages/contracts/src/events/customers/created.mock.json ✅ PLURAL
|
|
487
513
|
|
|
488
|
-
// ❌ WRONG - Singular
|
|
514
|
+
// ❌ WRONG - Singular domain in contract name
|
|
489
515
|
export const CustomerCreatedContract = createContract({
|
|
490
516
|
type: 'customer.created',
|
|
491
|
-
channel: { stream: '
|
|
517
|
+
channel: { stream: 'CUSTOMERS' }, // Mismatch: plural stream, singular contract
|
|
492
518
|
schema: CustomerCreatedSchema,
|
|
493
519
|
})
|
|
494
520
|
|
|
495
|
-
// ❌ WRONG -
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
521
|
+
// ❌ WRONG - Singular stream
|
|
522
|
+
export const CustomersCreatedContract = createContract({
|
|
523
|
+
type: 'customer.created',
|
|
524
|
+
channel: { stream: 'CUSTOMER' }, // ❌ Wrong! Should be CUSTOMERS (plural)
|
|
525
|
+
schema: CustomerCreatedSchema,
|
|
499
526
|
})
|
|
500
527
|
```
|
|
501
528
|
|
|
@@ -511,7 +538,7 @@ consumeJetStreams({
|
|
|
511
538
|
discover: './src/events/**/*.handler.ts',
|
|
512
539
|
})
|
|
513
540
|
|
|
514
|
-
// ✅ CORRECT -
|
|
541
|
+
// ✅ CORRECT - order.created event → ORDERS stream
|
|
515
542
|
consumeJetStreams({
|
|
516
543
|
streams: ['ORDERS'], // ✅ Plural!
|
|
517
544
|
consumer: 'my-service',
|
|
@@ -595,11 +622,12 @@ packages/contracts/src/events/
|
|
|
595
622
|
- ❌ Use `src/handlers/` (use `src/events/`)
|
|
596
623
|
- ❌ Create `src/types/` directory (use contracts)
|
|
597
624
|
- ❌ Insert semicolons
|
|
598
|
-
- ❌
|
|
625
|
+
- ❌ Create contracts without adding exports to `packages/contracts/src/index.ts`
|
|
599
626
|
- ❌ Create `use-cases/` folder in NestJS (use Services instead)
|
|
600
627
|
|
|
601
628
|
**DO:**
|
|
602
629
|
- ✅ Contracts in `packages/contracts/src/events/`
|
|
630
|
+
- ✅ **ALWAYS add contract exports to `packages/contracts/src/index.ts`** (CRITICAL!)
|
|
603
631
|
- ✅ Handlers in `src/events/*.handler.ts`
|
|
604
632
|
- ✅ Hono: Use-cases in `src/use-cases/*.use-case.ts`
|
|
605
633
|
- ✅ NestJS: Business logic in Services + pure helper functions
|
|
@@ -12,7 +12,7 @@ This package contains **shared event definitions** (contracts) for events that a
|
|
|
12
12
|
src/
|
|
13
13
|
├── events/ # Event contracts grouped by domain
|
|
14
14
|
│ ├── orders/ # Orders domain
|
|
15
|
-
│ │ ├── created.ts #
|
|
15
|
+
│ │ ├── created.ts # order.created event
|
|
16
16
|
│ │ ├── updated.ts # orders.updated event
|
|
17
17
|
│ │ └── index.ts # Re-exports
|
|
18
18
|
│ ├── customers/ # Customers domain
|
|
@@ -35,7 +35,7 @@ src/
|
|
|
35
35
|
### Using the CLI (Recommended)
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
pf event add
|
|
38
|
+
pf event add product.created --fields "productId:string,name:string,price:number"
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
This will create `src/events/products/created.ts` with proper domain structure.
|
|
@@ -50,9 +50,9 @@ See [Adding Events](#manual-creation) section below.
|
|
|
50
50
|
|
|
51
51
|
```ts
|
|
52
52
|
import { handleEvent } from '@crossdelta/cloudevents'
|
|
53
|
-
import { OrdersCreatedContract, type
|
|
53
|
+
import { OrdersCreatedContract, type OrderCreatedData } from '{{scope}}/contracts'
|
|
54
54
|
|
|
55
|
-
export default handleEvent(OrdersCreatedContract, async (data:
|
|
55
|
+
export default handleEvent(OrdersCreatedContract, async (data: OrderCreatedData) => {
|
|
56
56
|
// data is fully typed from contract
|
|
57
57
|
console.log(data.orderId, data.customerId)
|
|
58
58
|
})
|
|
@@ -76,9 +76,9 @@ await publish(OrdersCreatedContract, {
|
|
|
76
76
|
### In Use-Cases
|
|
77
77
|
|
|
78
78
|
```ts
|
|
79
|
-
import type {
|
|
79
|
+
import type { OrderCreatedData } from '{{scope}}/contracts'
|
|
80
80
|
|
|
81
|
-
export const processOrder = async (data:
|
|
81
|
+
export const processOrder = async (data: OrderCreatedData) => {
|
|
82
82
|
// Use typed event data
|
|
83
83
|
}
|
|
84
84
|
```
|
|
@@ -89,10 +89,10 @@ Contracts are **auto-generated** when you create event handlers:
|
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
91
|
# 1. Create service with event handler
|
|
92
|
-
pf new hono-micro notifications --ai -d "Sends emails on
|
|
92
|
+
pf new hono-micro notifications --ai -d "Sends emails on order.created events"
|
|
93
93
|
|
|
94
94
|
# 2. Add event (creates contract, mock, handler)
|
|
95
|
-
pf event add
|
|
95
|
+
pf event add order.created --service services/notifications
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
This creates:
|
|
@@ -107,7 +107,7 @@ This creates:
|
|
|
107
107
|
import { createContract } from '@crossdelta/cloudevents'
|
|
108
108
|
import { z } from 'zod'
|
|
109
109
|
|
|
110
|
-
export const
|
|
110
|
+
export const OrderCreatedSchema = z.object({
|
|
111
111
|
orderId: z.string(),
|
|
112
112
|
customerId: z.string(),
|
|
113
113
|
total: z.number(),
|
|
@@ -119,17 +119,17 @@ export const OrdersCreatedSchema = z.object({
|
|
|
119
119
|
})
|
|
120
120
|
|
|
121
121
|
export const OrdersCreatedContract = createContract({
|
|
122
|
-
type: '
|
|
122
|
+
type: 'order.created',
|
|
123
123
|
channel: { stream: 'ORDERS' }, // Stream routing metadata
|
|
124
|
-
schema:
|
|
124
|
+
schema: OrderCreatedSchema,
|
|
125
125
|
})
|
|
126
126
|
|
|
127
|
-
export type
|
|
127
|
+
export type OrderCreatedData = z.infer<typeof OrdersCreatedContract.schema>
|
|
128
128
|
```
|
|
129
129
|
|
|
130
130
|
**Channel Metadata:**
|
|
131
131
|
- `stream` - NATS JetStream stream name (e.g., `ORDERS`)
|
|
132
|
-
- `subject` - Optional, defaults to event type (e.g., `
|
|
132
|
+
- `subject` - Optional, defaults to event type (e.g., `order.created`)
|
|
133
133
|
|
|
134
134
|
**Stream Materialization:**
|
|
135
135
|
1. **Development**: `pf dev` scans contracts and auto-creates ephemeral streams from channel metadata
|
|
@@ -144,10 +144,10 @@ See [`infra/streams/README.md`](../../infra/streams/README.md) for details.
|
|
|
144
144
|
pf event list
|
|
145
145
|
|
|
146
146
|
# Publish mock event
|
|
147
|
-
pf event publish
|
|
147
|
+
pf event publish order.created
|
|
148
148
|
|
|
149
149
|
# Publish with custom data
|
|
150
|
-
pf event publish
|
|
150
|
+
pf event publish order.created --data '{"orderId":"test-123"}'
|
|
151
151
|
```
|
|
152
152
|
|
|
153
153
|
## Guidelines
|
|
@@ -161,6 +161,6 @@ pf event publish orders.created --data '{"orderId":"test-123"}'
|
|
|
161
161
|
|
|
162
162
|
**Naming conventions:**
|
|
163
163
|
- Contracts: `OrdersCreatedContract` (plural namespace)
|
|
164
|
-
- Types: `
|
|
164
|
+
- Types: `OrderCreatedData`
|
|
165
165
|
- Files: `orders-created.ts`
|
|
166
|
-
- Event types: `
|
|
166
|
+
- Event types: `order.created` (plural namespace, dot notation)
|