@crossdelta/platform-sdk 0.10.0 → 0.11.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.
- package/README.md +40 -3
- package/bin/cli.js +170 -42436
- package/bin/docs/generators/README.md +29 -20
- package/bin/docs/generators/code-style.md +96 -0
- package/bin/docs/generators/hono-bun.md +31 -45
- package/bin/docs/generators/hono-node.md +33 -57
- package/bin/docs/generators/nest.md +169 -107
- package/bin/docs/generators/service.md +133 -528
- package/bin/docs/generators/testing.md +97 -0
- package/bin/templates/nest-microservice/biome.json.hbs +1 -8
- package/bin/templates/workspace/biome.json.hbs +2 -1
- package/bin/templates/workspace/packages/contracts/package.json.hbs +1 -1
- package/package.json +128 -113
- package/bin/docs/generators/nest.md.new +0 -351
|
@@ -2,301 +2,65 @@
|
|
|
2
2
|
|
|
3
3
|
**Generic guidelines for all service types. Framework-specific details are in separate files:**
|
|
4
4
|
- `hono-bun.md` - Hono with Bun runtime
|
|
5
|
-
- `hono-
|
|
6
|
-
pf event add orders.created --service services/my-notifications
|
|
7
|
-
pf event add customers.updated --service services/my-notifications
|
|
8
|
-
```md` - Hono with Node.js runtime (tsx + @hono/node-server)
|
|
5
|
+
- `hono-node.md` - Hono with Node.js runtime
|
|
9
6
|
- `nest.md` - NestJS framework
|
|
10
7
|
|
|
11
8
|
---
|
|
12
9
|
|
|
13
10
|
## 🚨 CRITICAL: @crossdelta/cloudevents API
|
|
14
11
|
|
|
15
|
-
**ONLY these exports exist
|
|
16
|
-
- `
|
|
17
|
-
- `
|
|
18
|
-
- `
|
|
19
|
-
|
|
20
|
-
- `handleEvent` - create event handler
|
|
21
|
-
- `publish` - publish events
|
|
22
|
-
- `createContract` - create shared event contract
|
|
23
|
-
|
|
24
|
-
**DO NOT USE (these do NOT exist):**
|
|
25
|
-
- ❌ `stream` - DOES NOT EXIST
|
|
26
|
-
- ❌ `consumer` - DOES NOT EXIST
|
|
27
|
-
- ❌ `subscribe` - DOES NOT EXIST
|
|
28
|
-
- ❌ Any other function not listed above
|
|
29
|
-
|
|
30
|
-
**EXACT function signatures (use batch versions for multiple streams):**
|
|
12
|
+
**ONLY these exports exist:**
|
|
13
|
+
- `ensureJetStreams` / `consumeJetStreams` (preferred)
|
|
14
|
+
- `ensureJetStreamStream` / `consumeJetStreamEvents` (legacy)
|
|
15
|
+
- `handleEvent`, `publish`, `createContract`
|
|
16
|
+
|
|
31
17
|
```ts
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
streams: [
|
|
35
|
-
{ stream: 'ORDERS', subjects: ['orders.*'] },
|
|
36
|
-
{ stream: 'CUSTOMERS', subjects: ['customers.*'] },
|
|
37
|
-
]
|
|
18
|
+
await ensureJetStreams({
|
|
19
|
+
streams: [{ stream: 'ORDERS', subjects: ['orders.*'] }]
|
|
38
20
|
})
|
|
39
21
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
streams: ['ORDERS', 'CUSTOMERS'],
|
|
22
|
+
consumeJetStreams({
|
|
23
|
+
streams: ['ORDERS'],
|
|
43
24
|
consumer: 'my-service',
|
|
44
25
|
discover: './src/events/**/*.event.ts',
|
|
45
26
|
})
|
|
46
27
|
```
|
|
47
28
|
|
|
48
|
-
**
|
|
49
|
-
- ❌ `name:` - use `stream:` instead
|
|
50
|
-
- ❌ `handler:` - use `discover:` instead (auto-discovery pattern)
|
|
51
|
-
- ❌ `topic:` - use `subjects:` instead
|
|
29
|
+
**DO NOT USE (these do NOT exist):** `stream`, `consumer`, `subscribe`, `name:`, `handler:`, `topic:`
|
|
52
30
|
|
|
53
31
|
---
|
|
54
32
|
|
|
55
33
|
## 🚨 CRITICAL: @crossdelta/telemetry Import
|
|
56
34
|
|
|
57
|
-
**Telemetry is a side-effect import. Use EXACTLY:**
|
|
58
35
|
```ts
|
|
59
|
-
import '@crossdelta/telemetry'
|
|
36
|
+
import '@crossdelta/telemetry' // side-effect, MUST be FIRST import
|
|
60
37
|
```
|
|
61
38
|
|
|
62
|
-
**DO NOT USE:**
|
|
63
|
-
- ❌ `@crossdelta/telemetry/register` - DOES NOT EXIST
|
|
64
|
-
- ❌ `import { ... } from '@crossdelta/telemetry'` - No named exports needed
|
|
65
|
-
- ❌ Any subpath imports
|
|
39
|
+
**DO NOT USE:** `@crossdelta/telemetry/register`, named exports
|
|
66
40
|
|
|
67
41
|
---
|
|
68
42
|
|
|
69
|
-
## 🚨 CRITICAL:
|
|
70
|
-
|
|
71
|
-
**FOR EVENT CONSUMER SERVICES (services that "consume", "listen to", "react to" events):**
|
|
72
|
-
|
|
73
|
-
### What YOU (the AI) generate for Event Consumers:
|
|
74
|
-
|
|
75
|
-
1. **`packages/contracts/src/events/<event>.ts`** - Contract with **correct schema fields**
|
|
76
|
-
2. **`src/index.ts`** - Entry point with NATS stream/consumer setup
|
|
77
|
-
3. **`src/events/<event>.event.ts`** - Event handler that calls the use-case
|
|
78
|
-
4. **`src/use-cases/*.use-case.ts`** - Business logic (called by handlers)
|
|
79
|
-
5. **`src/use-cases/*.test.ts`** - Tests for use-cases
|
|
80
|
-
6. **`README.md`** - Documentation
|
|
81
|
-
|
|
82
|
-
The `pf event add` commands in `post-commands` will create:
|
|
83
|
-
- Mock data for testing
|
|
84
|
-
- Index exports (if not already present)
|
|
85
|
-
|
|
86
|
-
### Event Handler Template (YOU write this!)
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
// src/events/orders-created.event.ts
|
|
90
|
-
import { handleEvent } from '@crossdelta/cloudevents'
|
|
91
|
-
import { OrdersCreatedContract, type OrdersCreatedData } from '{{scope}}/contracts'
|
|
92
|
-
import { sendOrderNotification } from '../use-cases/send-order-notification.use-case'
|
|
93
|
-
|
|
94
|
-
export default handleEvent(OrdersCreatedContract, async (data: OrdersCreatedData) => {
|
|
95
|
-
await sendOrderNotification(data)
|
|
96
|
-
})
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
**Rules:**
|
|
100
|
-
- Handler imports the Contract from `{{scope}}/contracts`
|
|
101
|
-
- Handler calls the corresponding use-case function
|
|
102
|
-
- Handler is thin - NO business logic, just delegation
|
|
103
|
-
- Use-case name should match the handler purpose (e.g., `send-order-notification` for a notification service)
|
|
104
|
-
|
|
105
|
-
### Handler Location & Structure (CRITICAL!)
|
|
106
|
-
|
|
107
|
-
**⚠️ MUST be in `src/events/*.event.ts` - NEVER in `handlers/` subdirectory**
|
|
108
|
-
|
|
109
|
-
```
|
|
110
|
-
✅ CORRECT:
|
|
111
|
-
services/my-service/src/events/
|
|
112
|
-
├── orders-created.event.ts # Handler here
|
|
113
|
-
├── customers-updated.event.ts # Handler here
|
|
114
|
-
└── events.service.ts
|
|
115
|
-
|
|
116
|
-
❌ WRONG (DO NOT CREATE):
|
|
117
|
-
services/my-service/src/events/
|
|
118
|
-
├── handlers/ # NEVER create this folder!
|
|
119
|
-
│ └── orders-created.handler.ts # Handlers NEVER here!
|
|
120
|
-
└── events.module.ts (with named imports)
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
**Handler Export Pattern:**
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
// ✅ CORRECT: Default export
|
|
127
|
-
export default handleEvent(OrdersCreatedContract, async (data) => {
|
|
128
|
-
// Handler code
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
// ❌ WRONG: Named export
|
|
132
|
-
export const OrdersCreatedHandler = handleEvent(...)
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
**Module Pattern:**
|
|
136
|
-
|
|
137
|
-
```typescript
|
|
138
|
-
// ✅ CORRECT: EventsModule only provides EventsService
|
|
139
|
-
import { Module } from '@nestjs/common'
|
|
140
|
-
import { EventsService } from './events.service'
|
|
141
|
-
|
|
142
|
-
@Module({
|
|
143
|
-
providers: [EventsService],
|
|
144
|
-
exports: [EventsService],
|
|
145
|
-
})
|
|
146
|
-
export class EventsModule {}
|
|
147
|
-
|
|
148
|
-
// ❌ WRONG: Importing handlers and registering as providers
|
|
149
|
-
import { OrdersCreatedHandler } from './handlers/orders-created.handler'
|
|
150
|
-
|
|
151
|
-
@Module({
|
|
152
|
-
providers: [EventsService, OrdersCreatedHandler], // ❌ WRONG!
|
|
153
|
-
})
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
**Auto-Discovery Mechanism:**
|
|
157
|
-
|
|
158
|
-
Handlers are **automatically discovered** by the EventsService via glob pattern. Do NOT manually import them:
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
// ✅ CORRECT: EventsService uses discover glob
|
|
162
|
-
await consumeJetStreamStreams({
|
|
163
|
-
streams: ['ORDERS', 'CUSTOMERS'],
|
|
164
|
-
consumer: 'my-service',
|
|
165
|
-
discover: './src/events/**/*.event.ts', // Auto-discovers all handlers
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
// ❌ WRONG: Manual handler imports not needed - discovery handles it!
|
|
169
|
-
import { OrdersCreatedHandler } from './events/orders-created.event' // Don't do this
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
**Common Mistakes (from real failures):**
|
|
173
|
-
|
|
174
|
-
| Mistake | Why It Fails | Solution |
|
|
175
|
-
|---------|-------------|----------|
|
|
176
|
-
| Create `/handlers` subdirectory | Breaks auto-discovery path; creates duplicate files | Keep ALL handlers in `src/events/*.event.ts` |
|
|
177
|
-
| Named exports: `export const OrdersCreatedHandler` | CLI doesn't recognize as handler; causes import/export mismatches | Use `export default handleEvent(...)` ALWAYS |
|
|
178
|
-
| Import handlers in `events.module.ts` | Auto-discovery doesn't require imports; causes TS2614 errors | Remove handler imports; let discover glob find them |
|
|
179
|
-
| Register handlers as module providers | Not a NestJS injectable; breaks DI container | Only register EventsService as provider |
|
|
180
|
-
|
|
181
|
-
**Real-World Example of the Error:**
|
|
182
|
-
|
|
183
|
-
```
|
|
184
|
-
// ❌ What went wrong in test-ws-jack
|
|
185
|
-
src/events/events.module.ts:
|
|
186
|
-
import { OrdersCreatedHandler } from './handlers/orders-created.handler' ← File doesn't exist with named export
|
|
187
|
-
|
|
188
|
-
Error: TS2614 [ERROR]: Module "/services/my-nest-service/src/events/orders-created.event.ts" has no exported member "OrdersCreatedHandler"
|
|
189
|
-
|
|
190
|
-
Reason: Handler is `export default handleEvent(...)`, NOT `export const OrdersCreatedHandler`
|
|
191
|
-
Folder structure should NOT have `/handlers` subdirectory
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
**Checklist Before Committing Handler Code:**
|
|
195
|
-
|
|
196
|
-
- [ ] All handlers in `src/events/*.event.ts` (NOT in subdirectories)
|
|
197
|
-
- [ ] Handler use `export default handleEvent(Contract, ...)`
|
|
198
|
-
- [ ] `events.module.ts` only provides `EventsService`
|
|
199
|
-
- [ ] `events.service.ts` uses `discover: './src/events/**/*.event.ts'`
|
|
200
|
-
- [ ] No `/handlers` subdirectory exists
|
|
201
|
-
- [ ] No handler imports in `events.module.ts`
|
|
43
|
+
## 🚨 CRITICAL: Commands Block (REQUIRED - MUST BE FIRST)
|
|
202
44
|
|
|
203
|
-
|
|
45
|
+
Your response **MUST START** with a `commands` block that scaffolds the service.
|
|
46
|
+
The exact command depends on the framework - see the framework-specific docs:
|
|
47
|
+
- `hono-bun.md` / `hono-node.md` → `pf new hono-micro`
|
|
48
|
+
- `nest.md` → `pf new nest-micro`
|
|
204
49
|
|
|
205
|
-
**
|
|
206
|
-
|
|
207
|
-
**⚠️ Zod 4 Syntax:** Use `z.string().email()` without parameters. For custom messages, use `.check()`:
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
// packages/contracts/src/events/orders-created.ts
|
|
211
|
-
import { createContract } from '@crossdelta/cloudevents'
|
|
212
|
-
import { z } from 'zod'
|
|
213
|
-
|
|
214
|
-
export const OrdersCreatedSchema = z.object({
|
|
215
|
-
orderId: z.string(),
|
|
216
|
-
customerId: z.string(),
|
|
217
|
-
email: z.string().email(), // ✅ Zod 4: no params
|
|
218
|
-
total: z.number(),
|
|
219
|
-
items: z.array(z.object({
|
|
220
|
-
productId: z.string(),
|
|
221
|
-
quantity: z.number(),
|
|
222
|
-
})),
|
|
223
|
-
createdAt: z.string().datetime(), // ✅ Zod 4: no params
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
export const OrdersCreatedContract = createContract({
|
|
227
|
-
type: 'orders.created',
|
|
228
|
-
schema: OrdersCreatedSchema,
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
export type OrdersCreatedData = z.infer<typeof OrdersCreatedContract.schema>
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
**Zod 4 Quick Reference:**
|
|
235
|
-
- ✅ `z.string().email()` - no params
|
|
236
|
-
- ✅ `z.string().url()` - no params
|
|
237
|
-
- ✅ `z.string().uuid()` - no params
|
|
238
|
-
- ✅ `z.string().datetime()` - no params
|
|
239
|
-
- ❌ `z.string().email('Invalid')` - DEPRECATED
|
|
240
|
-
|
|
241
|
-
### Naming Convention (CRITICAL!)
|
|
242
|
-
|
|
243
|
-
**Event Type → Contract Name → File Name:**
|
|
244
|
-
|
|
245
|
-
| Event Type | Contract Name | File Name |
|
|
246
|
-
|------------|---------------|-----------|
|
|
247
|
-
| `orders.created` | `OrdersCreatedContract` | `orders-created.ts` |
|
|
248
|
-
| `users.registered` | `UsersRegisteredContract` | `users-registered.ts` |
|
|
249
|
-
| `payments.completed` | `PaymentsCompletedContract` | `payments-completed.ts` |
|
|
250
|
-
| `customers.updated` | `CustomersUpdatedContract` | `customers-updated.ts` |
|
|
251
|
-
|
|
252
|
-
**⚠️ ALWAYS use PLURAL namespace:**
|
|
253
|
-
- ✅ `orders.created`, `customers.updated`, `payments.completed`
|
|
254
|
-
- ❌ `order.created`, `customer.updated`, `payment.completed`
|
|
255
|
-
|
|
256
|
-
This ensures JetStream stream names match: `ORDERS`, `CUSTOMERS`, `PAYMENTS`
|
|
257
|
-
|
|
258
|
-
---
|
|
259
|
-
|
|
260
|
-
## 🚨 CRITICAL: Commands Block (REQUIRED - MUST BE FIRST IN YOUR RESPONSE)
|
|
261
|
-
|
|
262
|
-
Your response **MUST** start with a commands block that scaffolds the service structure:
|
|
263
|
-
|
|
264
|
-
```commands
|
|
265
|
-
pf new hono-micro <service-path> -y
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
**Example:** If user asks for "orders service", generate:
|
|
269
|
-
```commands
|
|
270
|
-
pf new hono-micro services/orders -y
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
This command creates: `package.json`, `tsconfig.json`, `Dockerfile`, and basic structure.
|
|
274
|
-
**Without this command, the service cannot be built or run!**
|
|
275
|
-
|
|
276
|
-
- Services MUST be in `services/` directory
|
|
277
|
-
- User says "orders" → Generate path: `services/orders`
|
|
278
|
-
- Always include `-y` flag to skip prompts
|
|
50
|
+
**Without this command, the service cannot be built!**
|
|
279
51
|
|
|
280
52
|
---
|
|
281
53
|
|
|
282
|
-
## 🚨 CRITICAL: Post-
|
|
54
|
+
## 🚨 CRITICAL: Post-Commands Block (REQUIRED for Event Consumers)
|
|
283
55
|
|
|
284
|
-
**For Event Consumer services,
|
|
56
|
+
**For Event Consumer services, include a `post-commands` block with ALL events:**
|
|
285
57
|
|
|
286
|
-
Your response MUST include this block right after the files:
|
|
287
|
-
|
|
288
|
-
```post-commands
|
|
289
|
-
pf event add <event.type> --service <service-path>
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
**Example:** Service that listens to `orders.created` AND `customers.updated`:
|
|
293
58
|
```post-commands
|
|
294
|
-
pf event add orders.created --service services/
|
|
295
|
-
pf event add customers.updated --service services/
|
|
59
|
+
pf event add orders.created --service services/my-service
|
|
60
|
+
pf event add customers.updated --service services/my-service
|
|
296
61
|
```
|
|
297
62
|
|
|
298
63
|
**What `pf event add` creates:**
|
|
299
|
-
- `src/events/<event>.event.ts` - Handler file that imports your contract
|
|
300
64
|
- `packages/contracts/src/events/<event>.mock.json` - Test mock data
|
|
301
65
|
- Adds export to `packages/contracts/src/index.ts`
|
|
302
66
|
|
|
@@ -304,136 +68,66 @@ pf event add customers.updated --service services/notifications
|
|
|
304
68
|
|
|
305
69
|
---
|
|
306
70
|
|
|
307
|
-
## 🚨 CRITICAL:
|
|
308
|
-
|
|
309
|
-
Your response MUST follow this exact structure:
|
|
310
|
-
|
|
311
|
-
1. **`commands` block** (scaffold service)
|
|
312
|
-
2. **Contract files** (`packages/contracts/src/events/<event>.ts` with correct schema)
|
|
313
|
-
3. **Service files** (src/index.ts, use-cases, tests, README)
|
|
314
|
-
4. **`post-commands` block** (wire event handlers via CLI)
|
|
315
|
-
5. **`dependencies` block** (if needed)
|
|
316
|
-
|
|
317
|
-
**Example complete response structure:**
|
|
318
|
-
|
|
319
|
-
\`\`\`commands
|
|
320
|
-
pf new hono-micro services/my-notifications -y
|
|
321
|
-
\`\`\`
|
|
322
|
-
|
|
323
|
-
#### \`packages/contracts/src/events/orders-created.ts\`
|
|
324
|
-
\`\`\`typescript
|
|
325
|
-
import { createContract } from '@crossdelta/cloudevents'
|
|
326
|
-
import { z } from 'zod'
|
|
327
|
-
|
|
328
|
-
export const OrdersCreatedSchema = z.object({
|
|
329
|
-
orderId: z.string(),
|
|
330
|
-
customerId: z.string(),
|
|
331
|
-
total: z.number(),
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
export const OrdersCreatedContract = createContract({
|
|
335
|
-
type: 'orders.created',
|
|
336
|
-
schema: OrdersCreatedSchema,
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
export type OrdersCreatedData = z.infer<typeof OrdersCreatedContract.schema>
|
|
340
|
-
\`\`\`
|
|
341
|
-
|
|
342
|
-
#### \`src/index.ts\`
|
|
343
|
-
\`\`\`typescript
|
|
344
|
-
// Entry point code...
|
|
345
|
-
\`\`\`
|
|
346
|
-
|
|
347
|
-
#### \`src/use-cases/send-notification.use-case.ts\`
|
|
348
|
-
\`\`\`typescript
|
|
349
|
-
// Use-case code using OrdersCreatedData type...
|
|
350
|
-
\`\`\`
|
|
71
|
+
## 🚨 CRITICAL: Event Consumer Workflow
|
|
351
72
|
|
|
352
|
-
|
|
353
|
-
pf event add orders.created --service services/my-notifications
|
|
354
|
-
pf event add customer.updated --service services/my-notifications
|
|
355
|
-
\`\`\`
|
|
73
|
+
**FOR EVENT CONSUMER SERVICES (services that "consume", "listen to", "react to" events):**
|
|
356
74
|
|
|
357
|
-
|
|
358
|
-
@pusher/push-notifications-server
|
|
359
|
-
\`\`\`
|
|
75
|
+
### What YOU (the AI) generate:
|
|
360
76
|
|
|
361
|
-
|
|
77
|
+
1. **`packages/contracts/src/events/<event>.ts`** - Contract with correct schema fields
|
|
78
|
+
2. **`src/index.ts`** - Entry point with NATS stream/consumer setup
|
|
79
|
+
3. **`src/events/<event>.event.ts`** - Event handler that calls the use-case
|
|
80
|
+
4. **`src/use-cases/*.use-case.ts`** - Business logic (called by handlers)
|
|
81
|
+
5. **`src/use-cases/*.test.ts`** - Tests for use-cases
|
|
82
|
+
6. **`README.md`** - Documentation
|
|
362
83
|
|
|
363
|
-
|
|
84
|
+
### Handler Location (CRITICAL!)
|
|
364
85
|
|
|
365
|
-
|
|
86
|
+
**⚠️ MUST be in `src/events/*.event.ts` - NEVER in `handlers/` subdirectory**
|
|
366
87
|
|
|
367
|
-
```dependencies
|
|
368
|
-
@pusher/push-notifications-server
|
|
369
|
-
drizzle-orm
|
|
370
88
|
```
|
|
89
|
+
✅ CORRECT:
|
|
90
|
+
services/my-service/src/events/
|
|
91
|
+
├── orders-created.event.ts
|
|
92
|
+
└── customers-updated.event.ts
|
|
371
93
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
---
|
|
375
|
-
|
|
376
|
-
## Output Format
|
|
377
|
-
|
|
378
|
-
**CRITICAL: Response MUST use Markdown format with specific block markers!**
|
|
379
|
-
|
|
380
|
-
### Commands Block (REQUIRED - at start)
|
|
381
|
-
|
|
382
|
-
```commands
|
|
383
|
-
pf new hono-micro services/my-service -y
|
|
94
|
+
❌ WRONG:
|
|
95
|
+
services/my-service/src/events/handlers/ # NEVER create!
|
|
384
96
|
```
|
|
385
97
|
|
|
386
|
-
###
|
|
387
|
-
|
|
388
|
-
Each file uses a header with backtick-wrapped path, followed by a code block:
|
|
389
|
-
|
|
390
|
-
#### `packages/contracts/src/events/orders-created.ts`
|
|
391
|
-
```typescript
|
|
392
|
-
import { createContract } from '@crossdelta/cloudevents'
|
|
393
|
-
import { z } from 'zod'
|
|
394
|
-
|
|
395
|
-
export const OrdersCreatedSchema = z.object({
|
|
396
|
-
orderId: z.string(),
|
|
397
|
-
total: z.number(),
|
|
398
|
-
})
|
|
98
|
+
### Handler Export Pattern
|
|
399
99
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
})
|
|
100
|
+
```ts
|
|
101
|
+
// ✅ CORRECT: Default export
|
|
102
|
+
export default handleEvent(OrdersCreatedContract, async (data) => { ... })
|
|
404
103
|
|
|
405
|
-
|
|
104
|
+
// ❌ WRONG: Named export
|
|
105
|
+
export const OrdersCreatedHandler = handleEvent(...)
|
|
406
106
|
```
|
|
407
107
|
|
|
408
|
-
|
|
409
|
-
```typescript
|
|
410
|
-
import '@crossdelta/telemetry'
|
|
411
|
-
|
|
412
|
-
import { Hono } from 'hono'
|
|
413
|
-
// ... rest of code
|
|
414
|
-
```
|
|
108
|
+
### Handler Logging
|
|
415
109
|
|
|
416
|
-
|
|
110
|
+
**Handlers must be thin** - use `console.log` for logging, then delegate to use-case/service:
|
|
417
111
|
|
|
418
|
-
```
|
|
419
|
-
|
|
112
|
+
```ts
|
|
113
|
+
export default handleEvent(OrdersCreatedContract, async (data) => {
|
|
114
|
+
console.log(`[orders.created] Processing orderId=${data.orderId}`) // ✅ console.log
|
|
115
|
+
await processOrder(data)
|
|
116
|
+
})
|
|
420
117
|
```
|
|
421
118
|
|
|
422
|
-
|
|
119
|
+
**Services/use-cases** can use structured logging (NestJS: `new Logger(ServiceName.name)`).
|
|
423
120
|
|
|
424
|
-
|
|
425
|
-
@pusher/push-notifications-server
|
|
426
|
-
lodash
|
|
427
|
-
```
|
|
121
|
+
---
|
|
428
122
|
|
|
429
|
-
|
|
123
|
+
## 🚨 Response Structure
|
|
430
124
|
|
|
431
125
|
```commands
|
|
432
126
|
pf new hono-micro services/my-service -y
|
|
433
127
|
```
|
|
434
128
|
|
|
435
129
|
#### `packages/contracts/src/events/orders-created.ts`
|
|
436
|
-
```
|
|
130
|
+
```ts
|
|
437
131
|
import { createContract } from '@crossdelta/cloudevents'
|
|
438
132
|
import { z } from 'zod'
|
|
439
133
|
|
|
@@ -450,41 +144,10 @@ export const OrdersCreatedContract = createContract({
|
|
|
450
144
|
export type OrdersCreatedData = z.infer<typeof OrdersCreatedContract.schema>
|
|
451
145
|
```
|
|
452
146
|
|
|
453
|
-
#### `src/index.ts`
|
|
454
|
-
```typescript
|
|
455
|
-
import '@crossdelta/telemetry'
|
|
456
|
-
|
|
457
|
-
import { Hono } from 'hono'
|
|
458
|
-
import { consumeJetStreamStreams, ensureJetStreamStreams } from '@crossdelta/cloudevents'
|
|
459
|
-
|
|
460
|
-
const app = new Hono()
|
|
461
|
-
const port = Number(process.env.PORT || process.env.MY_SERVICE_PORT) || 8080
|
|
462
|
-
|
|
463
|
-
const start = async () => {
|
|
464
|
-
await ensureJetStreamStreams({
|
|
465
|
-
streams: [{ stream: 'ORDERS', subjects: ['orders.*'] }],
|
|
466
|
-
})
|
|
467
|
-
|
|
468
|
-
consumeJetStreamStreams({
|
|
469
|
-
streams: ['ORDERS'],
|
|
470
|
-
consumer: 'my-service',
|
|
471
|
-
discover: './src/events/**/*.event.ts',
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
475
|
-
|
|
476
|
-
return app.listen(port, () => {
|
|
477
|
-
console.log(`Server running on http://localhost:${port}`)
|
|
478
|
-
})
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
start().catch(console.error)
|
|
482
|
-
```
|
|
483
|
-
|
|
484
147
|
#### `src/events/orders-created.event.ts`
|
|
485
|
-
```
|
|
148
|
+
```ts
|
|
486
149
|
import { handleEvent } from '@crossdelta/cloudevents'
|
|
487
|
-
import { OrdersCreatedContract, type OrdersCreatedData } from '
|
|
150
|
+
import { OrdersCreatedContract, type OrdersCreatedData } from '{{scope}}/contracts'
|
|
488
151
|
import { processOrder } from '../use-cases/process-order.use-case'
|
|
489
152
|
|
|
490
153
|
export default handleEvent(OrdersCreatedContract, async (data: OrdersCreatedData) => {
|
|
@@ -494,189 +157,131 @@ export default handleEvent(OrdersCreatedContract, async (data: OrdersCreatedData
|
|
|
494
157
|
```
|
|
495
158
|
|
|
496
159
|
#### `src/use-cases/process-order.use-case.ts`
|
|
497
|
-
```
|
|
498
|
-
import type { OrdersCreatedData } from '
|
|
160
|
+
```ts
|
|
161
|
+
import type { OrdersCreatedData } from '{{scope}}/contracts'
|
|
499
162
|
|
|
500
163
|
export const processOrder = async (data: OrdersCreatedData): Promise<void> => {
|
|
501
|
-
console.log('
|
|
502
|
-
// Add your business logic here
|
|
164
|
+
console.log('Processing:', data.orderId)
|
|
503
165
|
}
|
|
504
166
|
```
|
|
505
167
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
import { processOrder } from './process-order.use-case'
|
|
168
|
+
```post-commands
|
|
169
|
+
pf event add orders.created --service services/my-service
|
|
170
|
+
```
|
|
510
171
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const data = { orderId: '123', total: 100 }
|
|
514
|
-
await expect(processOrder(data)).resolves.toBeUndefined()
|
|
515
|
-
})
|
|
516
|
-
})
|
|
172
|
+
```dependencies
|
|
173
|
+
@pusher/push-notifications-server
|
|
517
174
|
```
|
|
518
175
|
|
|
519
|
-
|
|
520
|
-
```markdown
|
|
521
|
-
# My Service
|
|
176
|
+
---
|
|
522
177
|
|
|
523
|
-
|
|
178
|
+
## 🚨 CRITICAL: Schema Fields Consistency
|
|
524
179
|
|
|
525
|
-
|
|
180
|
+
**ONLY use fields that exist in YOUR generated schema!**
|
|
526
181
|
|
|
527
|
-
|
|
528
|
-
|
|
182
|
+
```ts
|
|
183
|
+
// If you generate this schema:
|
|
184
|
+
export const DomainCreatedSchema = z.object({
|
|
185
|
+
domainId: z.string(),
|
|
186
|
+
name: z.string(),
|
|
187
|
+
ownerEmail: z.string().email(),
|
|
188
|
+
})
|
|
529
189
|
|
|
530
|
-
|
|
531
|
-
|
|
190
|
+
// Then ONLY use these fields in service code:
|
|
191
|
+
await sendEmail(data.ownerEmail) // ✅ EXISTS in schema
|
|
192
|
+
await notifyUser(data.userId) // ❌ WRONG - userId not in schema!
|
|
532
193
|
```
|
|
533
194
|
|
|
534
|
-
|
|
535
|
-
```
|
|
195
|
+
**DO NOT hallucinate fields** like `userId`, `customerId`, etc. unless they are in YOUR schema.
|
|
536
196
|
|
|
537
|
-
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Contract Schema (Zod 4 Syntax)
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
export const OrdersCreatedSchema = z.object({
|
|
203
|
+
orderId: z.string(),
|
|
204
|
+
email: z.string().email(), // ✅ Zod 4: no params
|
|
205
|
+
total: z.number(),
|
|
206
|
+
createdAt: z.string().datetime(), // ✅ Zod 4: no params
|
|
207
|
+
})
|
|
208
|
+
```
|
|
538
209
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
5. **Dependencies** block at end (can be empty)
|
|
544
|
-
6. **Paths**: Use relative paths for service files, full paths for contracts
|
|
210
|
+
**Zod 4 Quick Reference:**
|
|
211
|
+
- ✅ `z.string().email()` - no params
|
|
212
|
+
- ✅ `z.string().url()` - no params
|
|
213
|
+
- ❌ `z.string().email('Invalid')` - DEPRECATED
|
|
545
214
|
|
|
546
215
|
---
|
|
547
216
|
|
|
548
|
-
##
|
|
217
|
+
## Naming Convention
|
|
549
218
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
├── src/
|
|
554
|
-
│ ├── index.ts # Entry point (YOU create this)
|
|
555
|
-
│ ├── events/
|
|
556
|
-
│ │ └── orders-created.event.ts # Handler calling use-case (YOU create this)
|
|
557
|
-
│ └── use-cases/
|
|
558
|
-
│ ├── process-order.use-case.ts # Business logic (YOU create this)
|
|
559
|
-
│ └── process-order.test.ts # Tests (YOU create this)
|
|
560
|
-
└── README.md # Documentation (YOU create this)
|
|
219
|
+
| Event Type | Contract | File |
|
|
220
|
+
|------------|----------|------|
|
|
221
|
+
| `orders.created` | `OrdersCreatedContract` | `orders-created.ts` |
|
|
561
222
|
|
|
562
|
-
|
|
563
|
-
└── orders-created.ts # Contract with schema (YOU create this)
|
|
564
|
-
```
|
|
223
|
+
**⚠️ ALWAYS use PLURAL namespace:** `orders.created` ✅, `order.created` ❌
|
|
565
224
|
|
|
566
|
-
|
|
567
|
-
```
|
|
568
|
-
packages/contracts/src/events/
|
|
569
|
-
└── orders-created.mock.json # Created by CLI
|
|
570
|
-
```
|
|
225
|
+
---
|
|
571
226
|
|
|
572
|
-
|
|
573
|
-
```
|
|
574
|
-
services/my-service/
|
|
575
|
-
├── src/
|
|
576
|
-
│ ├── [entry-point] # Framework-specific entry point
|
|
577
|
-
│ └── use-cases/
|
|
578
|
-
│ ├── create-order.use-case.ts
|
|
579
|
-
│ └── create-order.test.ts
|
|
580
|
-
└── README.md
|
|
581
|
-
```
|
|
227
|
+
## Block Explanations
|
|
582
228
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
229
|
+
| Block | Purpose |
|
|
230
|
+
|-------|---------|
|
|
231
|
+
| `commands` | Scaffolds service (REQUIRED FIRST) |
|
|
232
|
+
| `post-commands` | Runs after files created (e.g., `pf event add`) |
|
|
233
|
+
| `dependencies` | Extra npm packages (NOT `@crossdelta/*`) |
|
|
586
234
|
|
|
587
235
|
---
|
|
588
236
|
|
|
589
237
|
## Service Types
|
|
590
238
|
|
|
591
239
|
### 📤 Event Publisher (REST API)
|
|
592
|
-
|
|
593
240
|
**Keywords:** "publishes", "creates", "manages", "REST API"
|
|
594
241
|
|
|
595
|
-
Services that expose HTTP endpoints and publish events to NATS.
|
|
596
|
-
|
|
597
242
|
### 📥 Event Consumer (NATS)
|
|
598
|
-
|
|
599
243
|
**Keywords:** "consumes", "listens to", "reacts to", "handles events"
|
|
600
244
|
|
|
601
|
-
Services that listen to NATS events and process them. See 3-Step Workflow above.
|
|
602
245
|
|
|
603
246
|
### 🔄 Hybrid (Both)
|
|
604
|
-
|
|
605
247
|
Combines REST endpoints + NATS consumer.
|
|
606
248
|
|
|
607
249
|
---
|
|
608
250
|
|
|
609
|
-
##
|
|
610
|
-
|
|
611
|
-
- Live in `src/use-cases/*.use-case.ts`
|
|
612
|
-
- Pure functions, no framework imports
|
|
613
|
-
- Import types from `@scope/contracts`
|
|
614
|
-
- Inject dependencies as parameters (Map, services, etc.)
|
|
615
|
-
- NO manual validation (adapters handle that)
|
|
616
|
-
- **Event handlers:** Always log the event type and a key identifier (e.g., `console.log('📦 Processing order:', data.orderId)`) at the start of the use-case for debugging visibility
|
|
251
|
+
## Required Files (Hono)
|
|
617
252
|
|
|
618
|
-
```ts
|
|
619
|
-
import type { OrdersCreatedData } from '{{scope}}/contracts'
|
|
620
|
-
|
|
621
|
-
export const processOrder = async (
|
|
622
|
-
data: OrdersCreatedData,
|
|
623
|
-
orderStore: Map<string, any>
|
|
624
|
-
) => {
|
|
625
|
-
console.log('📦 Processing order:', data.orderId)
|
|
626
|
-
|
|
627
|
-
orderStore.set(data.orderId, { ...data, processedAt: new Date() })
|
|
628
|
-
return { success: true }
|
|
629
|
-
}
|
|
630
253
|
```
|
|
254
|
+
services/my-service/src/
|
|
255
|
+
├── index.ts
|
|
256
|
+
├── events/orders-created.event.ts
|
|
257
|
+
└── use-cases/
|
|
258
|
+
├── process-order.use-case.ts
|
|
259
|
+
└── process-order.test.ts
|
|
631
260
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
## Testing Rules
|
|
635
|
-
|
|
636
|
-
- Test ONLY use-cases
|
|
637
|
-
- Use appropriate test framework for the runtime
|
|
638
|
-
- Focus on error handling and validation
|
|
639
|
-
- No mocking - prefer simple direct tests
|
|
640
|
-
|
|
641
|
-
Example:
|
|
642
|
-
```ts
|
|
643
|
-
import { describe, expect, it } from 'bun:test'
|
|
644
|
-
import { processOrder } from './process-order.use-case'
|
|
645
|
-
|
|
646
|
-
describe('Process Order', () => {
|
|
647
|
-
it('should store order', async () => {
|
|
648
|
-
const store = new Map()
|
|
649
|
-
const result = await processOrder({ orderId: '123', customerId: 'c1', total: 100 }, store)
|
|
650
|
-
expect(store.has('123')).toBe(true)
|
|
651
|
-
})
|
|
652
|
-
})
|
|
261
|
+
packages/contracts/src/events/
|
|
262
|
+
└── orders-created.ts
|
|
653
263
|
```
|
|
654
264
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
## Code Quality Guidelines
|
|
658
|
-
|
|
659
|
-
- ✅ **Verify package names** on npmjs.com before using
|
|
660
|
-
- ✅ Use exact package names: `@crossdelta/cloudevents`, `@crossdelta/telemetry`
|
|
661
|
-
- ❌ **DO NOT hallucinate** APIs or package names
|
|
265
|
+
**Note:** For NestJS structure, see `nest.md` - NestJS uses Services instead of use-cases.
|
|
662
266
|
|
|
663
267
|
---
|
|
664
268
|
|
|
665
269
|
## Absolute Rules
|
|
666
270
|
|
|
667
271
|
**DO NOT:**
|
|
668
|
-
-
|
|
669
|
-
-
|
|
670
|
-
-
|
|
671
|
-
-
|
|
672
|
-
- Insert semicolons
|
|
673
|
-
- Edit `packages/contracts/src/index.ts`
|
|
272
|
+
- ❌ Use raw NATS clients
|
|
273
|
+
- ❌ Put business logic in handlers or entry points
|
|
274
|
+
- ❌ Use `src/handlers/` (use `src/events/`)
|
|
275
|
+
- ❌ Create `src/types/` directory (use contracts)
|
|
276
|
+
- ❌ Insert semicolons
|
|
277
|
+
- ❌ Edit `packages/contracts/src/index.ts` (CLI handles exports)
|
|
278
|
+
- ❌ Create `use-cases/` folder in NestJS (use Services instead)
|
|
674
279
|
|
|
675
280
|
**DO:**
|
|
676
|
-
-
|
|
677
|
-
-
|
|
678
|
-
-
|
|
679
|
-
-
|
|
680
|
-
-
|
|
681
|
-
- Keep
|
|
682
|
-
-
|
|
281
|
+
- ✅ Contracts in `packages/contracts/src/events/`
|
|
282
|
+
- ✅ Handlers in `src/events/*.event.ts`
|
|
283
|
+
- ✅ Hono: Use-cases in `src/use-cases/*.use-case.ts`
|
|
284
|
+
- ✅ NestJS: Business logic in Services + pure helper functions
|
|
285
|
+
- ✅ Log event type and key identifier in handlers
|
|
286
|
+
- ✅ Keep handlers thin - delegate to use-cases/services
|
|
287
|
+
- ✅ Verify package names on npmjs.com before using
|