@crossdelta/platform-sdk 0.7.8 → 0.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,340 +1,61 @@
|
|
|
1
1
|
# AI Generation Rules for CLI Code Output
|
|
2
2
|
|
|
3
|
-
These rules define how AI-generated scaffolded services must be structured
|
|
4
|
-
|
|
5
|
-
## 🚨 EMERGENCY RULES - READ FIRST 🚨
|
|
6
|
-
|
|
7
|
-
**FOR EVENT CONSUMER SERVICES (services that "listen to", "consume", "react to" events):**
|
|
8
|
-
|
|
9
|
-
**CRITICAL:** Contract schema fields MUST match event handler usage!
|
|
10
|
-
|
|
11
|
-
**MANDATORY 4-STEP WORKFLOW:**
|
|
12
|
-
|
|
13
|
-
1. **Analyze service description** → Extract required fields
|
|
14
|
-
Example: "needs orderId, customerId, total" → Fields: `orderId`, `customerId`, `total`
|
|
15
|
-
|
|
16
|
-
2. **CREATE contract FIRST** (`packages/contracts/src/events/<event-name>.ts`):
|
|
17
|
-
```ts
|
|
18
|
-
export const OrderCreatedContract = createContract({
|
|
19
|
-
type: 'order.created',
|
|
20
|
-
schema: z.object({
|
|
21
|
-
orderId: z.string(), // ✅ From step 1
|
|
22
|
-
customerId: z.string(), // ✅ From step 1
|
|
23
|
-
total: z.number(), // ✅ From step 1
|
|
24
|
-
}),
|
|
25
|
-
})
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
3. **Export contract** (`packages/contracts/src/index.ts`):
|
|
29
|
-
```ts
|
|
30
|
-
export * from './events/order-created'
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
4. **Create event handler** that uses SAME fields from contract:
|
|
34
|
-
```ts
|
|
35
|
-
export default handleEvent(OrderCreatedContract, async (data) => {
|
|
36
|
-
await sendNotification({
|
|
37
|
-
orderId: data.orderId, // ✅ Exists in contract
|
|
38
|
-
customerId: data.customerId, // ✅ Exists in contract
|
|
39
|
-
total: data.total, // ✅ Exists in contract
|
|
40
|
-
})
|
|
41
|
-
})
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
**DO NOT create contracts with generic fields like `id`, `createdAt` - use the actual fields from the service description!**
|
|
3
|
+
These rules define how AI-generated scaffolded services must be structured.
|
|
45
4
|
|
|
46
5
|
---
|
|
47
6
|
|
|
48
|
-
##
|
|
49
|
-
|
|
50
|
-
**NEVER invent APIs, package names, or TypeScript types:**
|
|
51
|
-
- ✅ **Research actual package documentation** before using third-party libraries
|
|
52
|
-
- ✅ **Verify TypeScript types** match the actual library's type definitions
|
|
53
|
-
- ✅ **Check npm registry** to confirm package names exist
|
|
54
|
-
- ❌ **DO NOT hallucinate** API methods, properties, or function signatures
|
|
55
|
-
- ❌ **DO NOT guess** at type structures - look them up or use `any` with a TODO comment
|
|
56
|
-
|
|
57
|
-
**When unsure:**
|
|
58
|
-
1. Use well-known, battle-tested packages (e.g., `@pusher/push-notifications-server`, not `pusher-beams`)
|
|
59
|
-
2. Keep code simple and explicit
|
|
60
|
-
3. Add TODO comments for unverified APIs: `// TODO: Verify this API signature`
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
# 1. Commands Block (REQUIRED - MUST BE FIRST)
|
|
65
|
-
|
|
66
|
-
**EVERY GENERATION MUST START WITH A COMMANDS BLOCK:**
|
|
67
|
-
|
|
68
|
-
```commands
|
|
69
|
-
pf new hono-micro services/my-service -y
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Rules:
|
|
73
|
-
- **ALWAYS** include this commands block as the FIRST thing in your output
|
|
74
|
-
- **CRITICAL:** Services MUST be created in `services/` directory (e.g., `services/orders`, `services/notifications`)
|
|
75
|
-
- **If user provides just a service name** (e.g., "orders"), prepend `services/` → `services/orders`
|
|
76
|
-
- **If user provides full path with `services/`** (e.g., "services/orders"), use as-is
|
|
77
|
-
- The `-y` flag is REQUIRED to skip prompts
|
|
78
|
-
|
|
79
|
-
**Path Examples:**
|
|
80
|
-
- ✅ User says "orders" → Generate: `pf new hono-micro services/orders -y`
|
|
81
|
-
- ✅ User says "payment processor" → Generate: `pf new hono-micro services/payment-processor -y`
|
|
82
|
-
- ✅ User says "services/orders" → Generate: `pf new hono-micro services/orders -y`
|
|
83
|
-
- ❌ NEVER generate: `pf new hono-micro orders -y` (missing services/ prefix)
|
|
84
|
-
- ❌ NEVER generate: `pf new hono-micro backend/orders -y` (wrong directory)
|
|
85
|
-
- ❌ NEVER generate: `pf new hono-micro apps/orders -y` (apps/ is for frontends)
|
|
86
|
-
|
|
87
|
-
---
|
|
88
|
-
|
|
89
|
-
# 2. Post-Generation Commands
|
|
90
|
-
|
|
91
|
-
After generating a service with event **handlers** (Event Consumer or Hybrid), include a post-generation command to generate event mocks:
|
|
92
|
-
|
|
93
|
-
```post-commands
|
|
94
|
-
pf event:generate services/my-service --overwrite
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
This command:
|
|
98
|
-
- Scans all `src/events/*.event.ts` files in the service
|
|
99
|
-
- Generates mock JSON files based on the exported Zod schemas
|
|
100
|
-
- Creates `*.mock.json` files in `packages/contracts/src/events/`
|
|
101
|
-
- Enables testing with `pf event:publish <event-name>`
|
|
102
|
-
|
|
103
|
-
**When to include this command:**
|
|
104
|
-
- ✅ Service is an **Event Consumer** (has event handlers in `src/events/`)
|
|
105
|
-
- ✅ Service is **Hybrid** (has both REST + event handlers)
|
|
106
|
-
- ❌ Service is an **Event Publisher only** (only REST endpoints, NO event handlers)
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
# 3. Dependencies Block
|
|
111
|
-
|
|
112
|
-
```dependencies
|
|
113
|
-
zod
|
|
114
|
-
@pusher/push-notifications-server
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Rules:
|
|
118
|
-
- One package per line.
|
|
119
|
-
- No versions.
|
|
120
|
-
- Only packages not included in the scaffold.
|
|
121
|
-
- **CRITICAL:** Only use packages that actually exist on npm. **Verify package names on npmjs.com** before using them (e.g., `@pusher/push-notifications-server` not `pusher-beams`).
|
|
122
|
-
- **TypeScript safety:** Ensure your code respects the actual TypeScript types exported by the package. Do not invent properties or methods.
|
|
123
|
-
- **IMPORTANT:** Workspace packages (like `{{scope}}/contracts`) are automatically detected and installed with `workspace:*` protocol. Just list the package name without version.
|
|
124
|
-
|
|
125
|
-
### Core Package Names (ALWAYS USE THESE EXACT NAMES)
|
|
126
|
-
|
|
127
|
-
**CRITICAL:** The following packages are part of the monorepo and MUST be imported with these exact names:
|
|
128
|
-
|
|
129
|
-
- `@crossdelta/cloudevents` - CloudEvents/NATS utilities (`handleEvent`, `consumeJetStreamEvents`, `publish`)
|
|
130
|
-
- `@crossdelta/telemetry` - OpenTelemetry setup
|
|
131
|
-
- `@crossdelta/infrastructure` - Infrastructure builders (for infra/ files only)
|
|
132
|
-
- `{{scope}}/contracts` - Shared event contracts (when using Advanced Mode)
|
|
133
|
-
|
|
134
|
-
**DO NOT use invented package names like:**
|
|
135
|
-
- ❌ `shared-events`
|
|
136
|
-
- ❌ `@orderboss/cloudevents`
|
|
137
|
-
- ❌ `@platform/events`
|
|
138
|
-
- ❌ Any other variations
|
|
139
|
-
|
|
140
|
-
---
|
|
141
|
-
|
|
142
|
-
# 4. Required Source Files
|
|
143
|
-
|
|
144
|
-
Generated services must include:
|
|
145
|
-
|
|
146
|
-
**For Event Consumers:**
|
|
147
|
-
- `src/index.ts` (with NATS setup)
|
|
148
|
-
- `src/events/*.event.ts` (event handlers - NOT `src/handlers/`)
|
|
149
|
-
- `src/use-cases/*.use-case.ts`
|
|
150
|
-
- `src/use-cases/*.test.ts`
|
|
151
|
-
- `src/types/*.ts` (Zod schemas and types)
|
|
152
|
-
- `packages/contracts/src/events/*.ts` (event contracts)
|
|
153
|
-
- `packages/contracts/src/index.ts` (export contract)
|
|
154
|
-
- `README.md`
|
|
155
|
-
|
|
156
|
-
**For Event Publishers:**
|
|
157
|
-
- `src/index.ts` (REST API)
|
|
158
|
-
- `src/use-cases/*.use-case.ts`
|
|
159
|
-
- `src/use-cases/*.test.ts`
|
|
160
|
-
- `src/types/*.ts` (Zod schemas and types)
|
|
161
|
-
- `packages/contracts/src/events/*.ts` (RECOMMENDED: contracts for published events)
|
|
162
|
-
- `packages/contracts/src/index.ts` (export contract)
|
|
163
|
-
- `README.md`
|
|
164
|
-
|
|
165
|
-
**Example for Event Consumer:**
|
|
166
|
-
```
|
|
167
|
-
services/order-notifications/
|
|
168
|
-
├── src/
|
|
169
|
-
│ ├── index.ts # NATS consumer setup
|
|
170
|
-
│ ├── events/
|
|
171
|
-
│ │ └── order-created.event.ts # Event handler
|
|
172
|
-
│ ├── use-cases/
|
|
173
|
-
│ │ ├── send-notification.use-case.ts
|
|
174
|
-
│ │ └── send-notification.test.ts
|
|
175
|
-
│ └── types/
|
|
176
|
-
│ └── notifications.ts # Zod schemas/types
|
|
177
|
-
└── README.md
|
|
178
|
-
|
|
179
|
-
packages/contracts/
|
|
180
|
-
├── src/
|
|
181
|
-
│ ├── events/
|
|
182
|
-
│ │ └── order-created.ts # Contract definition
|
|
183
|
-
│ └── index.ts # Export contract
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
Rules:
|
|
187
|
-
- Paths must be relative to the service root.
|
|
188
|
-
- Event handlers MUST go in `src/events/`, NOT `src/handlers/`.
|
|
189
|
-
- Contracts MUST go in `packages/contracts/src/events/`, NOT in service directory.
|
|
190
|
-
- Do NOT generate controller or module folders unless requested.
|
|
191
|
-
|
|
192
|
-
---
|
|
193
|
-
|
|
194
|
-
# 5. Code Style
|
|
195
|
-
|
|
196
|
-
- Biome-compatible formatting.
|
|
197
|
-
- Strict TS.
|
|
198
|
-
- Arrow functions only.
|
|
199
|
-
- No semicolons.
|
|
200
|
-
- Minimal comments.
|
|
201
|
-
- Alphabetized imports.
|
|
202
|
-
|
|
203
|
-
---
|
|
204
|
-
|
|
205
|
-
# 6. Service Structure
|
|
206
|
-
|
|
207
|
-
## 🔑 CRITICAL: Event Publishers vs Event Consumers
|
|
208
|
-
|
|
209
|
-
**Read the service description carefully to determine the service type:**
|
|
210
|
-
|
|
211
|
-
### 📤 Event Publisher (REST API that publishes events)
|
|
212
|
-
|
|
213
|
-
**Keywords in description:** "publishes", "creates", "manages", "REST API", "HTTP endpoints"
|
|
7
|
+
## 🚨 CRITICAL: Event Consumer Workflow
|
|
214
8
|
|
|
215
|
-
**
|
|
9
|
+
**FOR EVENT CONSUMER SERVICES (services that "consume", "listen to", "react to" events):**
|
|
216
10
|
|
|
217
|
-
|
|
218
|
-
- ✅ REST endpoints (POST, GET, PUT, DELETE)
|
|
219
|
-
- ✅ Use `publish()` from `@crossdelta/cloudevents`
|
|
220
|
-
- ✅ **RECOMMENDED:** Create contracts in `packages/contracts` for published events
|
|
221
|
-
- ❌ NO event handlers in `src/events/`
|
|
222
|
-
- ❌ NO `consumeJetStreamEvents()`
|
|
11
|
+
### Naming Convention (CRITICAL!)
|
|
223
12
|
|
|
224
|
-
**Event
|
|
13
|
+
**Event Type → Contract Name → File Name:**
|
|
225
14
|
|
|
226
|
-
|
|
15
|
+
| Event Type | Contract Name | File Name |
|
|
16
|
+
|------------|---------------|-----------|
|
|
17
|
+
| `orders.created` | `OrdersCreatedContract` | `orders-created.ts` |
|
|
18
|
+
| `users.registered` | `UsersRegisteredContract` | `users-registered.ts` |
|
|
19
|
+
| `payments.completed` | `PaymentsCompletedContract` | `payments-completed.ts` |
|
|
227
20
|
|
|
228
|
-
|
|
229
|
-
// Option 1: With contract (RECOMMENDED - type-safe)
|
|
230
|
-
import { OrderCreatedContract } from '@scope/contracts'
|
|
231
|
-
await publish(OrderCreatedContract, orderData)
|
|
232
|
-
|
|
233
|
-
// Option 2: String literal (OK for prototyping)
|
|
234
|
-
await publish('order.created', orderData, {
|
|
235
|
-
source: 'my-service',
|
|
236
|
-
subject: orderId,
|
|
237
|
-
})
|
|
238
|
-
```
|
|
21
|
+
**Rule:** Always use **plural namespace** (`orders.`, `users.`, `payments.`) to match JetStream stream subjects (`orders.>`, `users.>`, etc.)
|
|
239
22
|
|
|
240
|
-
|
|
241
|
-
- ✅ **Default/Recommended** - Create contract in `packages/contracts` for type-safety
|
|
242
|
-
- ✅ Event will be consumed by other services (now or future)
|
|
243
|
-
- ✅ Event schema should be documented and versioned
|
|
244
|
-
- ❌ Only skip if rapid prototyping or service-internal events
|
|
23
|
+
### 4-Step Workflow
|
|
245
24
|
|
|
246
|
-
**
|
|
247
|
-
```ts
|
|
248
|
-
import '@crossdelta/telemetry'
|
|
249
|
-
|
|
250
|
-
import { publish } from '@crossdelta/cloudevents'
|
|
251
|
-
import { OrderCreatedContract } from '@scope/contracts'
|
|
252
|
-
import { Hono } from 'hono'
|
|
253
|
-
import { CreateOrderSchema } from './types/orders'
|
|
254
|
-
import { createOrder } from './use-cases/create-order.use-case'
|
|
255
|
-
|
|
256
|
-
const port = Number(process.env.PORT || process.env.MY_SERVICE_PORT) || 4001
|
|
257
|
-
const app = new Hono()
|
|
258
|
-
const orders = new Map()
|
|
259
|
-
|
|
260
|
-
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
261
|
-
|
|
262
|
-
app.post('/orders', async (c) => {
|
|
263
|
-
const data = CreateOrderSchema.parse(await c.req.json())
|
|
264
|
-
const order = await createOrder(data, orders)
|
|
265
|
-
|
|
266
|
-
// Publish with contract (type-safe)
|
|
267
|
-
await publish(OrderCreatedContract, order)
|
|
268
|
-
|
|
269
|
-
return c.json({ success: true, order }, 201)
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
Bun.serve({ port, fetch: app.fetch })
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
### 📥 Event Consumer (Reacts to events from other services)
|
|
276
|
-
|
|
277
|
-
**Keywords in description:** "consumes", "listens to", "reacts to", "handles events", "processes events"
|
|
278
|
-
|
|
279
|
-
**Example:** "Notification service that sends emails when orders are created. Consumes order.created events."
|
|
280
|
-
|
|
281
|
-
**⚠️⚠️⚠️ MANDATORY WORKFLOW FOR EVENT CONSUMERS ⚠️⚠️⚠️**
|
|
282
|
-
|
|
283
|
-
**YOU MUST FOLLOW THIS EXACT ORDER:**
|
|
284
|
-
|
|
285
|
-
**STEP 1: Analyze Service Description**
|
|
286
|
-
Extract the required event data fields from the description.
|
|
287
|
-
|
|
288
|
-
Example: "sends push notifications when orders are created. Needs orderId, customerId, and total"
|
|
289
|
-
→ Required fields: `orderId`, `customerId`, `total`
|
|
290
|
-
|
|
291
|
-
**STEP 2: CREATE Contract FIRST (packages/contracts/src/events/<event-name>.ts)**
|
|
25
|
+
**STEP 1: CREATE Contract** (`packages/contracts/src/events/<event-name>.ts`):
|
|
292
26
|
```ts
|
|
293
27
|
import { createContract } from '@crossdelta/cloudevents'
|
|
294
28
|
import { z } from 'zod'
|
|
295
29
|
|
|
296
|
-
export const
|
|
297
|
-
type: '
|
|
30
|
+
export const OrdersCreatedContract = createContract({
|
|
31
|
+
type: 'orders.created', // plural namespace!
|
|
298
32
|
schema: z.object({
|
|
299
|
-
orderId: z.string(),
|
|
300
|
-
customerId: z.string(),
|
|
301
|
-
total: z.number(),
|
|
33
|
+
orderId: z.string(),
|
|
34
|
+
customerId: z.string(),
|
|
35
|
+
total: z.number(),
|
|
302
36
|
}),
|
|
303
37
|
})
|
|
304
38
|
|
|
305
|
-
export type
|
|
39
|
+
export type OrdersCreatedData = z.infer<typeof OrdersCreatedContract.schema>
|
|
306
40
|
```
|
|
307
41
|
|
|
308
|
-
**STEP
|
|
42
|
+
**STEP 2: Export Contract** (`packages/contracts/src/index.ts`):
|
|
309
43
|
```ts
|
|
310
|
-
export * from './events/
|
|
44
|
+
export * from './events/orders-created'
|
|
311
45
|
```
|
|
312
46
|
|
|
313
|
-
**STEP
|
|
47
|
+
**STEP 3: Create Event Handler** (`src/events/orders-created.event.ts`):
|
|
314
48
|
```ts
|
|
315
49
|
import { handleEvent } from '@crossdelta/cloudevents'
|
|
316
|
-
import {
|
|
317
|
-
import {
|
|
318
|
-
|
|
319
|
-
export default handleEvent(
|
|
320
|
-
await
|
|
321
|
-
orderId: data.orderId, // ✅ Field exists in contract
|
|
322
|
-
customerId: data.customerId, // ✅ Field exists in contract
|
|
323
|
-
total: data.total, // ✅ Field exists in contract
|
|
324
|
-
})
|
|
50
|
+
import { OrdersCreatedContract, type OrdersCreatedData } from '{{scope}}/contracts'
|
|
51
|
+
import { processOrder } from '../use-cases/process-order.use-case'
|
|
52
|
+
|
|
53
|
+
export default handleEvent(OrdersCreatedContract, async (data: OrdersCreatedData) => {
|
|
54
|
+
await processOrder(data)
|
|
325
55
|
})
|
|
326
56
|
```
|
|
327
57
|
|
|
328
|
-
**
|
|
329
|
-
|
|
330
|
-
**Structure:**
|
|
331
|
-
- ✅ Event handlers in `src/events/*.event.ts`
|
|
332
|
-
- ✅ Use `consumeJetStreamEvents()` in index.ts
|
|
333
|
-
- ✅ Use `handleEvent()` for each event type
|
|
334
|
-
- ❌ Usually NO REST endpoints (except /health)
|
|
335
|
-
|
|
336
|
-
**⚠️ CRITICAL: Event Consumer index.ts Template - COPY THIS EXACTLY:**
|
|
337
|
-
|
|
58
|
+
**STEP 4: Setup Consumer** (`src/index.ts`):
|
|
338
59
|
```ts
|
|
339
60
|
import '@crossdelta/telemetry'
|
|
340
61
|
|
|
@@ -348,13 +69,11 @@ app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
|
348
69
|
|
|
349
70
|
Bun.serve({ port, fetch: app.fetch })
|
|
350
71
|
|
|
351
|
-
// Ensure stream exists (idempotent - safe to call on every start)
|
|
352
72
|
await ensureJetStreamStream({
|
|
353
73
|
stream: 'ORDERS',
|
|
354
74
|
subjects: ['orders.>'],
|
|
355
75
|
})
|
|
356
76
|
|
|
357
|
-
// Start NATS consumer - handlers are auto-discovered
|
|
358
77
|
consumeJetStreamEvents({
|
|
359
78
|
stream: 'ORDERS',
|
|
360
79
|
consumer: 'my-service',
|
|
@@ -362,682 +81,184 @@ consumeJetStreamEvents({
|
|
|
362
81
|
})
|
|
363
82
|
```
|
|
364
83
|
|
|
365
|
-
|
|
366
|
-
- [ ] `import { consumeJetStreamEvents, ensureJetStreamStream } from '@crossdelta/cloudevents'`
|
|
367
|
-
- [ ] `await ensureJetStreamStream({ stream: 'ORDERS', subjects: ['orders.>'] })`
|
|
368
|
-
- [ ] `consumeJetStreamEvents({ stream: 'ORDERS', consumer: 'service-name', discover: './src/events/**/*.event.ts' })`
|
|
369
|
-
- [ ] Top-level await (Bun supports this natively)
|
|
370
|
-
- [ ] `ensureJetStreamStream()` called BEFORE `consumeJetStreamEvents()`
|
|
371
|
-
|
|
372
|
-
**CRITICAL ERRORS TO AVOID:**
|
|
373
|
-
- ❌ Missing `ensureJetStreamStream()` → Stream won't exist, events won't be consumed
|
|
374
|
-
- ❌ Missing `consumeJetStreamEvents()` → Service won't listen to events
|
|
375
|
-
- ❌ Wrong import order → Telemetry must be first
|
|
376
|
-
- ❌ Using `filterSubjects` in `ensureJetStreamStream()` → Use `subjects` parameter
|
|
377
|
-
- ❌ Using `subjects` in `consumeJetStreamEvents()` → Use `filterSubjects` parameter if needed
|
|
378
|
-
|
|
379
|
-
### � HTTP CloudEvent Receiver (Receives CloudEvents via HTTP POST)
|
|
380
|
-
|
|
381
|
-
**Keywords in description:** "webhook", "receives CloudEvents via HTTP", "HTTP push endpoint", "Pub/Sub Push"
|
|
382
|
-
|
|
383
|
-
**Example:** "Webhook service that receives payment events from Stripe as CloudEvents via HTTP POST."
|
|
384
|
-
|
|
385
|
-
**Structure:**
|
|
386
|
-
- ✅ Event handlers in `src/events/*.event.ts`
|
|
387
|
-
- ✅ Use `cloudEvents()` middleware in index.ts
|
|
388
|
-
- ✅ Use `handleEvent()` for each event type
|
|
389
|
-
- ❌ NO `consumeJetStreamEvents()` - events come via HTTP, not NATS
|
|
390
|
-
|
|
391
|
-
**Example index.ts:**
|
|
392
|
-
```ts
|
|
393
|
-
import '@crossdelta/telemetry'
|
|
84
|
+
---
|
|
394
85
|
|
|
395
|
-
|
|
396
|
-
import { Hono } from 'hono'
|
|
86
|
+
## ⚠️ Code Quality Guidelines
|
|
397
87
|
|
|
398
|
-
|
|
399
|
-
|
|
88
|
+
- ✅ **Verify package names** on npmjs.com before using
|
|
89
|
+
- ✅ Use exact package names: `@crossdelta/cloudevents`, `@crossdelta/telemetry`
|
|
90
|
+
- ❌ **DO NOT hallucinate** APIs or package names
|
|
400
91
|
|
|
401
|
-
|
|
92
|
+
---
|
|
402
93
|
|
|
403
|
-
|
|
404
|
-
app.use('/webhooks/*', cloudEvents({
|
|
405
|
-
discover: './src/events/**/*.event.ts',
|
|
406
|
-
log: true
|
|
407
|
-
}))
|
|
94
|
+
# 1. Commands Block (REQUIRED - MUST BE FIRST)
|
|
408
95
|
|
|
409
|
-
|
|
96
|
+
```commands
|
|
97
|
+
pf new hono-micro services/my-service -y
|
|
410
98
|
```
|
|
411
99
|
|
|
412
|
-
|
|
413
|
-
-
|
|
414
|
-
- ✅ Google Cloud Pub/Sub Push endpoints
|
|
415
|
-
- ✅ CloudEvent-to-CloudEvent bridges
|
|
416
|
-
- ❌ NOT for regular REST APIs (use Event Publisher pattern instead)
|
|
417
|
-
|
|
418
|
-
### �🔄 Hybrid Service (Both Publisher and Consumer)
|
|
419
|
-
|
|
420
|
-
Some services both consume events AND expose REST endpoints:
|
|
421
|
-
|
|
422
|
-
**Example:** "Payment service that processes payments via REST API and listens for order.created events."
|
|
423
|
-
|
|
424
|
-
**Structure:**
|
|
425
|
-
- ✅ REST endpoints that publish events
|
|
426
|
-
- ✅ Event handlers that consume events
|
|
427
|
-
- ✅ Use both `publish()` and `consumeJetStreamEvents()`
|
|
100
|
+
- Services MUST be in `services/` directory
|
|
101
|
+
- User says "orders" → Generate: `services/orders`
|
|
428
102
|
|
|
429
103
|
---
|
|
430
104
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
Contains only:
|
|
434
|
-
- Telemetry import (MUST be first)
|
|
435
|
-
- Server setup (Hono)
|
|
436
|
-
- Health endpoint
|
|
437
|
-
- REST endpoints (for Publishers/Hybrid)
|
|
438
|
-
- Event consumer registration (for Consumers/Hybrid)
|
|
439
|
-
- Port configuration from env vars
|
|
105
|
+
# 2. Post-Generation Commands
|
|
440
106
|
|
|
441
|
-
|
|
442
|
-
```ts
|
|
443
|
-
import { CreateOrderSchema, UpdateOrderSchema } from './types/orders'
|
|
107
|
+
For Event Consumer services:
|
|
444
108
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const order = await createOrder(data, orders)
|
|
448
|
-
await publish('order.created', order, { source: 'my-service' })
|
|
449
|
-
return c.json({ success: true, order }, 201)
|
|
450
|
-
})
|
|
109
|
+
```post-commands
|
|
110
|
+
pf event:generate services/my-service --overwrite
|
|
451
111
|
```
|
|
452
112
|
|
|
453
113
|
---
|
|
454
114
|
|
|
455
|
-
#
|
|
456
|
-
|
|
457
|
-
**CRITICAL:** All Zod schemas and types must live in `src/types/` directory.
|
|
458
|
-
|
|
459
|
-
```
|
|
460
|
-
src/
|
|
461
|
-
├── index.ts # Adapters (REST endpoints)
|
|
462
|
-
├── types/ # Schemas & Types (shared)
|
|
463
|
-
│ ├── orders.ts # Order-related schemas & types
|
|
464
|
-
│ ├── payments.ts # Payment-related schemas & types
|
|
465
|
-
│ └── index.ts
|
|
466
|
-
├── events/ # Event handlers (if Consumer)
|
|
467
|
-
│ └── *.event.ts
|
|
468
|
-
└── use-cases/ # Business logic
|
|
469
|
-
└── *.use-case.ts
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
**Types file structure:**
|
|
473
|
-
```ts
|
|
474
|
-
// src/types/orders.ts
|
|
475
|
-
import { z } from 'zod'
|
|
476
|
-
|
|
477
|
-
// Define Zod schemas
|
|
478
|
-
export const CreateOrderSchema = z.object({
|
|
479
|
-
customerId: z.string().min(1),
|
|
480
|
-
items: z.array(z.object({
|
|
481
|
-
productId: z.string().min(1),
|
|
482
|
-
quantity: z.number().int().min(1),
|
|
483
|
-
price: z.number().min(0),
|
|
484
|
-
})).min(1),
|
|
485
|
-
total: z.number().min(0),
|
|
486
|
-
})
|
|
115
|
+
# 3. Dependencies Block
|
|
487
116
|
|
|
488
|
-
|
|
489
|
-
items: z.array(z.object({
|
|
490
|
-
productId: z.string().min(1),
|
|
491
|
-
quantity: z.number().int().min(1),
|
|
492
|
-
price: z.number().min(0),
|
|
493
|
-
})).optional(),
|
|
494
|
-
total: z.number().min(0).optional(),
|
|
495
|
-
status: z.enum(['created', 'updated', 'completed']).optional(),
|
|
496
|
-
})
|
|
117
|
+
Only include packages not in the scaffold:
|
|
497
118
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
119
|
+
```dependencies
|
|
120
|
+
zod
|
|
121
|
+
@pusher/push-notifications-server
|
|
501
122
|
```
|
|
502
123
|
|
|
503
|
-
**
|
|
504
|
-
- ✅ Single source of truth (Zod schema → TypeScript types)
|
|
505
|
-
- ✅ No circular dependencies (types → use-cases, types → adapters)
|
|
506
|
-
- ✅ No duplication between schemas and types
|
|
507
|
-
- ✅ Shared between adapters (REST, Events) and use-cases
|
|
124
|
+
**Note:** `{{scope}}/contracts` is already in the template's package.json.
|
|
508
125
|
|
|
509
126
|
---
|
|
510
127
|
|
|
511
|
-
#
|
|
512
|
-
|
|
513
|
-
Services follow a **Lean Hexagonal Architecture** pattern:
|
|
514
|
-
- **Types & Schemas** (in `src/types/`) = Zod schemas and inferred types (shared by all layers)
|
|
515
|
-
- **Event Handlers** (in `src/events/`) = Adapters (receive events, validate, delegate to use-cases)
|
|
516
|
-
- **REST Endpoints** (in `src/index.ts`) = Adapters (receive HTTP requests, validate, delegate to use-cases)
|
|
517
|
-
- **Use-Cases** (in `src/use-cases/`) = Application Core (business logic, NO validation, NO schemas)
|
|
518
|
-
- No explicit ports/interfaces unless needed
|
|
519
|
-
|
|
520
|
-
**IMPORTANT:** A single service description may result in **multiple use-cases**. Break down complex functionality into focused, single-responsibility use-cases.
|
|
521
|
-
|
|
522
|
-
Example: "Payment processing service" might generate:
|
|
523
|
-
- `validate-payment.use-case.ts` - Validate payment details
|
|
524
|
-
- `process-payment.use-case.ts` - Execute payment with provider
|
|
525
|
-
- `notify-payment-status.use-case.ts` - Send payment notifications
|
|
526
|
-
- `refund-payment.use-case.ts` - Handle refund requests
|
|
527
|
-
|
|
528
|
-
Use-Case Rules:
|
|
529
|
-
- All domain logic lives in `src/use-cases/`
|
|
530
|
-
- One use-case = one responsibility
|
|
531
|
-
- Pure functions preferred
|
|
532
|
-
- No framework imports (no Hono, no Zod, no @crossdelta/cloudevents)
|
|
533
|
-
- **CRITICAL: Import types from `src/types/`** - do NOT redefine types in use-cases
|
|
534
|
-
- **CRITICAL: NO Zod schemas in use-cases!** Schemas live in `src/types/`
|
|
535
|
-
- **CRITICAL: NO manual validation (if/throw) for input data!** Trust that adapters validated the data
|
|
536
|
-
- **CRITICAL: NO globalThis hacks or in-memory state!** Pass state as parameters (Dependency Injection)
|
|
537
|
-
- **DO NOT export domain entity types from use-cases** - define those in `src/types/` if needed
|
|
538
|
-
- **DO NOT create repository interfaces** - keep it lean, inject concrete dependencies
|
|
539
|
-
- For complex persistence needs, add `// TODO: Replace with proper database persistence`
|
|
540
|
-
- Return plain data (no Response objects, no validation)
|
|
541
|
-
- Compose use-cases when needed (call one from another)
|
|
542
|
-
|
|
543
|
-
**Example - CORRECT use-case (Lean Hexagonal):**
|
|
544
|
-
```ts
|
|
545
|
-
// ✅ Import types from src/types/, concrete dependency injection
|
|
546
|
-
import type { CreateOrderInput } from '../types/orders'
|
|
128
|
+
# 4. Required Files
|
|
547
129
|
|
|
548
|
-
|
|
549
|
-
export const createOrder = async (
|
|
550
|
-
input: CreateOrderInput,
|
|
551
|
-
orderStore: Map<string, any> // Concrete dependency, but injected
|
|
552
|
-
) => {
|
|
553
|
-
const orderId = `order-${Date.now()}`
|
|
554
|
-
|
|
555
|
-
const order = {
|
|
556
|
-
orderId,
|
|
557
|
-
customerId: input.customerId,
|
|
558
|
-
items: input.items,
|
|
559
|
-
total: input.total,
|
|
560
|
-
status: 'created',
|
|
561
|
-
createdAt: new Date().toISOString(),
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
orderStore.set(orderId, order)
|
|
565
|
-
return order
|
|
566
|
-
}
|
|
130
|
+
**Event Consumer:**
|
|
567
131
|
```
|
|
132
|
+
services/my-service/
|
|
133
|
+
├── src/
|
|
134
|
+
│ ├── index.ts # NATS consumer setup
|
|
135
|
+
│ ├── events/
|
|
136
|
+
│ │ └── orders-created.event.ts # Event handler
|
|
137
|
+
│ ├── use-cases/
|
|
138
|
+
│ │ ├── process-order.use-case.ts
|
|
139
|
+
│ │ └── process-order.test.ts
|
|
140
|
+
│ └── types/
|
|
141
|
+
│ └── orders.ts
|
|
142
|
+
└── README.md
|
|
568
143
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
export const updateOrder = async (
|
|
575
|
-
input: UpdateOrderInput & { orderId: string }, // Extend with orderId
|
|
576
|
-
orderStore: Map<string, any>
|
|
577
|
-
) => {
|
|
578
|
-
const order = orderStore.get(input.orderId)
|
|
579
|
-
if (!order) {
|
|
580
|
-
throw new Error('Order not found') // ✅ Business logic validation OK
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const updatedOrder = {
|
|
584
|
-
...order,
|
|
585
|
-
...input,
|
|
586
|
-
updatedAt: new Date().toISOString(),
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
orderStore.set(input.orderId, updatedOrder)
|
|
590
|
-
return updatedOrder
|
|
591
|
-
}
|
|
144
|
+
packages/contracts/
|
|
145
|
+
├── src/
|
|
146
|
+
│ ├── events/
|
|
147
|
+
│ │ └── orders-created.ts # Contract
|
|
148
|
+
│ └── index.ts
|
|
592
149
|
```
|
|
593
150
|
|
|
594
|
-
**
|
|
595
|
-
```ts
|
|
596
|
-
import type { CreateOrderInput } from '../types/orders'
|
|
597
|
-
|
|
598
|
-
export const createOrder = async (
|
|
599
|
-
input: CreateOrderInput,
|
|
600
|
-
orderStore: Map<string, any> // TODO: Replace with proper database persistence
|
|
601
|
-
) => {
|
|
602
|
-
const orderId = `order-${Date.now()}`
|
|
603
|
-
const order = { orderId, ...input, status: 'created', createdAt: new Date().toISOString() }
|
|
604
|
-
orderStore.set(orderId, order)
|
|
605
|
-
return order
|
|
606
|
-
}
|
|
151
|
+
**Event Publisher:**
|
|
607
152
|
```
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
// ❌ Repository interface (too complex for simple services)
|
|
618
|
-
export interface OrderRepository { // ❌ NO! Keep it lean
|
|
619
|
-
save: (order: any) => Promise<void>
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// ❌ Manual validation in use-case
|
|
623
|
-
export const createOrder = async (input: CreateOrderInput) => {
|
|
624
|
-
if (!input.customerId) throw new Error('Missing customerId') // ❌ NO!
|
|
625
|
-
**Example - WRONG use-case:**
|
|
626
|
-
```ts
|
|
627
|
-
// ❌ Defining types in use-case instead of importing from src/types/
|
|
628
|
-
export type CreateOrderInput = { ... } // ❌ NO! Import from src/types/
|
|
629
|
-
|
|
630
|
-
// ❌ Zod schema in use-case
|
|
631
|
-
import { z } from 'zod'
|
|
632
|
-
const CreateOrderSchema = z.object({ ... }) // ❌ NO! Define in src/types/
|
|
633
|
-
|
|
634
|
-
// ❌ Manual input validation
|
|
635
|
-
export const createOrder = async (input: CreateOrderInput) => {
|
|
636
|
-
if (!input.customerId) throw new Error('Missing customerId') // ❌ NO!
|
|
637
|
-
// Trust adapters validated the data!
|
|
638
|
-
}
|
|
153
|
+
services/my-service/
|
|
154
|
+
├── src/
|
|
155
|
+
│ ├── index.ts # REST API + publish()
|
|
156
|
+
│ ├── use-cases/
|
|
157
|
+
│ │ ├── create-order.use-case.ts
|
|
158
|
+
│ │ └── create-order.test.ts
|
|
159
|
+
│ └── types/
|
|
160
|
+
│ └── orders.ts
|
|
161
|
+
└── README.md
|
|
639
162
|
```
|
|
640
163
|
|
|
641
|
-
Types and schemas belong in **`src/types/`**, NOT in use-cases!
|
|
642
|
-
|
|
643
164
|
---
|
|
644
165
|
|
|
645
|
-
#
|
|
646
|
-
|
|
647
|
-
**CRITICAL:** Event handlers are ONLY for services that **consume/react to** events from other services!
|
|
648
|
-
|
|
649
|
-
**Do NOT create event handlers if:**
|
|
650
|
-
- ❌ Service description says "publishes events" or "creates/manages X"
|
|
651
|
-
- ❌ Service is a REST API that publishes events
|
|
652
|
-
- ❌ Service is the source/origin of the events
|
|
653
|
-
|
|
654
|
-
**DO create event handlers if:**
|
|
655
|
-
- ✅ Service description says "consumes", "listens to", "reacts to", "handles events"
|
|
656
|
-
- ✅ Service processes events published by other services
|
|
657
|
-
|
|
658
|
-
**CRITICAL: Event handlers MUST live in `src/events/` directory, NOT `src/handlers/`!**
|
|
659
|
-
|
|
660
|
-
## Event Handling Modes
|
|
661
|
-
|
|
662
|
-
The platform supports **Advanced Mode** (contracts) with automatic creation for missing contracts.
|
|
663
|
-
|
|
664
|
-
### 🎯 DECISION RULE (CRITICAL - READ FIRST!)
|
|
665
|
-
|
|
666
|
-
**Always use Advanced Mode (contracts) - You MUST create complete contracts!**
|
|
667
|
-
|
|
668
|
-
When generating an Event Consumer service:
|
|
669
|
-
|
|
670
|
-
1. **CREATE the contract** in `packages/contracts/src/events/` with **complete schema** based on the service description
|
|
671
|
-
2. **Use the contract** in the event handler (`import { OrderCreatedContract } from '{{scope}}/contracts'`)
|
|
672
|
-
3. **Export the contract** in `packages/contracts/src/index.ts`
|
|
673
|
-
|
|
674
|
-
**⚠️ CRITICAL: Contract Schema Must Match Handler Usage!**
|
|
675
|
-
|
|
676
|
-
If the event handler uses fields like `orderId`, `customerId`, `total`, the contract schema MUST include those exact fields!
|
|
677
|
-
|
|
678
|
-
**Example Flow:**
|
|
679
|
-
```
|
|
680
|
-
User prompt: "Notification service that sends push notifications when orders are created"
|
|
681
|
-
|
|
682
|
-
Step 1: Analyze what data the service needs:
|
|
683
|
-
- orderId (to identify the order)
|
|
684
|
-
- customerId (to send notification to customer)
|
|
685
|
-
- total (to show order amount)
|
|
686
|
-
|
|
687
|
-
Step 2: CREATE contract with those fields:
|
|
688
|
-
packages/contracts/src/events/order-created.ts:
|
|
689
|
-
schema: z.object({
|
|
690
|
-
orderId: z.string(),
|
|
691
|
-
customerId: z.string(),
|
|
692
|
-
total: z.number(),
|
|
693
|
-
createdAt: z.string().optional(), // Add optional context fields
|
|
694
|
-
})
|
|
695
|
-
|
|
696
|
-
Step 3: CREATE event handler that uses those fields:
|
|
697
|
-
services/order-notifications/src/events/order-created.event.ts:
|
|
698
|
-
import { OrderCreatedContract, type OrderCreatedData } from '@scope/contracts'
|
|
699
|
-
export default handleEvent(OrderCreatedContract, async (data) => {
|
|
700
|
-
await sendNotification({
|
|
701
|
-
orderId: data.orderId, // ✅ Field exists in contract
|
|
702
|
-
customerId: data.customerId, // ✅ Field exists in contract
|
|
703
|
-
total: data.total, // ✅ Field exists in contract
|
|
704
|
-
})
|
|
705
|
-
})
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
**Why Create Contracts Immediately?**
|
|
709
|
-
- ✅ Schema matches handler requirements from the start
|
|
710
|
-
- ✅ No schema mismatch errors at runtime
|
|
711
|
-
- ✅ Type-safety between contract and handler
|
|
712
|
-
- ✅ Single source of truth for event structure
|
|
166
|
+
# 5. Code Style
|
|
713
167
|
|
|
714
|
-
|
|
715
|
-
-
|
|
716
|
-
- Schema must include ALL fields used by the event handler
|
|
717
|
-
- Export contract in `packages/contracts/src/index.ts`
|
|
718
|
-
- Include `pf event:generate` in post-commands to create mock JSON
|
|
168
|
+
- Biome-compatible, strict TS, arrow functions only
|
|
169
|
+
- No semicolons, minimal comments, alphabetized imports
|
|
719
170
|
|
|
720
|
-
|
|
171
|
+
---
|
|
721
172
|
|
|
722
|
-
|
|
173
|
+
# 6. Service Types
|
|
723
174
|
|
|
724
|
-
|
|
175
|
+
## 📤 Event Publisher (REST API)
|
|
725
176
|
|
|
726
|
-
|
|
727
|
-
2. **CREATE contract file** in `packages/contracts/src/events/<event-name>.ts`
|
|
728
|
-
3. **Include ALL fields** that the event handler will use
|
|
729
|
-
4. **Export in index** - add to `packages/contracts/src/index.ts`
|
|
730
|
-
5. **Add dependency** `{{scope}}/contracts` to service's dependencies block
|
|
177
|
+
**Keywords:** "publishes", "creates", "manages", "REST API"
|
|
731
178
|
|
|
732
|
-
**Contract File Template:**
|
|
733
179
|
```ts
|
|
734
|
-
|
|
735
|
-
import { createContract } from '@crossdelta/cloudevents'
|
|
736
|
-
import { z } from 'zod'
|
|
737
|
-
|
|
738
|
-
export const OrderCreatedContract = createContract({
|
|
739
|
-
type: 'order.created', // Exact event type string
|
|
740
|
-
schema: z.object({
|
|
741
|
-
// REQUIRED: Fields used by the event handler
|
|
742
|
-
orderId: z.string(),
|
|
743
|
-
customerId: z.string(),
|
|
744
|
-
total: z.number(),
|
|
745
|
-
|
|
746
|
-
// OPTIONAL: Additional context fields
|
|
747
|
-
items: z.array(z.object({
|
|
748
|
-
productId: z.string(),
|
|
749
|
-
quantity: z.number(),
|
|
750
|
-
price: z.number(),
|
|
751
|
-
})).optional(),
|
|
752
|
-
status: z.enum(['created', 'processing', 'completed']).optional(),
|
|
753
|
-
createdAt: z.string().optional(),
|
|
754
|
-
}),
|
|
755
|
-
})
|
|
180
|
+
import '@crossdelta/telemetry'
|
|
756
181
|
|
|
757
|
-
|
|
758
|
-
|
|
182
|
+
import { publish } from '@crossdelta/cloudevents'
|
|
183
|
+
import { Hono } from 'hono'
|
|
759
184
|
|
|
760
|
-
|
|
761
|
-
```ts
|
|
762
|
-
// packages/contracts/src/index.ts
|
|
763
|
-
export * from './events/order-created'
|
|
764
|
-
// ... other exports
|
|
765
|
-
```
|
|
185
|
+
const app = new Hono()
|
|
766
186
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
import { OrderCreatedContract, type OrderCreatedData } from '@scope/contracts'
|
|
772
|
-
import { sendNotification } from '../use-cases/send-notification.use-case'
|
|
773
|
-
|
|
774
|
-
export default handleEvent(OrderCreatedContract, async (data: OrderCreatedData) => {
|
|
775
|
-
// data has all fields from contract schema
|
|
776
|
-
await sendNotification({
|
|
777
|
-
orderId: data.orderId, // ✅ Exists in contract
|
|
778
|
-
customerId: data.customerId, // ✅ Exists in contract
|
|
779
|
-
total: data.total, // ✅ Exists in contract
|
|
780
|
-
})
|
|
187
|
+
app.post('/orders', async (c) => {
|
|
188
|
+
const data = await c.req.json()
|
|
189
|
+
await publish('orders.created', data, { source: 'my-service' })
|
|
190
|
+
return c.json({ success: true })
|
|
781
191
|
})
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
**Why Contracts as Default?**
|
|
785
|
-
- ✅ Strong type-safety between Publisher and Consumer
|
|
786
|
-
- ✅ Single source of truth for event schemas
|
|
787
|
-
- ✅ Schema evolution and versioning
|
|
788
|
-
- ✅ Automatic mock generation
|
|
789
|
-
- ✅ Better documentation
|
|
790
192
|
|
|
791
|
-
|
|
193
|
+
Bun.serve({ port: 4001, fetch: app.fetch })
|
|
194
|
+
```
|
|
792
195
|
|
|
793
|
-
|
|
196
|
+
## 📥 Event Consumer (NATS)
|
|
794
197
|
|
|
795
|
-
**
|
|
198
|
+
**Keywords:** "consumes", "listens to", "reacts to", "handles events"
|
|
796
199
|
|
|
797
|
-
|
|
200
|
+
See 4-Step Workflow above.
|
|
798
201
|
|
|
799
|
-
|
|
800
|
-
- ❌ Local schemas in `src/types/events.ts`
|
|
801
|
-
- ❌ String literals for event types
|
|
802
|
-
- ❌ No type-safety between services
|
|
202
|
+
## 🔄 Hybrid (Both)
|
|
803
203
|
|
|
804
|
-
|
|
805
|
-
- ✅ Always create contracts in `packages/contracts/src/events/`
|
|
806
|
-
- ✅ Complete schemas with all required fields
|
|
807
|
-
- ✅ Type-safety across all services
|
|
808
|
-
- ✅ Single source of truth
|
|
204
|
+
Combines REST endpoints + NATS consumer.
|
|
809
205
|
|
|
810
206
|
---
|
|
811
207
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
- Live under `src/events/*.event.ts` (NOT `src/handlers/`)
|
|
815
|
-
- Use Zod for validation (NO `type` field in schema!)
|
|
816
|
-
- Delegate ALL logic to use-cases
|
|
817
|
-
- Contain NO business logic
|
|
818
|
-
- **ALWAYS import from `@crossdelta/cloudevents`** (NOT `shared-events` or any other package!)
|
|
208
|
+
# 7. Use-Case Rules
|
|
819
209
|
|
|
820
|
-
|
|
210
|
+
- Live in `src/use-cases/*.use-case.ts`
|
|
211
|
+
- Pure functions, no framework imports
|
|
212
|
+
- Import types from `src/types/`, NOT Zod schemas
|
|
213
|
+
- Inject dependencies as parameters (Map, services, etc.)
|
|
214
|
+
- NO manual validation (adapters handle that)
|
|
821
215
|
|
|
822
|
-
**CRITICAL:** ALWAYS use the exact import: `import { handleEvent } from '@crossdelta/cloudevents'`
|
|
823
|
-
|
|
824
|
-
**Naming conventions:**
|
|
825
|
-
- Schema constants: PascalCase with `Schema` suffix (e.g., `OrderCreatedSchema`)
|
|
826
|
-
- Exported types: PascalCase with `Data` suffix (e.g., `OrderCreatedData`)
|
|
827
|
-
- Contract constants: PascalCase with `Contract` suffix (e.g., `OrderCreatedContract`)
|
|
828
|
-
|
|
829
|
-
**Contract Structure (Advanced Mode - Default):**
|
|
830
216
|
```ts
|
|
831
|
-
|
|
832
|
-
import { createContract } from '@crossdelta/cloudevents'
|
|
833
|
-
import { z } from 'zod'
|
|
217
|
+
import type { OrdersCreatedData } from '{{scope}}/contracts'
|
|
834
218
|
|
|
835
|
-
export const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
})
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
type: 'order.created',
|
|
843
|
-
schema: OrderCreatedSchema,
|
|
844
|
-
})
|
|
845
|
-
|
|
846
|
-
export type OrderCreatedData = z.infer<typeof OrderCreatedContract.schema>
|
|
219
|
+
export const processOrder = async (
|
|
220
|
+
data: OrdersCreatedData,
|
|
221
|
+
orderStore: Map<string, any>
|
|
222
|
+
) => {
|
|
223
|
+
orderStore.set(data.orderId, { ...data, processedAt: new Date() })
|
|
224
|
+
return { success: true }
|
|
225
|
+
}
|
|
847
226
|
```
|
|
848
227
|
|
|
849
|
-
**Decision Rule:**
|
|
850
|
-
- **Always use Advanced Mode** - contracts from `{{scope}}/contracts`
|
|
851
|
-
- **Contracts auto-created** - `pf event:generate` creates missing contracts
|
|
852
|
-
- Meaning → `packages/contracts`
|
|
853
|
-
- Behavior → `services/*/src`
|
|
854
|
-
|
|
855
|
-
Forbidden in handlers:
|
|
856
|
-
- `type: z.literal('...')` in schema (redundant and causes errors)
|
|
857
|
-
- DB access
|
|
858
|
-
- Multi-step logic
|
|
859
|
-
- Env var parsing
|
|
860
|
-
|
|
861
228
|
---
|
|
862
229
|
|
|
863
|
-
#
|
|
230
|
+
# 8. Testing Rules
|
|
864
231
|
|
|
865
|
-
|
|
866
|
-
-
|
|
867
|
-
-
|
|
868
|
-
-
|
|
869
|
-
- Focus on validation (input params, env vars) and error handling
|
|
870
|
-
- Keep tests simple - avoid complex scenarios
|
|
232
|
+
- Test ONLY use-cases
|
|
233
|
+
- Use `import { describe, expect, it } from 'bun:test'`
|
|
234
|
+
- NO mocking (Bun doesn't support it)
|
|
235
|
+
- Focus on error handling and validation
|
|
871
236
|
|
|
872
|
-
**What to test:**
|
|
873
|
-
- ✅ Input validation (missing/empty parameters)
|
|
874
|
-
- ✅ Environment variable validation
|
|
875
|
-
- ✅ Error messages and error types
|
|
876
|
-
- ❌ External API integrations (Pusher, AWS, etc.)
|
|
877
|
-
- ❌ Complex mocking scenarios
|
|
878
|
-
|
|
879
|
-
Example:
|
|
880
237
|
```ts
|
|
881
|
-
import { describe, expect, it
|
|
882
|
-
import {
|
|
883
|
-
|
|
884
|
-
describe('
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
})
|
|
890
|
-
|
|
891
|
-
afterEach(() => {
|
|
892
|
-
process.env = originalEnv
|
|
893
|
-
})
|
|
894
|
-
|
|
895
|
-
it('should throw error if orderId is missing', async () => {
|
|
896
|
-
await expect(
|
|
897
|
-
sendNotification({ orderId: '', customerId: 'cust-1' })
|
|
898
|
-
).rejects.toThrow('Missing orderId')
|
|
899
|
-
})
|
|
900
|
-
|
|
901
|
-
it('should throw error if credentials not set', async () => {
|
|
902
|
-
delete process.env.PUSHER_BEAMS_INSTANCE_ID
|
|
903
|
-
|
|
904
|
-
await expect(
|
|
905
|
-
sendNotification({ orderId: '123', customerId: 'cust-1' })
|
|
906
|
-
).rejects.toThrow('Missing Pusher Beams credentials')
|
|
238
|
+
import { describe, expect, it } from 'bun:test'
|
|
239
|
+
import { processOrder } from './process-order.use-case'
|
|
240
|
+
|
|
241
|
+
describe('Process Order', () => {
|
|
242
|
+
it('should store order', async () => {
|
|
243
|
+
const store = new Map()
|
|
244
|
+
const result = await processOrder({ orderId: '123', customerId: 'c1', total: 100 }, store)
|
|
245
|
+
expect(store.has('123')).toBe(true)
|
|
907
246
|
})
|
|
908
247
|
})
|
|
909
248
|
```
|
|
910
249
|
|
|
911
250
|
---
|
|
912
251
|
|
|
913
|
-
#
|
|
914
|
-
|
|
915
|
-
MUST include:
|
|
916
|
-
- Description
|
|
917
|
-
- Features list
|
|
918
|
-
- Environment variable table
|
|
919
|
-
- Published/consumed events (with schemas)
|
|
920
|
-
- API endpoints
|
|
921
|
-
- **Testing section with event mock examples**
|
|
922
|
-
- Dev commands
|
|
923
|
-
|
|
924
|
-
### README Structure
|
|
925
|
-
|
|
926
|
-
```markdown
|
|
927
|
-
# Service Name
|
|
928
|
-
|
|
929
|
-
Brief description of what this service does.
|
|
930
|
-
|
|
931
|
-
## Features
|
|
932
|
-
|
|
933
|
-
- Feature 1
|
|
934
|
-
- Feature 2
|
|
935
|
-
|
|
936
|
-
## Environment Variables
|
|
937
|
-
|
|
938
|
-
| Variable | Description | Required | Default |
|
|
939
|
-
|----------|-------------|----------|---------|
|
|
940
|
-
| `SERVICE_PORT` | Port the service runs on | No | 4001 |
|
|
941
|
-
| `NATS_URL` | NATS server URL | Yes | - |
|
|
942
|
-
|
|
943
|
-
## Events
|
|
944
|
-
|
|
945
|
-
### Publishes
|
|
946
|
-
- `orderboss.service.event` - Description
|
|
947
|
-
```json
|
|
948
|
-
{
|
|
949
|
-
"field": "value"
|
|
950
|
-
}
|
|
951
|
-
```
|
|
952
|
-
|
|
953
|
-
### Consumes
|
|
954
|
-
- `orderboss.other.event` - Description
|
|
955
|
-
```json
|
|
956
|
-
{
|
|
957
|
-
"field": "value"
|
|
958
|
-
}
|
|
959
|
-
```
|
|
960
|
-
|
|
961
|
-
## API Endpoints
|
|
962
|
-
|
|
963
|
-
### `GET /health`
|
|
964
|
-
Health check endpoint.
|
|
965
|
-
|
|
966
|
-
**Response:**
|
|
967
|
-
\`\`\`json
|
|
968
|
-
{ "status": "ok" }
|
|
969
|
-
\`\`\`
|
|
970
|
-
|
|
971
|
-
## Testing
|
|
972
|
-
|
|
973
|
-
### Event Mocks
|
|
974
|
-
|
|
975
|
-
This service includes generated mock files for all event handlers in `src/events/*.mock.json`. These mocks can be used for testing and development.
|
|
976
|
-
|
|
977
|
-
**List all available event mocks:**
|
|
978
|
-
\`\`\`bash
|
|
979
|
-
pf event:list
|
|
980
|
-
\`\`\`
|
|
981
|
-
|
|
982
|
-
**Publish a test event using a mock:**
|
|
983
|
-
\`\`\`bash
|
|
984
|
-
pf event:publish order-created
|
|
985
|
-
\`\`\`
|
|
986
|
-
|
|
987
|
-
**Regenerate event mocks:**
|
|
988
|
-
\`\`\`bash
|
|
989
|
-
pf event:generate . --overwrite
|
|
990
|
-
\`\`\`
|
|
991
|
-
|
|
992
|
-
### Manual Testing
|
|
993
|
-
|
|
994
|
-
To test event handlers manually:
|
|
995
|
-
|
|
996
|
-
1. Start the service:
|
|
997
|
-
\`\`\`bash
|
|
998
|
-
bun dev
|
|
999
|
-
\`\`\`
|
|
1000
|
-
|
|
1001
|
-
2. In another terminal, publish a test event:
|
|
1002
|
-
\`\`\`bash
|
|
1003
|
-
pf event:publish order-created
|
|
1004
|
-
\`\`\`
|
|
1005
|
-
|
|
1006
|
-
3. Check the service logs for the handler output
|
|
1007
|
-
|
|
1008
|
-
### Unit Tests
|
|
1009
|
-
|
|
1010
|
-
Run use-case tests:
|
|
1011
|
-
\`\`\`bash
|
|
1012
|
-
bun test
|
|
1013
|
-
\`\`\`
|
|
1014
|
-
|
|
1015
|
-
## Development
|
|
1016
|
-
|
|
1017
|
-
\`\`\`bash
|
|
1018
|
-
bun dev # Start in development mode
|
|
1019
|
-
bun test # Run tests
|
|
1020
|
-
bun lint # Check code quality
|
|
1021
|
-
\`\`\`
|
|
1022
|
-
```
|
|
1023
|
-
|
|
1024
|
-
---
|
|
1025
|
-
|
|
1026
|
-
# 11. Absolute Rules
|
|
252
|
+
# 9. Absolute Rules
|
|
1027
253
|
|
|
1028
|
-
DO NOT
|
|
254
|
+
**DO NOT:**
|
|
1029
255
|
- Generate full rewrites
|
|
1030
|
-
- Add new architecture concepts
|
|
1031
256
|
- Use raw NATS clients
|
|
1032
|
-
- Put logic in
|
|
257
|
+
- Put logic in handlers or index.ts
|
|
258
|
+
- Use `src/handlers/` (use `src/events/`)
|
|
1033
259
|
- Insert semicolons
|
|
1034
|
-
- Add unused folders/files
|
|
1035
|
-
- **Use `src/handlers/` directory (use `src/events/` instead!)**
|
|
1036
260
|
|
|
1037
|
-
DO
|
|
1038
|
-
-
|
|
1039
|
-
-
|
|
1040
|
-
- Keep
|
|
1041
|
-
- **Always place event handlers in `src/events/` directory**
|
|
1042
|
-
|
|
1043
|
-
---
|
|
261
|
+
**DO:**
|
|
262
|
+
- Follow naming convention (plural event types)
|
|
263
|
+
- Create contracts in `packages/contracts`
|
|
264
|
+
- Keep code minimal and clean
|