@crossdelta/platform-sdk 0.12.0 → 0.13.1
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 +22 -7
- package/bin/cli.js +144 -136
- package/bin/docs/generators/hono-bun.md +70 -26
- package/bin/docs/generators/hono-node.md +76 -26
- package/bin/docs/generators/nest.md +25 -7
- 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/.github/workflows/build-and-deploy.yml.hbs +0 -3
- package/bin/templates/workspace/.github/workflows/publish-packages.yml +1 -2
- 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 +1 -1
- 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
|
@@ -7,26 +7,223 @@
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
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
|
+
|
|
10
207
|
## 🚨 CRITICAL: @crossdelta/cloudevents API
|
|
11
208
|
|
|
12
209
|
**ONLY these exports exist:**
|
|
13
|
-
- `
|
|
14
|
-
- `
|
|
210
|
+
- `consumeJetStreams` (preferred) - Consume from multiple streams
|
|
211
|
+
- `consumeJetStreamEvents` (legacy) - Consume from single stream
|
|
15
212
|
- `handleEvent`, `publish`, `createContract`
|
|
16
213
|
|
|
17
214
|
```ts
|
|
18
|
-
|
|
19
|
-
streams: [{ stream: 'ORDERS', subjects: ['orders.*'] }]
|
|
20
|
-
})
|
|
21
|
-
|
|
215
|
+
// Services only consume (DO NOT use ensureJetStreams!)
|
|
22
216
|
consumeJetStreams({
|
|
23
217
|
streams: ['ORDERS'],
|
|
24
218
|
consumer: 'my-service',
|
|
25
|
-
discover: './src/events/**/*.
|
|
219
|
+
discover: './src/events/**/*.handler.ts',
|
|
26
220
|
})
|
|
27
221
|
```
|
|
28
222
|
|
|
29
|
-
**DO NOT USE
|
|
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!
|
|
30
227
|
|
|
31
228
|
---
|
|
32
229
|
|
|
@@ -49,6 +246,16 @@ The exact command depends on the framework - see the framework-specific docs:
|
|
|
49
246
|
|
|
50
247
|
**Without this command, the service cannot be built!**
|
|
51
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
|
+
|
|
52
259
|
---
|
|
53
260
|
|
|
54
261
|
## 🚨 CRITICAL: Post-Commands Block (REQUIRED for Event Consumers)
|
|
@@ -74,22 +281,23 @@ pf event add customers.updated --service services/my-service
|
|
|
74
281
|
|
|
75
282
|
### What YOU (the AI) generate:
|
|
76
283
|
|
|
77
|
-
1. **`packages/contracts/src/events/<event>.ts`** - Contract with correct schema fields
|
|
78
|
-
2. **`src/
|
|
79
|
-
3. **`src/
|
|
80
|
-
4. **`src/use-cases/*.
|
|
81
|
-
5. **`
|
|
82
|
-
|
|
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!
|
|
83
291
|
|
|
84
292
|
### Handler Location (CRITICAL!)
|
|
85
293
|
|
|
86
|
-
**⚠️ MUST be in `src/events/*.
|
|
294
|
+
**⚠️ MUST be in `src/events/*.handler.ts` - NEVER in `handlers/` subdirectory**
|
|
87
295
|
|
|
88
296
|
```
|
|
89
297
|
✅ CORRECT:
|
|
90
298
|
services/my-service/src/events/
|
|
91
|
-
├── orders-created.
|
|
92
|
-
└── customers-updated.
|
|
299
|
+
├── orders-created.handler.ts
|
|
300
|
+
└── customers-updated.handler.ts
|
|
93
301
|
|
|
94
302
|
❌ WRONG:
|
|
95
303
|
services/my-service/src/events/handlers/ # NEVER create!
|
|
@@ -126,7 +334,7 @@ export default handleEvent(OrdersCreatedContract, async (data) => {
|
|
|
126
334
|
pf new hono-micro services/my-service -y
|
|
127
335
|
```
|
|
128
336
|
|
|
129
|
-
#### `packages/contracts/src/events/orders
|
|
337
|
+
#### `packages/contracts/src/events/orders/created.ts`
|
|
130
338
|
```ts
|
|
131
339
|
import { createContract } from '@crossdelta/cloudevents'
|
|
132
340
|
import { z } from 'zod'
|
|
@@ -138,13 +346,14 @@ export const OrdersCreatedSchema = z.object({
|
|
|
138
346
|
|
|
139
347
|
export const OrdersCreatedContract = createContract({
|
|
140
348
|
type: 'orders.created',
|
|
349
|
+
channel: { stream: 'ORDERS' },
|
|
141
350
|
schema: OrdersCreatedSchema,
|
|
142
351
|
})
|
|
143
352
|
|
|
144
353
|
export type OrdersCreatedData = z.infer<typeof OrdersCreatedContract.schema>
|
|
145
354
|
```
|
|
146
355
|
|
|
147
|
-
#### `src/events/orders-created.
|
|
356
|
+
#### `src/events/orders-created.handler.ts`
|
|
148
357
|
```ts
|
|
149
358
|
import { handleEvent } from '@crossdelta/cloudevents'
|
|
150
359
|
import { OrdersCreatedContract, type OrdersCreatedData } from '{{scope}}/contracts'
|
|
@@ -216,11 +425,79 @@ export const OrdersCreatedSchema = z.object({
|
|
|
216
425
|
|
|
217
426
|
## Naming Convention
|
|
218
427
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
|
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
|
+
```
|
|
222
499
|
|
|
223
|
-
|
|
500
|
+
**Rule:** Extract the stream name from your contract's `channel.stream` field and use it EXACTLY in `consumeJetStreams({ streams: ['...'] })`.
|
|
224
501
|
|
|
225
502
|
---
|
|
226
503
|
|
|
@@ -253,7 +530,7 @@ Combines REST endpoints + NATS consumer.
|
|
|
253
530
|
```
|
|
254
531
|
services/my-service/src/
|
|
255
532
|
├── index.ts
|
|
256
|
-
├── events/orders-created.
|
|
533
|
+
├── events/orders-created.handler.ts
|
|
257
534
|
└── use-cases/
|
|
258
535
|
├── process-order.use-case.ts
|
|
259
536
|
└── process-order.test.ts
|
|
@@ -279,7 +556,7 @@ packages/contracts/src/events/
|
|
|
279
556
|
|
|
280
557
|
**DO:**
|
|
281
558
|
- ✅ Contracts in `packages/contracts/src/events/`
|
|
282
|
-
- ✅ Handlers in `src/events/*.
|
|
559
|
+
- ✅ Handlers in `src/events/*.handler.ts`
|
|
283
560
|
- ✅ Hono: Use-cases in `src/use-cases/*.use-case.ts`
|
|
284
561
|
- ✅ NestJS: Business logic in Services + pure helper functions
|
|
285
562
|
- ✅ Log event type and key identifier in handlers
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// IMPORTANT: telemetry must be imported first to patch modules before they're loaded
|
|
2
|
+
import '@crossdelta/telemetry'
|
|
3
|
+
|
|
4
|
+
import { Hono } from 'hono'
|
|
5
|
+
|
|
6
|
+
const port = Number(process.env.{{envKey}}_PORT) || 8080
|
|
7
|
+
const app = new Hono()
|
|
8
|
+
|
|
9
|
+
app.get('/health', (c) => {
|
|
10
|
+
return c.json({ status: 'ok' })
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
Bun.serve({
|
|
14
|
+
port,
|
|
15
|
+
fetch: app.fetch,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
console.log(`🚀 Service ready at http://localhost:${port}`)
|
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import { Injectable, Logger } from '@nestjs/common'
|
|
2
|
-
import {
|
|
2
|
+
import { consumeJetStreams } from '@crossdelta/cloudevents'
|
|
3
3
|
|
|
4
4
|
@Injectable()
|
|
5
5
|
export class EventsService {
|
|
6
6
|
private readonly logger = new Logger(EventsService.name)
|
|
7
7
|
|
|
8
8
|
async startConsumers(): Promise<void> {
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// streams: [
|
|
13
|
-
// { stream: 'ORDERS', subjects: ['orders.*'] },
|
|
14
|
-
// ],
|
|
15
|
-
// })
|
|
9
|
+
// Services NEVER create streams!
|
|
10
|
+
// - Development: pf dev auto-creates ephemeral streams from contracts
|
|
11
|
+
// - Production: Pulumi materializes persistent streams
|
|
16
12
|
|
|
17
|
-
//
|
|
13
|
+
// Uncomment and configure when you have events:
|
|
14
|
+
// consumeJetStreams({
|
|
18
15
|
// streams: ['ORDERS'],
|
|
19
16
|
// consumer: '{{serviceName}}',
|
|
20
17
|
// discover: './src/events/**/*.event.ts',
|
|
21
18
|
// })
|
|
22
19
|
|
|
23
|
-
this.logger.log('Event consumers ready
|
|
20
|
+
this.logger.log('Event consumers ready')
|
|
24
21
|
}
|
|
25
22
|
}
|
|
@@ -8,7 +8,7 @@ import { AppModule } from './app.module'
|
|
|
8
8
|
import { setAppContext } from './app.context'
|
|
9
9
|
import { EventsService } from './events/events.service'
|
|
10
10
|
|
|
11
|
-
const port = Number(process.env.
|
|
11
|
+
const port = Number(process.env.{{envKey}}_PORT) || {{defaultPort}}
|
|
12
12
|
const serviceName = '{{displayName}}'
|
|
13
13
|
|
|
14
14
|
const logger = new ConsoleLogger({
|
|
@@ -214,9 +214,6 @@ jobs:
|
|
|
214
214
|
cache-restore-keys: bun-$\{{ runner.os }}-
|
|
215
215
|
npm-token: $\{{ secrets.NPM_TOKEN }}
|
|
216
216
|
|
|
217
|
-
- name: Build infrastructure package
|
|
218
|
-
run: bun run --cwd packages/infrastructure build
|
|
219
|
-
|
|
220
217
|
- name: Pulumi up
|
|
221
218
|
uses: pulumi/actions@v6
|
|
222
219
|
with:
|
|
@@ -126,8 +126,7 @@ jobs:
|
|
|
126
126
|
fi
|
|
127
127
|
|
|
128
128
|
- name: Build
|
|
129
|
-
|
|
130
|
-
run: bun run --if-present build
|
|
129
|
+
run: bunx turbo run build --filter=${{ matrix.package.name }}
|
|
131
130
|
|
|
132
131
|
- name: Test
|
|
133
132
|
working-directory: ${{ matrix.package.dir }}
|
|
File without changes
|
|
@@ -10,18 +10,39 @@ This package contains **shared event definitions** (contracts) for events that a
|
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
src/
|
|
13
|
-
├── events/
|
|
14
|
-
│ ├── orders
|
|
15
|
-
│ ├──
|
|
16
|
-
│
|
|
17
|
-
└── index.ts
|
|
13
|
+
├── events/ # Event contracts grouped by domain
|
|
14
|
+
│ ├── orders/ # Orders domain
|
|
15
|
+
│ │ ├── created.ts # orders.created event
|
|
16
|
+
│ │ ├── updated.ts # orders.updated event
|
|
17
|
+
│ │ └── index.ts # Re-exports
|
|
18
|
+
│ ├── customers/ # Customers domain
|
|
19
|
+
│ │ ├── updated.ts # customers.updated event
|
|
20
|
+
│ │ └── index.ts # Re-exports
|
|
21
|
+
│ └── index.ts # Re-exports all domains
|
|
22
|
+
├── stream-policies.ts # NATS JetStream retention policies
|
|
23
|
+
└── index.ts # Main export file
|
|
18
24
|
```
|
|
19
25
|
|
|
20
26
|
**Contract files contain:**
|
|
21
27
|
- Zod schema definition
|
|
22
|
-
- Contract object (type + schema)
|
|
28
|
+
- Contract object (type + schema + **channel metadata**)
|
|
23
29
|
- TypeScript type inference
|
|
24
|
-
|
|
30
|
+
|
|
31
|
+
**Channel metadata** defines which NATS JetStream stream the event belongs to. This enables infrastructure-as-code workflows where streams are auto-created in dev (`pf dev`) and materialized via Pulumi in production.
|
|
32
|
+
|
|
33
|
+
## Adding New Events
|
|
34
|
+
|
|
35
|
+
### Using the CLI (Recommended)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pf event add products.created --fields "productId:string,name:string,price:number"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This will create `src/events/products/created.ts` with proper domain structure.
|
|
42
|
+
|
|
43
|
+
### Manual Creation
|
|
44
|
+
|
|
45
|
+
See [Adding Events](#manual-creation) section below.
|
|
25
46
|
|
|
26
47
|
## Usage
|
|
27
48
|
|
|
@@ -82,7 +103,7 @@ This creates:
|
|
|
82
103
|
### Manual Contract Creation
|
|
83
104
|
|
|
84
105
|
```ts
|
|
85
|
-
// packages/contracts/src/events/orders
|
|
106
|
+
// packages/contracts/src/events/orders/created.ts
|
|
86
107
|
import { createContract } from '@crossdelta/cloudevents'
|
|
87
108
|
import { z } from 'zod'
|
|
88
109
|
|
|
@@ -99,12 +120,23 @@ export const OrdersCreatedSchema = z.object({
|
|
|
99
120
|
|
|
100
121
|
export const OrdersCreatedContract = createContract({
|
|
101
122
|
type: 'orders.created',
|
|
123
|
+
channel: { stream: 'ORDERS' }, // Stream routing metadata
|
|
102
124
|
schema: OrdersCreatedSchema,
|
|
103
125
|
})
|
|
104
126
|
|
|
105
127
|
export type OrdersCreatedData = z.infer<typeof OrdersCreatedContract.schema>
|
|
106
128
|
```
|
|
107
129
|
|
|
130
|
+
**Channel Metadata:**
|
|
131
|
+
- `stream` - NATS JetStream stream name (e.g., `ORDERS`)
|
|
132
|
+
- `subject` - Optional, defaults to event type (e.g., `orders.created`)
|
|
133
|
+
|
|
134
|
+
**Stream Materialization:**
|
|
135
|
+
1. **Development**: `pf dev` scans contracts and auto-creates ephemeral streams from channel metadata
|
|
136
|
+
2. **Production**: Pulumi collects streams from contracts and materializes with retention policies
|
|
137
|
+
|
|
138
|
+
See [`infra/streams/README.md`](../../infra/streams/README.md) for details.
|
|
139
|
+
|
|
108
140
|
## Testing with Mocks
|
|
109
141
|
|
|
110
142
|
```bash
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Contracts Index
|
|
3
|
+
*
|
|
4
|
+
* Re-exports all event contracts for convenient importing.
|
|
5
|
+
* Services can import from '{{scope}}/contracts' instead of deep imports.
|
|
6
|
+
*
|
|
7
|
+
* Structure:
|
|
8
|
+
* - events/<domain>/<event>.ts - Individual event contracts
|
|
9
|
+
* - events/<domain>/index.ts - Domain-specific exports
|
|
10
|
+
* - This file: Re-exports all domains
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Add your event domains here as you create them:
|
|
14
|
+
// export * from './orders'
|
|
15
|
+
// export * from './customers'
|
|
16
|
+
// export * from './products'
|
|
@@ -1 +1,10 @@
|
|
|
1
1
|
// Export your event contracts and schemas here
|
|
2
|
+
// Use the CLI to generate contracts:
|
|
3
|
+
//
|
|
4
|
+
// pf event add orders.created --fields "orderId:string,total:number"
|
|
5
|
+
//
|
|
6
|
+
// This will create: src/events/orders/created.ts
|
|
7
|
+
// Then uncomment the exports below:
|
|
8
|
+
|
|
9
|
+
// export * from './events'
|
|
10
|
+
// export * from './stream-policies'
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{workspaceName}} Stream Policies
|
|
3
|
+
*
|
|
4
|
+
* Business rules for NATS JetStream stream configuration.
|
|
5
|
+
* Defines retention, replication, and storage policies per stream.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - This file: Business rules (data)
|
|
9
|
+
* - @crossdelta/infrastructure: Deployment logic
|
|
10
|
+
* - infra/streams: Connects business rules + deployment logic
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { StreamPolicy } from '@crossdelta/infrastructure'
|
|
14
|
+
|
|
15
|
+
// Time constants for readability
|
|
16
|
+
const DAYS = 24 * 60 * 60 * 1000
|
|
17
|
+
const MB = 1024 * 1024
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Stream-specific policies
|
|
21
|
+
* Add entries here as you create new streams via event contracts
|
|
22
|
+
*/
|
|
23
|
+
export const STREAM_POLICIES: Record<string, Partial<StreamPolicy>> = {
|
|
24
|
+
// Example:
|
|
25
|
+
// ORDERS: {
|
|
26
|
+
// maxAge: 14 * DAYS,
|
|
27
|
+
// replicas: 3,
|
|
28
|
+
// },
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Default stream policy
|
|
33
|
+
* Applied to all streams without specific overrides
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_POLICY: Partial<StreamPolicy> = {
|
|
36
|
+
maxAge: 7 * DAYS, // 7 days default retention
|
|
37
|
+
storage: 'file', // Persistent storage
|
|
38
|
+
replicas: 1, // Single replica for non-critical streams
|
|
39
|
+
maxMsgSize: 1 * MB, // 1MB max message size
|
|
40
|
+
}
|