@crossdelta/platform-sdk 0.13.3 → 0.13.4

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.
Files changed (42) hide show
  1. package/bin/cli.js +312 -0
  2. package/bin/docs/generators/README.md +56 -0
  3. package/bin/docs/generators/code-style.md +96 -0
  4. package/bin/docs/generators/hono-bun.md +181 -0
  5. package/bin/docs/generators/hono-node.md +194 -0
  6. package/bin/docs/generators/nest.md +358 -0
  7. package/bin/docs/generators/service.md +564 -0
  8. package/bin/docs/generators/testing.md +97 -0
  9. package/bin/integration.collection.json +18 -0
  10. package/bin/templates/hono-microservice/Dockerfile.hbs +16 -0
  11. package/bin/templates/hono-microservice/biome.json.hbs +3 -0
  12. package/bin/templates/hono-microservice/src/index.ts.hbs +18 -0
  13. package/bin/templates/hono-microservice/tsconfig.json.hbs +14 -0
  14. package/bin/templates/nest-microservice/Dockerfile.hbs +37 -0
  15. package/bin/templates/nest-microservice/biome.json.hbs +3 -0
  16. package/bin/templates/nest-microservice/src/app.context.ts.hbs +17 -0
  17. package/bin/templates/nest-microservice/src/events/events.module.ts.hbs +8 -0
  18. package/bin/templates/nest-microservice/src/events/events.service.ts.hbs +22 -0
  19. package/bin/templates/nest-microservice/src/main.ts.hbs +34 -0
  20. package/bin/templates/workspace/biome.json.hbs +62 -0
  21. package/bin/templates/workspace/bunfig.toml.hbs +5 -0
  22. package/bin/templates/workspace/editorconfig.hbs +9 -0
  23. package/bin/templates/workspace/gitignore.hbs +15 -0
  24. package/bin/templates/workspace/infra/Pulumi.dev.yaml.hbs +5 -0
  25. package/bin/templates/workspace/infra/Pulumi.yaml.hbs +6 -0
  26. package/bin/templates/workspace/infra/index.ts.hbs +56 -0
  27. package/bin/templates/workspace/infra/package.json.hbs +23 -0
  28. package/bin/templates/workspace/infra/tsconfig.json.hbs +15 -0
  29. package/bin/templates/workspace/npmrc.hbs +2 -0
  30. package/bin/templates/workspace/package.json.hbs +51 -0
  31. package/bin/templates/workspace/packages/contracts/README.md.hbs +166 -0
  32. package/bin/templates/workspace/packages/contracts/package.json.hbs +22 -0
  33. package/bin/templates/workspace/packages/contracts/src/events/index.ts +16 -0
  34. package/bin/templates/workspace/packages/contracts/src/index.ts +10 -0
  35. package/bin/templates/workspace/packages/contracts/src/stream-policies.ts.hbs +40 -0
  36. package/bin/templates/workspace/packages/contracts/tsconfig.json.hbs +7 -0
  37. package/bin/templates/workspace/pnpm-workspace.yaml.hbs +5 -0
  38. package/bin/templates/workspace/turbo.json +38 -0
  39. package/bin/templates/workspace/turbo.json.hbs +29 -0
  40. package/install.sh +46 -8
  41. package/package.json +1 -3
  42. package/scripts/postinstall.js +0 -53
@@ -0,0 +1,564 @@
1
+ # Service Generator Instructions
2
+
3
+ **Generic guidelines for all service types. Framework-specific details are in separate files:**
4
+ - `hono-bun.md` - Hono with Bun runtime
5
+ - `hono-node.md` - Hono with Node.js runtime
6
+ - `nest.md` - NestJS framework
7
+
8
+ ---
9
+
10
+ ## 🚨 CRITICAL: Port Configuration
11
+
12
+ Services MUST read their port from environment variables using this pattern:
13
+
14
+ ```typescript
15
+ // Convert service name to ENV key format
16
+ // Example: my-hono-service → MY_HONO_SERVICE_PORT
17
+ const port = Number(process.env.MY_HONO_SERVICE_PORT) || 8080
18
+ ```
19
+
20
+ **Port ENV variable naming:**
21
+ - Take service name from package.json `name` field (without scope)
22
+ - Convert to SCREAMING_SNAKE_CASE
23
+ - Append `_PORT`
24
+
25
+ **Examples:**
26
+ - `@my-platform/orders` → `ORDERS_PORT`
27
+ - `@my-platform/my-nest-service` → `MY_NEST_SERVICE_PORT`
28
+ - `@my-platform/api-gateway` → `API_GATEWAY_PORT`
29
+
30
+ **⚠️ CRITICAL: DO NOT hardcode or modify the port!**
31
+ - The `pf new` command automatically assigns a unique port
32
+ - The port is written to `.env.local` (e.g., `MY_SERVICE_PORT=4001`)
33
+ - Service code MUST use the correct env variable name
34
+ - DO NOT change the port in generated files!
35
+
36
+ **When generating service code:**
37
+ 1. Determine the correct env variable name from service name
38
+ 2. Use ONLY that variable: `process.env.MY_SERVICE_PORT`
39
+ 3. DO NOT use generic `PORT` - it causes conflicts!
40
+ 4. Keep the `|| 8080` fallback for when `.env.local` is missing
41
+
42
+ ---
43
+
44
+ ## 🚨 CRITICAL: Stream Architecture
45
+
46
+ > **Services NEVER create streams!**
47
+ >
48
+ > In development, `pf dev` ensures ephemeral streams derived from contracts.
49
+ > In production, streams are materialized explicitly via Pulumi.
50
+
51
+ ---
52
+
53
+ ## Development Workflow
54
+
55
+ ### 1️⃣ **Initial Setup** (once per workspace)
56
+
57
+ ```bash
58
+ pf new workspace my-platform
59
+ cd my-platform
60
+ ```
61
+
62
+ ### 2️⃣ **Create Service** (AI or manual)
63
+
64
+ #### Option A: AI Generation (via GitHub Copilot Chat)
65
+ ```
66
+ "Create an order processing service that consumes orders.created events"
67
+ ```
68
+
69
+ **What AI generates:**
70
+ 1. **Contract** (`packages/contracts/src/events/orders/created.ts`) - With proper Zod schema in domain-grouped structure
71
+ 2. **Service** (`services/order-processing/`) - Scaffolded via `pf new hono-micro`
72
+ 3. **Event Handler** (`services/order-processing/src/events/orders-created.handler.ts`)
73
+ 4. **Use-Cases** (`services/order-processing/src/use-cases/`)
74
+ 5. **Tests** (`services/order-processing/src/use-cases/*.test.ts`)
75
+
76
+ **What AI includes in response:**
77
+ ```commands
78
+ pf new hono-micro services/order-processing -y
79
+ ```
80
+
81
+ ```post-commands
82
+ pf event add orders.created --service services/order-processing
83
+ ```
84
+
85
+ **What `pf event add` does:**
86
+ - Creates `packages/contracts/src/events/orders-created.mock.json` (test mock)
87
+ - Adds export to `packages/contracts/src/index.ts`
88
+ - Skips contract creation if already exists (AI's schema is preserved!)
89
+
90
+ #### Option B: Manual Creation
91
+
92
+ **Step 1: Create contract with CLI**
93
+ ```bash
94
+ # Create contract with schema
95
+ pf event add orders.created --fields "orderId:string,total:number,customerId:string"
96
+
97
+ # Or with JSON schema
98
+ pf event add orders.created --schema '{"orderId":"string","total":"number"}'
99
+ ```
100
+
101
+ **Step 2: Scaffold service**
102
+ ```bash
103
+ pf new hono-micro services/order-processing
104
+ ```
105
+
106
+ **Step 3: Register event and create handler**
107
+ ```bash
108
+ pf event add orders.created --service services/order-processing
109
+ ```
110
+
111
+ **Step 4: Implement use-cases**
112
+ ```bash
113
+ # Edit services/order-processing/src/use-cases/process-order.use-case.ts
114
+ ```
115
+
116
+ #### Option C: Pure Manual (no CLI)
117
+
118
+ ```bash
119
+ # 1. Create contract manually (domain-grouped)
120
+ mkdir -p packages/contracts/src/events/orders
121
+ # Edit packages/contracts/src/events/orders/created.ts
122
+
123
+ # 2. Scaffold service
124
+ pf new hono-micro services/order-processing
125
+
126
+ # 3. Create handler manually
127
+ # Edit services/order-processing/src/events/orders-created.handler.ts
128
+
129
+ # 4. Implement use-cases
130
+ # Edit services/order-processing/src/use-cases/
131
+ ```
132
+
133
+ ### 3️⃣ **Run Development Environment**
134
+
135
+ ```bash
136
+ pf dev # Starts NATS + creates ephemeral streams from contracts + all services
137
+ ```
138
+
139
+ **What happens:**
140
+ - **NATS** starts in Docker with JetStream enabled
141
+ - **Ephemeral streams** auto-created from contract channel metadata (memory storage, 1h retention)
142
+ - **Services** start and consume from streams
143
+ - **Hot reload** watches for changes
144
+
145
+ **Stream Creation:** `pf dev` scans `packages/contracts/src/index.ts` for contracts with `channel.stream` metadata and creates ephemeral streams automatically. No manual stream creation needed!
146
+
147
+ ### 4️⃣ **Deploy to Production**
148
+
149
+ ```bash
150
+ cd infra
151
+ pulumi up
152
+ ```
153
+
154
+ **What happens:**
155
+ 1. **`infra/streams/`** collects contracts via `collectStreamDefinitions()`
156
+ 2. **Streams materialized** via Pulumi with retention policies
157
+ 3. **Services deployed** to Kubernetes
158
+
159
+ ---
160
+
161
+ ## �🚨 CRITICAL: Stream Architecture
162
+
163
+ > **Services NEVER create streams!**
164
+ > **Dev:** Streams are auto-created (ephemeral, memory)
165
+ > **Prod:** Streams are materialized via Pulumi from contracts
166
+
167
+ ### Contracts → Streams → Infra Flow
168
+
169
+ ```typescript
170
+ // 1. Contract defines conceptually (packages/contracts)
171
+ export const OrdersCreatedContract = createContract({
172
+ type: 'orders.created',
173
+ channel: { stream: 'ORDERS' }, // Routing metadata
174
+ schema: OrderSchema,
175
+ })
176
+
177
+ // 2. Services consume (no stream creation!)
178
+ consumeJetStreams({
179
+ streams: ['ORDERS'],
180
+ consumer: 'my-service',
181
+ discover: './src/events/**/*.handler.ts',
182
+ })
183
+
184
+ // 3. Infra materializes (infra/streams/)
185
+ import { collectStreamDefinitions } from '@crossdelta/infrastructure'
186
+ import * as contracts from '@workspace/contracts'
187
+
188
+ // Generic logic in library, workspace data injected
189
+ const streams = collectStreamDefinitions(contracts)
190
+ deployStreams(provider, namespace, streams)
191
+ ```
192
+
193
+ **Architecture layers:**
194
+ - **@crossdelta/infrastructure:** Generic stream collection logic
195
+ - **Contracts:** Define event semantics + channel routing (workspace data)
196
+ - **`pf dev`:** Ensures ephemeral streams (transient, memory-only)
197
+ - **Platform-Infra:** Materializes streams via Pulumi (persistent, retention)
198
+ - **Services:** Consume streams (never create!)
199
+
200
+ **Separation of concerns:**
201
+ - **Libraries contain logic** (generic, reusable)
202
+ - **Workspaces contain data** (contracts, policies)
203
+ - **No workspace imports inside libraries**
204
+
205
+ ---
206
+
207
+ ## 🚨 CRITICAL: @crossdelta/cloudevents API
208
+
209
+ **ONLY these exports exist:**
210
+ - `consumeJetStreams` (preferred) - Consume from multiple streams
211
+ - `consumeJetStreamEvents` (legacy) - Consume from single stream
212
+ - `handleEvent`, `publish`, `createContract`
213
+
214
+ ```ts
215
+ // Services only consume (DO NOT use ensureJetStreams!)
216
+ consumeJetStreams({
217
+ streams: ['ORDERS'],
218
+ consumer: 'my-service',
219
+ discover: './src/events/**/*.handler.ts',
220
+ })
221
+ ```
222
+
223
+ **DO NOT USE in services:**
224
+ - ❌ `ensureJetStreams()` - Only for infra materialization
225
+ - ❌ `ensureJetStreamStream()` - Legacy, only for infra
226
+ - ❌ Manual stream creation - Services never create streams!
227
+
228
+ ---
229
+
230
+ ## 🚨 CRITICAL: @crossdelta/telemetry Import
231
+
232
+ ```ts
233
+ import '@crossdelta/telemetry' // side-effect, MUST be FIRST import
234
+ ```
235
+
236
+ **DO NOT USE:** `@crossdelta/telemetry/register`, named exports
237
+
238
+ ---
239
+
240
+ ## 🚨 CRITICAL: Commands Block (REQUIRED - MUST BE FIRST)
241
+
242
+ Your response **MUST START** with a `commands` block that scaffolds the service.
243
+ The exact command depends on the framework - see the framework-specific docs:
244
+ - `hono-bun.md` / `hono-node.md` → `pf new hono-micro`
245
+ - `nest.md` → `pf new nest-micro`
246
+
247
+ **Without this command, the service cannot be built!**
248
+
249
+ **What `pf new` creates automatically:**
250
+ 1. Service directory structure (`services/<name>/`)
251
+ 2. Infrastructure config (`infra/services/<name>.ts`) with assigned port
252
+ 3. Package.json with dependencies
253
+ 4. Entry point (`src/index.ts` or `src/main.ts`) with port configuration
254
+ 5. Health check endpoint
255
+ 6. Dockerfile
256
+
257
+ **⚠️ AI MUST NOT generate `infra/services/<name>.ts`** - This file is automatically created by the CLI with the correct port assignment!
258
+
259
+ ---
260
+
261
+ ## 🚨 CRITICAL: Post-Commands Block (REQUIRED for Event Consumers)
262
+
263
+ **For Event Consumer services, include a `post-commands` block with ALL events:**
264
+
265
+ ```post-commands
266
+ pf event add orders.created --service services/my-service
267
+ pf event add customers.updated --service services/my-service
268
+ ```
269
+
270
+ **What `pf event add` creates:**
271
+ - `packages/contracts/src/events/<event>.mock.json` - Test mock data
272
+ - Adds export to `packages/contracts/src/index.ts`
273
+
274
+ **⚠️ IMPORTANT:** `pf event add` skips contract creation if it already exists - so YOUR contract with the correct schema is preserved!
275
+
276
+ ---
277
+
278
+ ## 🚨 CRITICAL: Event Consumer Workflow
279
+
280
+ **FOR EVENT CONSUMER SERVICES (services that "consume", "listen to", "react to" events):**
281
+
282
+ ### What YOU (the AI) generate:
283
+
284
+ 1. **`packages/contracts/src/events/<domain>/<event>.ts`** - Contract with correct schema fields in domain-grouped structure (e.g., `events/orders/created.ts`)
285
+ 2. **`src/events/<event>.handler.ts`** - Event handler that calls the use-case
286
+ 3. **`src/use-cases/*.use-case.ts`** - Business logic (called by handlers)
287
+ 4. **`src/use-cases/*.test.ts`** - Tests for use-cases
288
+ 5. **`README.md`** - Documentation
289
+
290
+ **⚠️ DO NOT generate `src/index.ts` or `src/main.ts`** - These are created by `pf new` with correct port config!
291
+
292
+ ### Handler Location (CRITICAL!)
293
+
294
+ **⚠️ MUST be in `src/events/*.handler.ts` - NEVER in `handlers/` subdirectory**
295
+
296
+ ```
297
+ ✅ CORRECT:
298
+ services/my-service/src/events/
299
+ ├── orders-created.handler.ts
300
+ └── customers-updated.handler.ts
301
+
302
+ ❌ WRONG:
303
+ services/my-service/src/events/handlers/ # NEVER create!
304
+ ```
305
+
306
+ ### Handler Export Pattern
307
+
308
+ ```ts
309
+ // ✅ CORRECT: Default export
310
+ export default handleEvent(OrdersCreatedContract, async (data) => { ... })
311
+
312
+ // ❌ WRONG: Named export
313
+ export const OrdersCreatedHandler = handleEvent(...)
314
+ ```
315
+
316
+ ### Handler Logging
317
+
318
+ **Handlers must be thin** - use `console.log` for logging, then delegate to use-case/service:
319
+
320
+ ```ts
321
+ export default handleEvent(OrdersCreatedContract, async (data) => {
322
+ console.log(`[orders.created] Processing orderId=${data.orderId}`) // ✅ console.log
323
+ await processOrder(data)
324
+ })
325
+ ```
326
+
327
+ **Services/use-cases** can use structured logging (NestJS: `new Logger(ServiceName.name)`).
328
+
329
+ ---
330
+
331
+ ## 🚨 Response Structure
332
+
333
+ ```commands
334
+ pf new hono-micro services/my-service -y
335
+ ```
336
+
337
+ #### `packages/contracts/src/events/orders/created.ts`
338
+ ```ts
339
+ import { createContract } from '@crossdelta/cloudevents'
340
+ import { z } from 'zod'
341
+
342
+ export const OrdersCreatedSchema = z.object({
343
+ orderId: z.string(),
344
+ total: z.number(),
345
+ })
346
+
347
+ export const OrdersCreatedContract = createContract({
348
+ type: 'orders.created',
349
+ channel: { stream: 'ORDERS' },
350
+ schema: OrdersCreatedSchema,
351
+ })
352
+
353
+ export type OrdersCreatedData = z.infer<typeof OrdersCreatedContract.schema>
354
+ ```
355
+
356
+ #### `src/events/orders-created.handler.ts`
357
+ ```ts
358
+ import { handleEvent } from '@crossdelta/cloudevents'
359
+ import { OrdersCreatedContract, type OrdersCreatedData } from '{{scope}}/contracts'
360
+ import { processOrder } from '../use-cases/process-order.use-case'
361
+
362
+ export default handleEvent(OrdersCreatedContract, async (data: OrdersCreatedData) => {
363
+ console.log('📦 Processing order:', data.orderId)
364
+ await processOrder(data)
365
+ })
366
+ ```
367
+
368
+ #### `src/use-cases/process-order.use-case.ts`
369
+ ```ts
370
+ import type { OrdersCreatedData } from '{{scope}}/contracts'
371
+
372
+ export const processOrder = async (data: OrdersCreatedData): Promise<void> => {
373
+ console.log('Processing:', data.orderId)
374
+ }
375
+ ```
376
+
377
+ ```post-commands
378
+ pf event add orders.created --service services/my-service
379
+ ```
380
+
381
+ ```dependencies
382
+ @pusher/push-notifications-server
383
+ ```
384
+
385
+ ---
386
+
387
+ ## 🚨 CRITICAL: Schema Fields Consistency
388
+
389
+ **ONLY use fields that exist in YOUR generated schema!**
390
+
391
+ ```ts
392
+ // If you generate this schema:
393
+ export const DomainCreatedSchema = z.object({
394
+ domainId: z.string(),
395
+ name: z.string(),
396
+ ownerEmail: z.string().email(),
397
+ })
398
+
399
+ // Then ONLY use these fields in service code:
400
+ await sendEmail(data.ownerEmail) // ✅ EXISTS in schema
401
+ await notifyUser(data.userId) // ❌ WRONG - userId not in schema!
402
+ ```
403
+
404
+ **DO NOT hallucinate fields** like `userId`, `customerId`, etc. unless they are in YOUR schema.
405
+
406
+ ---
407
+
408
+ ## Contract Schema (Zod 4 Syntax)
409
+
410
+ ```ts
411
+ export const OrdersCreatedSchema = z.object({
412
+ orderId: z.string(),
413
+ email: z.string().email(), // ✅ Zod 4: no params
414
+ total: z.number(),
415
+ createdAt: z.string().datetime(), // ✅ Zod 4: no params
416
+ })
417
+ ```
418
+
419
+ **Zod 4 Quick Reference:**
420
+ - ✅ `z.string().email()` - no params
421
+ - ✅ `z.string().url()` - no params
422
+ - ❌ `z.string().email('Invalid')` - DEPRECATED
423
+
424
+ ---
425
+
426
+ ## Naming Convention
427
+
428
+ **Architecture Rule:**
429
+
430
+ | Component | Format | Example | Reason |
431
+ |-----------|--------|---------|--------|
432
+ | **Event Type** | **Singular** | `domain.created` | Describes a single event |
433
+ | **Stream** | **PLURAL** | `DOMAINS` | Collection of events for a domain |
434
+
435
+ **Merksatz:** Events sind singular. Streams sind plural.
436
+
437
+ ### Examples
438
+
439
+ | Event Type | Contract | File | Stream |
440
+ |------------|----------|------|--------|
441
+ | `orders.created` | `OrdersCreatedContract` | `orders-created.ts` | `ORDERS` ✅ |
442
+ | `domain.created` | `DomainCreatedContract` | `domain-created.ts` | `DOMAINS` ✅ |
443
+
444
+ ```typescript
445
+ // ✅ CORRECT
446
+ export const DomainCreatedContract = createContract({
447
+ type: 'domain.created', // Singular event
448
+ channel: { stream: 'DOMAINS' }, // Plural stream
449
+ schema: DomainCreatedSchema,
450
+ })
451
+
452
+ export const OrdersCreatedContract = createContract({
453
+ type: 'orders.created', // Singular event (but namespace plural!)
454
+ channel: { stream: 'ORDERS' }, // Plural stream
455
+ schema: OrdersCreatedSchema,
456
+ })
457
+
458
+ // ❌ WRONG
459
+ export const DomainCreatedContract = createContract({
460
+ type: 'domain.created',
461
+ channel: { stream: 'DOMAIN' }, // ❌ Must be DOMAINS (plural!)
462
+ schema: DomainCreatedSchema,
463
+ })
464
+ ```
465
+
466
+ ### Stream Names in consumeJetStreams
467
+
468
+ **CRITICAL:** When consuming from streams, use the **PLURAL** stream name from the contract's `channel.stream`:
469
+
470
+ ```typescript
471
+ // ✅ CORRECT - domain.created event → DOMAINS stream
472
+ consumeJetStreams({
473
+ streams: ['DOMAINS'], // ✅ Plural!
474
+ consumer: 'my-service',
475
+ discover: './src/events/**/*.handler.ts',
476
+ })
477
+
478
+ // ✅ CORRECT - orders.created event → ORDERS stream
479
+ consumeJetStreams({
480
+ streams: ['ORDERS'], // ✅ Plural!
481
+ consumer: 'my-service',
482
+ discover: './src/events/**/*.handler.ts',
483
+ })
484
+
485
+ // ❌ WRONG - Don't use singular!
486
+ consumeJetStreams({
487
+ streams: ['DOMAIN'], // ❌ Wrong! Should be DOMAINS
488
+ consumer: 'my-service',
489
+ discover: './src/events/**/*.handler.ts',
490
+ })
491
+
492
+ // ❌ WRONG - Don't use lowercase!
493
+ consumeJetStreams({
494
+ streams: ['domains'], // ❌ Wrong! Should be DOMAINS (uppercase)
495
+ consumer: 'my-service',
496
+ discover: './src/events/**/*.handler.ts',
497
+ })
498
+ ```
499
+
500
+ **Rule:** Extract the stream name from your contract's `channel.stream` field and use it EXACTLY in `consumeJetStreams({ streams: ['...'] })`.
501
+
502
+ ---
503
+
504
+ ## Block Explanations
505
+
506
+ | Block | Purpose |
507
+ |-------|---------|
508
+ | `commands` | Scaffolds service (REQUIRED FIRST) |
509
+ | `post-commands` | Runs after files created (e.g., `pf event add`) |
510
+ | `dependencies` | Extra npm packages (NOT `@crossdelta/*`) |
511
+
512
+ ---
513
+
514
+ ## Service Types
515
+
516
+ ### 📤 Event Publisher (REST API)
517
+ **Keywords:** "publishes", "creates", "manages", "REST API"
518
+
519
+ ### 📥 Event Consumer (NATS)
520
+ **Keywords:** "consumes", "listens to", "reacts to", "handles events"
521
+
522
+
523
+ ### 🔄 Hybrid (Both)
524
+ Combines REST endpoints + NATS consumer.
525
+
526
+ ---
527
+
528
+ ## Required Files (Hono)
529
+
530
+ ```
531
+ services/my-service/src/
532
+ ├── index.ts
533
+ ├── events/orders-created.handler.ts
534
+ └── use-cases/
535
+ ├── process-order.use-case.ts
536
+ └── process-order.test.ts
537
+
538
+ packages/contracts/src/events/
539
+ └── orders-created.ts
540
+ ```
541
+
542
+ **Note:** For NestJS structure, see `nest.md` - NestJS uses Services instead of use-cases.
543
+
544
+ ---
545
+
546
+ ## Absolute Rules
547
+
548
+ **DO NOT:**
549
+ - ❌ Use raw NATS clients
550
+ - ❌ Put business logic in handlers or entry points
551
+ - ❌ Use `src/handlers/` (use `src/events/`)
552
+ - ❌ Create `src/types/` directory (use contracts)
553
+ - ❌ Insert semicolons
554
+ - ❌ Edit `packages/contracts/src/index.ts` (CLI handles exports)
555
+ - ❌ Create `use-cases/` folder in NestJS (use Services instead)
556
+
557
+ **DO:**
558
+ - ✅ Contracts in `packages/contracts/src/events/`
559
+ - ✅ Handlers in `src/events/*.handler.ts`
560
+ - ✅ Hono: Use-cases in `src/use-cases/*.use-case.ts`
561
+ - ✅ NestJS: Business logic in Services + pure helper functions
562
+ - ✅ Log event type and key identifier in handlers
563
+ - ✅ Keep handlers thin - delegate to use-cases/services
564
+ - ✅ Verify package names on npmjs.com before using
@@ -0,0 +1,97 @@
1
+ # Testing Guidelines
2
+
3
+ ## 🚨 CRITICAL: bun:test Only
4
+
5
+ ```ts
6
+ // ✅ CORRECT
7
+ import { describe, expect, it } from 'bun:test'
8
+
9
+ // ❌ WRONG - do NOT use
10
+ jest.mock(...) // Jest doesn't work
11
+ jest.unstable_mockModule() // Jest doesn't work
12
+ vi.mock(...) // Vitest doesn't work
13
+ ```
14
+
15
+ ---
16
+
17
+ ## What to Test
18
+
19
+ - Test ONLY use-cases (not handlers)
20
+ - No mocking - use dependency injection
21
+
22
+ ```ts
23
+ import { describe, expect, it } from 'bun:test'
24
+ import { processOrder } from './process-order.use-case'
25
+
26
+ describe('processOrder', () => {
27
+ it('processes order', async () => {
28
+ const store = new Map()
29
+ await processOrder({ orderId: '123', total: 100 }, store)
30
+ expect(store.has('123')).toBe(true)
31
+ })
32
+
33
+ it('throws on missing data', async () => {
34
+ await expect(processOrder(null as any, new Map())).rejects.toThrow()
35
+ })
36
+ })
37
+ ```
38
+
39
+ ---
40
+
41
+ ## External Services & Config
42
+
43
+ Use dependency injection, not mocking:
44
+
45
+ ```ts
46
+ // ✅ Use-case accepts dependencies
47
+ export const sendPush = async (
48
+ data: PushData,
49
+ config: { instanceId: string }
50
+ ) => {
51
+ if (!config.instanceId) throw new Error('Missing instanceId')
52
+ }
53
+
54
+ // ✅ Test with fake/config
55
+ it('throws on missing config', async () => {
56
+ await expect(sendPush({}, { instanceId: '' })).rejects.toThrow()
57
+ })
58
+ ```
59
+
60
+ ---
61
+
62
+ ## ❌ WRONG patterns (NEVER use these)
63
+
64
+ ```ts
65
+ // process.env manipulation
66
+ beforeEach(() => { process.env.X = 'test' })
67
+ afterEach(() => { process.env = env })
68
+
69
+ // @ts-ignore for mocking
70
+ // @ts-ignore
71
+ svc.beams = new FakeBeams()
72
+
73
+ // Jest/Vitest mocking
74
+ jest.mock(...)
75
+ vi.mock(...)
76
+
77
+ // Dynamic imports for error testing
78
+ await expect(import('./module')).rejects.toThrow()
79
+ ```
80
+
81
+ ---
82
+
83
+ ## NestJS Services
84
+
85
+ For NestJS: extract business logic into pure functions, test those:
86
+
87
+ ```ts
88
+ // ✅ Extract testable logic from service
89
+ export const validateConfig = (config: { id: string }) => {
90
+ if (!config.id) throw new Error('Missing id')
91
+ }
92
+
93
+ // ✅ Test the extracted function
94
+ it('validates config', () => {
95
+ expect(() => validateConfig({ id: '' })).toThrow()
96
+ })
97
+ ```
@@ -0,0 +1,18 @@
1
+ [
2
+ {
3
+ "name": "@crossdelta/cloudevents",
4
+ "description": "Type-safe event-driven microservices with NATS and Zod validation",
5
+ "link": "https://www.npmjs.com/package/@crossdelta/cloudevents",
6
+ "install": ["@crossdelta/cloudevents", "zod@^4.0.0"],
7
+ "run": [],
8
+ "initial": true
9
+ },
10
+ {
11
+ "name": "@crossdelta/telemetry",
12
+ "description": "Zero-config OpenTelemetry instrumentation for TypeScript services",
13
+ "link": "https://www.npmjs.com/package/@crossdelta/telemetry",
14
+ "install": "@crossdelta/telemetry",
15
+ "run": [],
16
+ "initial": true
17
+ }
18
+ ]
@@ -0,0 +1,16 @@
1
+ # syntax=docker/dockerfile:1.7-labs
2
+ ARG BUN_VERSION={{bunVersion}}
3
+
4
+ FROM oven/bun:${BUN_VERSION}-alpine AS production
5
+ WORKDIR /app
6
+
7
+ COPY bunfig.toml package.json ./
8
+ COPY src ./src
9
+
10
+ RUN --mount=type=secret,id=NPM_TOKEN \
11
+ export NPM_TOKEN="$(cat /run/secrets/NPM_TOKEN)" && \
12
+ bun install --production --omit=optional
13
+
14
+ USER bun
15
+
16
+ CMD ["bun", "run", "src/index.ts"]
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": ["../../biome.json"]
3
+ }