@crossdelta/platform-sdk 0.5.12 → 0.7.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 +143 -198
- package/bin/cli.js +215 -152
- package/bin/services/ai/instructions/ai-instructions.md +734 -45
- package/bin/templates/workspace/.github/copilot-instructions.md.hbs +56 -694
- package/bin/templates/workspace/.github/workflows/publish-packages.yml +6 -7
- package/bin/templates/workspace/package.json.hbs +21 -3
- package/bin/templates/workspace/packages/contracts/README.md.hbs +134 -0
- package/bin/templates/workspace/packages/contracts/package.json.hbs +19 -0
- package/bin/templates/workspace/packages/contracts/src/index.ts +8 -0
- package/bin/templates/workspace/packages/contracts/tsconfig.json.hbs +8 -0
- package/install.sh +160 -0
- package/package.json +30 -10
|
@@ -14,13 +14,43 @@ pf new hono-micro services/my-service -y
|
|
|
14
14
|
|
|
15
15
|
Rules:
|
|
16
16
|
- **ALWAYS** include this commands block as the FIRST thing in your output
|
|
17
|
-
-
|
|
18
|
-
-
|
|
17
|
+
- **CRITICAL:** Services MUST be created in `services/` directory (e.g., `services/orders`, `services/notifications`)
|
|
18
|
+
- **If user provides just a service name** (e.g., "orders"), prepend `services/` → `services/orders`
|
|
19
|
+
- **If user provides full path with `services/`** (e.g., "services/orders"), use as-is
|
|
19
20
|
- The `-y` flag is REQUIRED to skip prompts
|
|
20
21
|
|
|
22
|
+
**Path Examples:**
|
|
23
|
+
- ✅ User says "orders" → Generate: `pf new hono-micro services/orders -y`
|
|
24
|
+
- ✅ User says "payment processor" → Generate: `pf new hono-micro services/payment-processor -y`
|
|
25
|
+
- ✅ User says "services/orders" → Generate: `pf new hono-micro services/orders -y`
|
|
26
|
+
- ❌ NEVER generate: `pf new hono-micro orders -y` (missing services/ prefix)
|
|
27
|
+
- ❌ NEVER generate: `pf new hono-micro backend/orders -y` (wrong directory)
|
|
28
|
+
- ❌ NEVER generate: `pf new hono-micro apps/orders -y` (apps/ is for frontends)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# 2. Post-Generation Commands
|
|
33
|
+
|
|
34
|
+
After generating a service with event **handlers** (Event Consumer or Hybrid), include a post-generation command to generate event mocks:
|
|
35
|
+
|
|
36
|
+
```post-commands
|
|
37
|
+
pf event:generate services/my-service --overwrite
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This command:
|
|
41
|
+
- Scans all `src/events/*.event.ts` files in the service
|
|
42
|
+
- Generates mock JSON files based on the exported Zod schemas
|
|
43
|
+
- Creates `*.mock.json` files in `packages/contracts/src/events/`
|
|
44
|
+
- Enables testing with `pf event:publish <event-name>`
|
|
45
|
+
|
|
46
|
+
**When to include this command:**
|
|
47
|
+
- ✅ Service is an **Event Consumer** (has event handlers in `src/events/`)
|
|
48
|
+
- ✅ Service is **Hybrid** (has both REST + event handlers)
|
|
49
|
+
- ❌ Service is an **Event Publisher only** (only REST endpoints, NO event handlers)
|
|
50
|
+
|
|
21
51
|
---
|
|
22
52
|
|
|
23
|
-
#
|
|
53
|
+
# 3. Dependencies Block
|
|
24
54
|
|
|
25
55
|
```dependencies
|
|
26
56
|
zod
|
|
@@ -32,26 +62,43 @@ Rules:
|
|
|
32
62
|
- No versions.
|
|
33
63
|
- Only packages not included in the scaffold.
|
|
34
64
|
- **CRITICAL:** Only use packages that actually exist on npm. Verify package names are correct (e.g., `@pusher/push-notifications-server` not `pusher-beams`). When unsure, use well-known, established packages.
|
|
65
|
+
- **IMPORTANT:** Workspace packages (like `{{scope}}/contracts`) are automatically detected and installed with `workspace:*` protocol. Just list the package name without version.
|
|
66
|
+
|
|
67
|
+
### Core Package Names (ALWAYS USE THESE EXACT NAMES)
|
|
68
|
+
|
|
69
|
+
**CRITICAL:** The following packages are part of the monorepo and MUST be imported with these exact names:
|
|
70
|
+
|
|
71
|
+
- `@crossdelta/cloudevents` - CloudEvents/NATS utilities (`handleEvent`, `consumeJetStreamEvents`, `publish`)
|
|
72
|
+
- `@crossdelta/telemetry` - OpenTelemetry setup
|
|
73
|
+
- `@crossdelta/infrastructure` - Infrastructure builders (for infra/ files only)
|
|
74
|
+
- `{{scope}}/contracts` - Shared event contracts (when using Advanced Mode)
|
|
75
|
+
|
|
76
|
+
**DO NOT use invented package names like:**
|
|
77
|
+
- ❌ `shared-events`
|
|
78
|
+
- ❌ `@orderboss/cloudevents`
|
|
79
|
+
- ❌ `@platform/events`
|
|
80
|
+
- ❌ Any other variations
|
|
35
81
|
|
|
36
82
|
---
|
|
37
83
|
|
|
38
|
-
#
|
|
84
|
+
# 4. Required Source Files
|
|
39
85
|
|
|
40
86
|
Generated services must include:
|
|
41
87
|
|
|
42
88
|
- `src/index.ts`
|
|
43
|
-
- `src/
|
|
89
|
+
- `src/events/*.event.ts` (event handlers - NOT `src/handlers/`)
|
|
44
90
|
- `src/use-cases/*.use-case.ts`
|
|
45
91
|
- `src/use-cases/*.test.ts`
|
|
46
92
|
- `README.md`
|
|
47
93
|
|
|
48
94
|
Rules:
|
|
49
95
|
- Paths must be relative to the service root.
|
|
96
|
+
- Event handlers MUST go in `src/events/`, NOT `src/handlers/`.
|
|
50
97
|
- Do NOT generate controller or module folders unless requested.
|
|
51
98
|
|
|
52
99
|
---
|
|
53
100
|
|
|
54
|
-
#
|
|
101
|
+
# 5. Code Style
|
|
55
102
|
|
|
56
103
|
- Biome-compatible formatting.
|
|
57
104
|
- Strict TS.
|
|
@@ -62,11 +109,89 @@ Rules:
|
|
|
62
109
|
|
|
63
110
|
---
|
|
64
111
|
|
|
65
|
-
#
|
|
112
|
+
# 6. Service Structure
|
|
113
|
+
|
|
114
|
+
## 🔑 CRITICAL: Event Publishers vs Event Consumers
|
|
115
|
+
|
|
116
|
+
**Read the service description carefully to determine the service type:**
|
|
117
|
+
|
|
118
|
+
### 📤 Event Publisher (REST API that publishes events)
|
|
119
|
+
|
|
120
|
+
**Keywords in description:** "publishes", "creates", "manages", "REST API", "HTTP endpoints"
|
|
121
|
+
|
|
122
|
+
**Example:** "Order management service that handles order creation. Publishes order.created events."
|
|
123
|
+
|
|
124
|
+
**Structure:**
|
|
125
|
+
- ✅ REST endpoints (POST, GET, PUT, DELETE)
|
|
126
|
+
- ✅ Use `publish()` from `@crossdelta/cloudevents`
|
|
127
|
+
- ✅ **RECOMMENDED:** Create contracts in `packages/contracts` for published events
|
|
128
|
+
- ❌ NO event handlers in `src/events/`
|
|
129
|
+
- ❌ NO `consumeJetStreamEvents()`
|
|
130
|
+
|
|
131
|
+
**Event Publishing Strategy:**
|
|
132
|
+
|
|
133
|
+
Publishers can use **contracts** (recommended) or **string literals**:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// Option 1: With contract (RECOMMENDED - type-safe)
|
|
137
|
+
import { OrderCreatedContract } from '@scope/contracts'
|
|
138
|
+
await publish(OrderCreatedContract, orderData)
|
|
139
|
+
|
|
140
|
+
// Option 2: String literal (OK for prototyping)
|
|
141
|
+
await publish('order.created', orderData, {
|
|
142
|
+
source: 'my-service',
|
|
143
|
+
subject: orderId,
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**When to create contracts for published events:**
|
|
148
|
+
- ✅ **Default/Recommended** - Create contract in `packages/contracts` for type-safety
|
|
149
|
+
- ✅ Event will be consumed by other services (now or future)
|
|
150
|
+
- ✅ Event schema should be documented and versioned
|
|
151
|
+
- ❌ Only skip if rapid prototyping or service-internal events
|
|
152
|
+
|
|
153
|
+
**Example index.ts with contracts:**
|
|
154
|
+
```ts
|
|
155
|
+
import '@crossdelta/telemetry'
|
|
156
|
+
|
|
157
|
+
import { publish } from '@crossdelta/cloudevents'
|
|
158
|
+
import { OrderCreatedContract } from '@scope/contracts'
|
|
159
|
+
import { Hono } from 'hono'
|
|
160
|
+
import { CreateOrderSchema } from './types/orders'
|
|
161
|
+
import { createOrder } from './use-cases/create-order.use-case'
|
|
162
|
+
|
|
163
|
+
const port = Number(process.env.PORT || process.env.MY_SERVICE_PORT) || 4001
|
|
164
|
+
const app = new Hono()
|
|
165
|
+
const orders = new Map()
|
|
166
|
+
|
|
167
|
+
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
168
|
+
|
|
169
|
+
app.post('/orders', async (c) => {
|
|
170
|
+
const data = CreateOrderSchema.parse(await c.req.json())
|
|
171
|
+
const order = await createOrder(data, orders)
|
|
172
|
+
|
|
173
|
+
// Publish with contract (type-safe)
|
|
174
|
+
await publish(OrderCreatedContract, order)
|
|
175
|
+
|
|
176
|
+
return c.json({ success: true, order }, 201)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
Bun.serve({ port, fetch: app.fetch })
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 📥 Event Consumer (Reacts to events from other services)
|
|
183
|
+
|
|
184
|
+
**Keywords in description:** "consumes", "listens to", "reacts to", "handles events", "processes events"
|
|
66
185
|
|
|
67
|
-
|
|
68
|
-
**IMPORTANT:** Telemetry MUST be the first import!
|
|
186
|
+
**Example:** "Notification service that sends emails when orders are created. Consumes order.created events."
|
|
69
187
|
|
|
188
|
+
**Structure:**
|
|
189
|
+
- ✅ Event handlers in `src/events/*.event.ts`
|
|
190
|
+
- ✅ Use `consumeJetStreamEvents()` in index.ts
|
|
191
|
+
- ✅ Use `handleEvent()` for each event type
|
|
192
|
+
- ❌ Usually NO REST endpoints (except /health)
|
|
193
|
+
|
|
194
|
+
**Example index.ts:**
|
|
70
195
|
```ts
|
|
71
196
|
import '@crossdelta/telemetry'
|
|
72
197
|
|
|
@@ -81,28 +206,154 @@ app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
|
81
206
|
// Start NATS consumer - handlers are auto-discovered
|
|
82
207
|
consumeJetStreamEvents({
|
|
83
208
|
stream: 'ORDERS',
|
|
84
|
-
subjects: ['orders.>'],
|
|
85
209
|
consumer: 'my-service',
|
|
86
|
-
discover: './src/
|
|
210
|
+
discover: './src/events/**/*.event.ts',
|
|
87
211
|
})
|
|
88
212
|
|
|
89
213
|
Bun.serve({ port, fetch: app.fetch })
|
|
90
214
|
```
|
|
91
215
|
|
|
216
|
+
**IMPORTANT:** Do NOT include `subjects` parameter in `consumeJetStreamEvents` - it's not supported.
|
|
217
|
+
|
|
218
|
+
### � HTTP CloudEvent Receiver (Receives CloudEvents via HTTP POST)
|
|
219
|
+
|
|
220
|
+
**Keywords in description:** "webhook", "receives CloudEvents via HTTP", "HTTP push endpoint", "Pub/Sub Push"
|
|
221
|
+
|
|
222
|
+
**Example:** "Webhook service that receives payment events from Stripe as CloudEvents via HTTP POST."
|
|
223
|
+
|
|
224
|
+
**Structure:**
|
|
225
|
+
- ✅ Event handlers in `src/events/*.event.ts`
|
|
226
|
+
- ✅ Use `cloudEvents()` middleware in index.ts
|
|
227
|
+
- ✅ Use `handleEvent()` for each event type
|
|
228
|
+
- ❌ NO `consumeJetStreamEvents()` - events come via HTTP, not NATS
|
|
229
|
+
|
|
230
|
+
**Example index.ts:**
|
|
231
|
+
```ts
|
|
232
|
+
import '@crossdelta/telemetry'
|
|
233
|
+
|
|
234
|
+
import { cloudEvents } from '@crossdelta/cloudevents'
|
|
235
|
+
import { Hono } from 'hono'
|
|
236
|
+
|
|
237
|
+
const port = Number(process.env.PORT || process.env.MY_SERVICE_PORT) || 4003
|
|
238
|
+
const app = new Hono()
|
|
239
|
+
|
|
240
|
+
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
241
|
+
|
|
242
|
+
// HTTP CloudEvent receiver - handlers are auto-discovered
|
|
243
|
+
app.use('/webhooks/*', cloudEvents({
|
|
244
|
+
discover: './src/events/**/*.event.ts',
|
|
245
|
+
log: true
|
|
246
|
+
}))
|
|
247
|
+
|
|
248
|
+
Bun.serve({ port, fetch: app.fetch })
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**When to use:**
|
|
252
|
+
- ✅ Receiving webhooks (Stripe, GitHub, etc.)
|
|
253
|
+
- ✅ Google Cloud Pub/Sub Push endpoints
|
|
254
|
+
- ✅ CloudEvent-to-CloudEvent bridges
|
|
255
|
+
- ❌ NOT for regular REST APIs (use Event Publisher pattern instead)
|
|
256
|
+
|
|
257
|
+
### �🔄 Hybrid Service (Both Publisher and Consumer)
|
|
258
|
+
|
|
259
|
+
Some services both consume events AND expose REST endpoints:
|
|
260
|
+
|
|
261
|
+
**Example:** "Payment service that processes payments via REST API and listens for order.created events."
|
|
262
|
+
|
|
263
|
+
**Structure:**
|
|
264
|
+
- ✅ REST endpoints that publish events
|
|
265
|
+
- ✅ Event handlers that consume events
|
|
266
|
+
- ✅ Use both `publish()` and `consumeJetStreamEvents()`
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
### index.ts Rules
|
|
271
|
+
|
|
92
272
|
Contains only:
|
|
93
273
|
- Telemetry import (MUST be first)
|
|
94
274
|
- Server setup (Hono)
|
|
95
275
|
- Health endpoint
|
|
96
|
-
-
|
|
276
|
+
- REST endpoints (for Publishers/Hybrid)
|
|
277
|
+
- Event consumer registration (for Consumers/Hybrid)
|
|
97
278
|
- Port configuration from env vars
|
|
98
279
|
|
|
280
|
+
**Import schemas and types from `src/types/` directory:**
|
|
281
|
+
```ts
|
|
282
|
+
import { CreateOrderSchema, UpdateOrderSchema } from './types/orders'
|
|
283
|
+
|
|
284
|
+
app.post('/orders', async (c) => {
|
|
285
|
+
const data = CreateOrderSchema.parse(await c.req.json())
|
|
286
|
+
const order = await createOrder(data, orders)
|
|
287
|
+
await publish('order.created', order, { source: 'my-service' })
|
|
288
|
+
return c.json({ success: true, order }, 201)
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
# 6.5. Types & Schemas Structure
|
|
295
|
+
|
|
296
|
+
**CRITICAL:** All Zod schemas and types must live in `src/types/` directory.
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
src/
|
|
300
|
+
├── index.ts # Adapters (REST endpoints)
|
|
301
|
+
├── types/ # Schemas & Types (shared)
|
|
302
|
+
│ ├── orders.ts # Order-related schemas & types
|
|
303
|
+
│ ├── payments.ts # Payment-related schemas & types
|
|
304
|
+
│ └── index.ts
|
|
305
|
+
├── events/ # Event handlers (if Consumer)
|
|
306
|
+
│ └── *.event.ts
|
|
307
|
+
└── use-cases/ # Business logic
|
|
308
|
+
└── *.use-case.ts
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Types file structure:**
|
|
312
|
+
```ts
|
|
313
|
+
// src/types/orders.ts
|
|
314
|
+
import { z } from 'zod'
|
|
315
|
+
|
|
316
|
+
// Define Zod schemas
|
|
317
|
+
export const CreateOrderSchema = z.object({
|
|
318
|
+
customerId: z.string().min(1),
|
|
319
|
+
items: z.array(z.object({
|
|
320
|
+
productId: z.string().min(1),
|
|
321
|
+
quantity: z.number().int().min(1),
|
|
322
|
+
price: z.number().min(0),
|
|
323
|
+
})).min(1),
|
|
324
|
+
total: z.number().min(0),
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
export const UpdateOrderSchema = z.object({
|
|
328
|
+
items: z.array(z.object({
|
|
329
|
+
productId: z.string().min(1),
|
|
330
|
+
quantity: z.number().int().min(1),
|
|
331
|
+
price: z.number().min(0),
|
|
332
|
+
})).optional(),
|
|
333
|
+
total: z.number().min(0).optional(),
|
|
334
|
+
status: z.enum(['created', 'updated', 'completed']).optional(),
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
// Export inferred types
|
|
338
|
+
export type CreateOrderInput = z.infer<typeof CreateOrderSchema>
|
|
339
|
+
export type UpdateOrderInput = z.infer<typeof UpdateOrderSchema>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Why `src/types/`?**
|
|
343
|
+
- ✅ Single source of truth (Zod schema → TypeScript types)
|
|
344
|
+
- ✅ No circular dependencies (types → use-cases, types → adapters)
|
|
345
|
+
- ✅ No duplication between schemas and types
|
|
346
|
+
- ✅ Shared between adapters (REST, Events) and use-cases
|
|
347
|
+
|
|
99
348
|
---
|
|
100
349
|
|
|
101
|
-
#
|
|
350
|
+
# 7. Use-Case Rules (Lean Hexagonal Architecture)
|
|
102
351
|
|
|
103
352
|
Services follow a **Lean Hexagonal Architecture** pattern:
|
|
104
|
-
- **
|
|
105
|
-
- **
|
|
353
|
+
- **Types & Schemas** (in `src/types/`) = Zod schemas and inferred types (shared by all layers)
|
|
354
|
+
- **Event Handlers** (in `src/events/`) = Adapters (receive events, validate, delegate to use-cases)
|
|
355
|
+
- **REST Endpoints** (in `src/index.ts`) = Adapters (receive HTTP requests, validate, delegate to use-cases)
|
|
356
|
+
- **Use-Cases** (in `src/use-cases/`) = Application Core (business logic, NO validation, NO schemas)
|
|
106
357
|
- No explicit ports/interfaces unless needed
|
|
107
358
|
|
|
108
359
|
**IMPORTANT:** A single service description may result in **multiple use-cases**. Break down complex functionality into focused, single-responsibility use-cases.
|
|
@@ -114,66 +365,400 @@ Example: "Payment processing service" might generate:
|
|
|
114
365
|
- `refund-payment.use-case.ts` - Handle refund requests
|
|
115
366
|
|
|
116
367
|
Use-Case Rules:
|
|
117
|
-
- All domain logic lives in `use-cases/`
|
|
368
|
+
- All domain logic lives in `src/use-cases/`
|
|
118
369
|
- One use-case = one responsibility
|
|
119
370
|
- Pure functions preferred
|
|
120
|
-
- No framework imports
|
|
121
|
-
-
|
|
371
|
+
- No framework imports (no Hono, no Zod, no @crossdelta/cloudevents)
|
|
372
|
+
- **CRITICAL: Import types from `src/types/`** - do NOT redefine types in use-cases
|
|
373
|
+
- **CRITICAL: NO Zod schemas in use-cases!** Schemas live in `src/types/`
|
|
374
|
+
- **CRITICAL: NO manual validation (if/throw) for input data!** Trust that adapters validated the data
|
|
375
|
+
- **CRITICAL: NO globalThis hacks or in-memory state!** Pass state as parameters (Dependency Injection)
|
|
376
|
+
- **DO NOT export domain entity types from use-cases** - define those in `src/types/` if needed
|
|
377
|
+
- **DO NOT create repository interfaces** - keep it lean, inject concrete dependencies
|
|
378
|
+
- For complex persistence needs, add `// TODO: Replace with proper database persistence`
|
|
379
|
+
- Return plain data (no Response objects, no validation)
|
|
122
380
|
- Compose use-cases when needed (call one from another)
|
|
123
381
|
|
|
382
|
+
**Example - CORRECT use-case (Lean Hexagonal):**
|
|
383
|
+
```ts
|
|
384
|
+
// ✅ Import types from src/types/, concrete dependency injection
|
|
385
|
+
import type { CreateOrderInput } from '../types/orders'
|
|
386
|
+
|
|
387
|
+
// Inject concrete dependencies (Map, Set, etc.) - keep it lean
|
|
388
|
+
export const createOrder = async (
|
|
389
|
+
input: CreateOrderInput,
|
|
390
|
+
orderStore: Map<string, any> // Concrete dependency, but injected
|
|
391
|
+
) => {
|
|
392
|
+
const orderId = `order-${Date.now()}`
|
|
393
|
+
|
|
394
|
+
const order = {
|
|
395
|
+
orderId,
|
|
396
|
+
customerId: input.customerId,
|
|
397
|
+
items: input.items,
|
|
398
|
+
total: input.total,
|
|
399
|
+
status: 'created',
|
|
400
|
+
createdAt: new Date().toISOString(),
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
orderStore.set(orderId, order)
|
|
404
|
+
return order
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**Example - CORRECT use-case with extended input:**
|
|
409
|
+
```ts
|
|
410
|
+
// ✅ Extend imported type for additional parameters
|
|
411
|
+
import type { UpdateOrderInput } from '../types/orders'
|
|
412
|
+
|
|
413
|
+
export const updateOrder = async (
|
|
414
|
+
input: UpdateOrderInput & { orderId: string }, // Extend with orderId
|
|
415
|
+
orderStore: Map<string, any>
|
|
416
|
+
) => {
|
|
417
|
+
const order = orderStore.get(input.orderId)
|
|
418
|
+
if (!order) {
|
|
419
|
+
throw new Error('Order not found') // ✅ Business logic validation OK
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const updatedOrder = {
|
|
423
|
+
...order,
|
|
424
|
+
...input,
|
|
425
|
+
updatedAt: new Date().toISOString(),
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
orderStore.set(input.orderId, updatedOrder)
|
|
429
|
+
return updatedOrder
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Example - CORRECT use-case with DB TODO:**
|
|
434
|
+
```ts
|
|
435
|
+
import type { CreateOrderInput } from '../types/orders'
|
|
436
|
+
|
|
437
|
+
export const createOrder = async (
|
|
438
|
+
input: CreateOrderInput,
|
|
439
|
+
orderStore: Map<string, any> // TODO: Replace with proper database persistence
|
|
440
|
+
) => {
|
|
441
|
+
const orderId = `order-${Date.now()}`
|
|
442
|
+
const order = { orderId, ...input, status: 'created', createdAt: new Date().toISOString() }
|
|
443
|
+
orderStore.set(orderId, order)
|
|
444
|
+
return order
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**Example - WRONG use-case:**
|
|
449
|
+
```ts
|
|
450
|
+
// ❌ globalThis hack for state
|
|
451
|
+
const orders = globalThis.__ORDERS__ || (globalThis.__ORDERS__ = {}) // ❌ NO!
|
|
452
|
+
|
|
453
|
+
// ❌ Domain entity types exported from use-case
|
|
454
|
+
export type Order = { ... } // ❌ NO! Define in adapter
|
|
455
|
+
|
|
456
|
+
// ❌ Repository interface (too complex for simple services)
|
|
457
|
+
export interface OrderRepository { // ❌ NO! Keep it lean
|
|
458
|
+
save: (order: any) => Promise<void>
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ❌ Manual validation in use-case
|
|
462
|
+
export const createOrder = async (input: CreateOrderInput) => {
|
|
463
|
+
if (!input.customerId) throw new Error('Missing customerId') // ❌ NO!
|
|
464
|
+
**Example - WRONG use-case:**
|
|
465
|
+
```ts
|
|
466
|
+
// ❌ Defining types in use-case instead of importing from src/types/
|
|
467
|
+
export type CreateOrderInput = { ... } // ❌ NO! Import from src/types/
|
|
468
|
+
|
|
469
|
+
// ❌ Zod schema in use-case
|
|
470
|
+
import { z } from 'zod'
|
|
471
|
+
const CreateOrderSchema = z.object({ ... }) // ❌ NO! Define in src/types/
|
|
472
|
+
|
|
473
|
+
// ❌ Manual input validation
|
|
474
|
+
export const createOrder = async (input: CreateOrderInput) => {
|
|
475
|
+
if (!input.customerId) throw new Error('Missing customerId') // ❌ NO!
|
|
476
|
+
// Trust adapters validated the data!
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
Types and schemas belong in **`src/types/`**, NOT in use-cases!
|
|
481
|
+
|
|
124
482
|
---
|
|
125
483
|
|
|
126
|
-
#
|
|
484
|
+
# 8. Event Handler Rules (For Event Consumers Only!)
|
|
127
485
|
|
|
128
|
-
|
|
129
|
-
- Live under `src/handlers/*.event.ts`
|
|
130
|
-
- Use Zod for validation (NO `type` field in schema!)
|
|
131
|
-
- Export inferred types for use in use-cases
|
|
132
|
-
- Delegate ALL logic to use-cases
|
|
133
|
-
- Contain NO business logic
|
|
486
|
+
**CRITICAL:** Event handlers are ONLY for services that **consume/react to** events from other services!
|
|
134
487
|
|
|
135
|
-
**
|
|
488
|
+
**Do NOT create event handlers if:**
|
|
489
|
+
- ❌ Service description says "publishes events" or "creates/manages X"
|
|
490
|
+
- ❌ Service is a REST API that publishes events
|
|
491
|
+
- ❌ Service is the source/origin of the events
|
|
136
492
|
|
|
137
|
-
|
|
493
|
+
**DO create event handlers if:**
|
|
494
|
+
- ✅ Service description says "consumes", "listens to", "reacts to", "handles events"
|
|
495
|
+
- ✅ Service processes events published by other services
|
|
496
|
+
|
|
497
|
+
**CRITICAL: Event handlers MUST live in `src/events/` directory, NOT `src/handlers/`!**
|
|
498
|
+
|
|
499
|
+
## Event Handling Modes
|
|
500
|
+
|
|
501
|
+
The platform supports **Advanced Mode** (contracts) with automatic creation for missing contracts.
|
|
502
|
+
|
|
503
|
+
### 🎯 DECISION RULE (CRITICAL - READ FIRST!)
|
|
504
|
+
|
|
505
|
+
**Always use Advanced Mode (contracts) - Missing contracts are auto-created!**
|
|
506
|
+
|
|
507
|
+
When generating an Event Consumer service:
|
|
508
|
+
|
|
509
|
+
1. **Use contracts** from `{{scope}}/contracts` package
|
|
510
|
+
2. **Import and use** the contract in the event handler (`import { OrderCreatedContract } from '{{scope}}/contracts'`)
|
|
511
|
+
3. **If contract doesn't exist**, `pf event:generate` will create it automatically:
|
|
512
|
+
- Extracts event name from handler
|
|
513
|
+
- Creates contract in `packages/contracts/src/events/`
|
|
514
|
+
- Updates `packages/contracts/src/index.ts`
|
|
515
|
+
- Generates mock JSON file
|
|
516
|
+
|
|
517
|
+
**Example Flow:**
|
|
518
|
+
```
|
|
519
|
+
User prompt: "Event Consumer that listens to order.created events"
|
|
520
|
+
→ AI creates: src/events/order-created.event.ts (imports OrderCreatedContract from @scope/contracts)
|
|
521
|
+
→ pf event:generate runs:
|
|
522
|
+
✓ Detects missing contract
|
|
523
|
+
✓ Creates packages/contracts/src/events/order-created.ts (contract)
|
|
524
|
+
✓ Creates packages/contracts/src/events/order-created.mock.json
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
**Why This Approach?**
|
|
528
|
+
- ✅ AI generates contracts reference (type-safe from start)
|
|
529
|
+
- ✅ Automatic contract creation when missing
|
|
530
|
+
- ✅ End result: Strong type-safety via Schema Registry Pattern
|
|
531
|
+
- ✅ No manual contract creation needed
|
|
532
|
+
|
|
533
|
+
**IMPORTANT:** Always include `pf event:generate` in post-commands block for Event Consumers!
|
|
534
|
+
|
|
535
|
+
### 🟢 ADVANCED MODE (Default - Recommended)
|
|
536
|
+
|
|
537
|
+
**Use Advanced Mode for Event Consumers** - contracts will be auto-created if missing:
|
|
538
|
+
|
|
539
|
+
1. **Add dependency** `{{scope}}/contracts` in dependencies block
|
|
540
|
+
2. **Import contract** in event handler
|
|
541
|
+
3. **Use with `handleEvent`** for type-safe event handling
|
|
542
|
+
|
|
543
|
+
**Example Event Handler:**
|
|
138
544
|
```ts
|
|
139
545
|
import { handleEvent } from '@crossdelta/cloudevents'
|
|
140
|
-
import {
|
|
546
|
+
import { OrderCreatedContract, type OrderCreatedData } from '@scope/contracts'
|
|
141
547
|
import { processOrder } from '../use-cases/process-order.use-case'
|
|
142
548
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
549
|
+
export default handleEvent(OrderCreatedContract,
|
|
550
|
+
async (data: OrderCreatedData) => {
|
|
551
|
+
await processOrder(data)
|
|
552
|
+
},
|
|
553
|
+
)
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
**Why Contracts as Default?**
|
|
557
|
+
- ✅ Strong type-safety between Publisher and Consumer
|
|
558
|
+
- ✅ Single source of truth for event schemas
|
|
559
|
+
- ✅ Schema evolution and versioning
|
|
560
|
+
- ✅ Automatic mock generation
|
|
561
|
+
- ✅ Better documentation
|
|
562
|
+
|
|
563
|
+
**IMPORTANT:** The `packages/contracts` package is your Schema Registry. Always use contracts from there for Event Consumers.
|
|
564
|
+
|
|
565
|
+
### � ADVANCED MODE (Shared Contracts - RECOMMENDED DEFAULT)
|
|
566
|
+
|
|
567
|
+
Use **Advanced Mode** for all production services:
|
|
568
|
+
|
|
569
|
+
**Steps for Advanced Mode:**
|
|
570
|
+
|
|
571
|
+
1. **Check if contract exists** in `packages/contracts/src/events/`
|
|
572
|
+
2. **If exists:** Import and use the contract
|
|
573
|
+
3. **If NOT exists:** Create contract first in `packages/contracts`, then use it
|
|
574
|
+
|
|
575
|
+
**Advanced Mode Example:**
|
|
576
|
+
|
|
577
|
+
```ts
|
|
578
|
+
// packages/contracts/src/events/order-created.ts (create if not exists)
|
|
579
|
+
import { createContract } from '@crossdelta/cloudevents'
|
|
580
|
+
import { z } from 'zod'
|
|
581
|
+
|
|
582
|
+
export const OrderCreatedContract = createContract({
|
|
583
|
+
type: 'order.created',
|
|
584
|
+
schema: z.object({
|
|
585
|
+
orderId: z.string(),
|
|
586
|
+
customerId: z.string(),
|
|
587
|
+
items: z.array(z.object({
|
|
588
|
+
productId: z.string(),
|
|
589
|
+
quantity: z.number(),
|
|
590
|
+
price: z.number(),
|
|
591
|
+
})),
|
|
592
|
+
total: z.number(),
|
|
593
|
+
status: z.enum(['created']),
|
|
594
|
+
createdAt: z.string(),
|
|
595
|
+
}),
|
|
147
596
|
})
|
|
148
597
|
|
|
149
|
-
|
|
150
|
-
|
|
598
|
+
export type OrderCreatedData = z.infer<typeof OrderCreatedContract.schema>
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
```ts
|
|
602
|
+
// services/my-service/src/events/order-created.event.ts
|
|
603
|
+
import { handleEvent } from '@crossdelta/cloudevents'
|
|
604
|
+
import { OrderCreatedContract, type OrderCreatedData } from '@scope/contracts'
|
|
605
|
+
import { processOrder } from '../use-cases/process-order.use-case'
|
|
151
606
|
|
|
152
607
|
export default handleEvent(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
608
|
+
OrderCreatedContract, // Type-safe contract
|
|
609
|
+
async (data: OrderCreatedData) => {
|
|
610
|
+
await processOrder(data)
|
|
156
611
|
},
|
|
157
|
-
|
|
612
|
+
)
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
```ts
|
|
616
|
+
// services/my-service/src/use-cases/process-order.use-case.ts
|
|
617
|
+
import type { OrderCreatedData } from '@scope/contracts'
|
|
618
|
+
|
|
619
|
+
export const processOrder = async (data: OrderCreatedData) => {
|
|
620
|
+
// Business logic with fully typed data
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
**Dependencies:**
|
|
625
|
+
Add `{{scope}}/contracts` to your service's `package.json`:
|
|
626
|
+
```json
|
|
627
|
+
{
|
|
628
|
+
"dependencies": {
|
|
629
|
+
"@scope/contracts": "workspace:*"
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### � DEPRECATED: Basic Mode
|
|
635
|
+
|
|
636
|
+
**Basic Mode is deprecated and no longer recommended.**
|
|
637
|
+
|
|
638
|
+
The platform automatically creates missing contracts when you run `pf event:generate`.
|
|
639
|
+
|
|
640
|
+
**Previous Basic Mode approach:**
|
|
641
|
+
- ❌ Local schemas in `src/types/events.ts`
|
|
642
|
+
- ❌ String literals for event types
|
|
643
|
+
- ❌ No type-safety between services
|
|
644
|
+
|
|
645
|
+
**Current approach (Advanced Mode only):**
|
|
646
|
+
- ✅ Always use contracts from `{{scope}}/contracts`
|
|
647
|
+
- ✅ Contracts auto-created if missing
|
|
648
|
+
- ✅ Type-safety across all services
|
|
649
|
+
- ✅ Single source of truth
|
|
650
|
+
|
|
651
|
+
**Migration:** If you have Basic Mode handlers, run `pf event:generate` to automatically migrate them to Advanced Mode.
|
|
652
|
+
|
|
653
|
+
export const processOrder = async (data: OrderCreatedData) => {
|
|
654
|
+
// Use data.orderId, data.customerId, etc.
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
{{AVAILABLE_CONTRACTS}}
|
|
659
|
+
|
|
660
|
+
**DO NOT:**
|
|
661
|
+
- ❌ Redefine schemas that exist in contracts
|
|
662
|
+
- ❌ Create inline schemas for existing event types
|
|
663
|
+
- ❌ Duplicate event type definitions
|
|
664
|
+
|
|
665
|
+
**Advanced Mode Example:**
|
|
666
|
+
```ts
|
|
667
|
+
import { handleEvent } from '@crossdelta/cloudevents'
|
|
668
|
+
import { OrderCreatedContract, type OrderCreatedData } from '{{scope}}/contracts'
|
|
669
|
+
import { processOrder } from '../use-cases/process-order.use-case'
|
|
670
|
+
|
|
671
|
+
export default handleEvent(
|
|
672
|
+
OrderCreatedContract, // Import from contracts package
|
|
673
|
+
async (data: OrderCreatedData) => {
|
|
158
674
|
await processOrder(data)
|
|
159
675
|
},
|
|
160
676
|
)
|
|
161
677
|
```
|
|
162
678
|
|
|
679
|
+
**If you need only a subset of fields**, extract them in the handler:
|
|
680
|
+
```ts
|
|
681
|
+
export default handleEvent(
|
|
682
|
+
OrderCreatedContract,
|
|
683
|
+
async (data: OrderCreatedData) => {
|
|
684
|
+
// Extract only what you need
|
|
685
|
+
const { orderId, items } = data
|
|
686
|
+
await updateInventory({ orderId, items })
|
|
687
|
+
},
|
|
688
|
+
)
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
**Contract Definition** (in `packages/contracts/src/schemas/order-created.schema.ts`):
|
|
692
|
+
```ts
|
|
693
|
+
import { createContract } from '@crossdelta/cloudevents'
|
|
694
|
+
import { z } from 'zod'
|
|
695
|
+
|
|
696
|
+
export const OrderCreatedSchema = z.object({
|
|
697
|
+
orderId: z.string(),
|
|
698
|
+
customerId: z.string(),
|
|
699
|
+
total: z.number(),
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
export type OrderCreatedData = z.infer<typeof OrderCreatedSchema>
|
|
703
|
+
|
|
704
|
+
export const OrderCreatedContract = createContract({
|
|
705
|
+
type: 'orders.created',
|
|
706
|
+
schema: OrderCreatedSchema,
|
|
707
|
+
})
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
## General Event Handler Requirements
|
|
711
|
+
|
|
712
|
+
- Live under `src/events/*.event.ts` (NOT `src/handlers/`)
|
|
713
|
+
- Use Zod for validation (NO `type` field in schema!)
|
|
714
|
+
- Delegate ALL logic to use-cases
|
|
715
|
+
- Contain NO business logic
|
|
716
|
+
- **ALWAYS import from `@crossdelta/cloudevents`** (NOT `shared-events` or any other package!)
|
|
717
|
+
|
|
718
|
+
**CRITICAL:** Schema validates ONLY the event data payload (without `type` field). Event type is declared in the options object or contract.
|
|
719
|
+
|
|
720
|
+
**CRITICAL:** ALWAYS use the exact import: `import { handleEvent } from '@crossdelta/cloudevents'`
|
|
721
|
+
|
|
163
722
|
**Naming conventions:**
|
|
164
723
|
- Schema constants: PascalCase with `Schema` suffix (e.g., `OrderCreatedSchema`)
|
|
165
|
-
- Exported types: PascalCase with `
|
|
724
|
+
- Exported types: PascalCase with `Data` suffix (e.g., `OrderCreatedData`)
|
|
725
|
+
- Contract constants: PascalCase with `Contract` suffix (e.g., `OrderCreatedContract`)
|
|
726
|
+
|
|
727
|
+
**Contract Structure (Advanced Mode - Default):**
|
|
728
|
+
```ts
|
|
729
|
+
// packages/contracts/src/events/order-created.ts
|
|
730
|
+
import { createContract } from '@crossdelta/cloudevents'
|
|
731
|
+
import { z } from 'zod'
|
|
732
|
+
|
|
733
|
+
export const OrderCreatedSchema = z.object({
|
|
734
|
+
orderId: z.string(),
|
|
735
|
+
customerId: z.string(),
|
|
736
|
+
total: z.number(),
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
export const OrderCreatedContract = createContract({
|
|
740
|
+
type: 'order.created',
|
|
741
|
+
schema: OrderCreatedSchema,
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
export type OrderCreatedData = z.infer<typeof OrderCreatedContract.schema>
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
**Decision Rule:**
|
|
748
|
+
- **Always use Advanced Mode** - contracts from `{{scope}}/contracts`
|
|
749
|
+
- **Contracts auto-created** - `pf event:generate` creates missing contracts
|
|
750
|
+
- Meaning → `packages/contracts`
|
|
751
|
+
- Behavior → `services/*/src`
|
|
166
752
|
|
|
167
753
|
Forbidden in handlers:
|
|
168
754
|
- `type: z.literal('...')` in schema (redundant and causes errors)
|
|
169
755
|
- DB access
|
|
170
|
-
- External API calls
|
|
171
756
|
- Multi-step logic
|
|
172
757
|
- Env var parsing
|
|
173
758
|
|
|
174
759
|
---
|
|
175
760
|
|
|
176
|
-
#
|
|
761
|
+
# 9. Testing Rules
|
|
177
762
|
|
|
178
763
|
**CRITICAL:**
|
|
179
764
|
- Test ONLY use-cases (NOT event handlers)
|
|
@@ -223,30 +808,134 @@ describe('Send Notification Use Case', () => {
|
|
|
223
808
|
|
|
224
809
|
---
|
|
225
810
|
|
|
226
|
-
#
|
|
811
|
+
# 10. README Requirements
|
|
227
812
|
|
|
228
813
|
MUST include:
|
|
229
814
|
- Description
|
|
815
|
+
- Features list
|
|
230
816
|
- Environment variable table
|
|
231
|
-
- Published/consumed events
|
|
817
|
+
- Published/consumed events (with schemas)
|
|
232
818
|
- API endpoints
|
|
819
|
+
- **Testing section with event mock examples**
|
|
233
820
|
- Dev commands
|
|
234
821
|
|
|
822
|
+
### README Structure
|
|
823
|
+
|
|
824
|
+
```markdown
|
|
825
|
+
# Service Name
|
|
826
|
+
|
|
827
|
+
Brief description of what this service does.
|
|
828
|
+
|
|
829
|
+
## Features
|
|
830
|
+
|
|
831
|
+
- Feature 1
|
|
832
|
+
- Feature 2
|
|
833
|
+
|
|
834
|
+
## Environment Variables
|
|
835
|
+
|
|
836
|
+
| Variable | Description | Required | Default |
|
|
837
|
+
|----------|-------------|----------|---------|
|
|
838
|
+
| `SERVICE_PORT` | Port the service runs on | No | 4001 |
|
|
839
|
+
| `NATS_URL` | NATS server URL | Yes | - |
|
|
840
|
+
|
|
841
|
+
## Events
|
|
842
|
+
|
|
843
|
+
### Publishes
|
|
844
|
+
- `orderboss.service.event` - Description
|
|
845
|
+
```json
|
|
846
|
+
{
|
|
847
|
+
"field": "value"
|
|
848
|
+
}
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
### Consumes
|
|
852
|
+
- `orderboss.other.event` - Description
|
|
853
|
+
```json
|
|
854
|
+
{
|
|
855
|
+
"field": "value"
|
|
856
|
+
}
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
## API Endpoints
|
|
860
|
+
|
|
861
|
+
### `GET /health`
|
|
862
|
+
Health check endpoint.
|
|
863
|
+
|
|
864
|
+
**Response:**
|
|
865
|
+
\`\`\`json
|
|
866
|
+
{ "status": "ok" }
|
|
867
|
+
\`\`\`
|
|
868
|
+
|
|
869
|
+
## Testing
|
|
870
|
+
|
|
871
|
+
### Event Mocks
|
|
872
|
+
|
|
873
|
+
This service includes generated mock files for all event handlers in `src/events/*.mock.json`. These mocks can be used for testing and development.
|
|
874
|
+
|
|
875
|
+
**List all available event mocks:**
|
|
876
|
+
\`\`\`bash
|
|
877
|
+
pf event:list
|
|
878
|
+
\`\`\`
|
|
879
|
+
|
|
880
|
+
**Publish a test event using a mock:**
|
|
881
|
+
\`\`\`bash
|
|
882
|
+
pf event:publish order-created
|
|
883
|
+
\`\`\`
|
|
884
|
+
|
|
885
|
+
**Regenerate event mocks:**
|
|
886
|
+
\`\`\`bash
|
|
887
|
+
pf event:generate . --overwrite
|
|
888
|
+
\`\`\`
|
|
889
|
+
|
|
890
|
+
### Manual Testing
|
|
891
|
+
|
|
892
|
+
To test event handlers manually:
|
|
893
|
+
|
|
894
|
+
1. Start the service:
|
|
895
|
+
\`\`\`bash
|
|
896
|
+
bun dev
|
|
897
|
+
\`\`\`
|
|
898
|
+
|
|
899
|
+
2. In another terminal, publish a test event:
|
|
900
|
+
\`\`\`bash
|
|
901
|
+
pf event:publish order-created
|
|
902
|
+
\`\`\`
|
|
903
|
+
|
|
904
|
+
3. Check the service logs for the handler output
|
|
905
|
+
|
|
906
|
+
### Unit Tests
|
|
907
|
+
|
|
908
|
+
Run use-case tests:
|
|
909
|
+
\`\`\`bash
|
|
910
|
+
bun test
|
|
911
|
+
\`\`\`
|
|
912
|
+
|
|
913
|
+
## Development
|
|
914
|
+
|
|
915
|
+
\`\`\`bash
|
|
916
|
+
bun dev # Start in development mode
|
|
917
|
+
bun test # Run tests
|
|
918
|
+
bun lint # Check code quality
|
|
919
|
+
\`\`\`
|
|
920
|
+
```
|
|
921
|
+
|
|
235
922
|
---
|
|
236
923
|
|
|
237
|
-
#
|
|
924
|
+
# 11. Absolute Rules
|
|
238
925
|
|
|
239
926
|
DO NOT:
|
|
240
927
|
- Generate full rewrites
|
|
241
928
|
- Add new architecture concepts
|
|
242
929
|
- Use raw NATS clients
|
|
243
|
-
- Put logic in handlers or index.ts
|
|
930
|
+
- Put logic in event handlers or index.ts
|
|
244
931
|
- Insert semicolons
|
|
245
932
|
- Add unused folders/files
|
|
933
|
+
- **Use `src/handlers/` directory (use `src/events/` instead!)**
|
|
246
934
|
|
|
247
935
|
DO:
|
|
248
936
|
- Produce minimal, clean code
|
|
249
937
|
- Follow structure and conventions strictly
|
|
250
938
|
- Keep output compact and correct
|
|
939
|
+
- **Always place event handlers in `src/events/` directory**
|
|
251
940
|
|
|
252
941
|
---
|