@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
|
@@ -1,694 +1,56 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Use
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const app = new Hono()
|
|
58
|
-
|
|
59
|
-
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
60
|
-
|
|
61
|
-
Bun.serve({ port, fetch: app.fetch })
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## CloudEvents & NATS Messaging
|
|
65
|
-
|
|
66
|
-
**Always use `@crossdelta/cloudevents`**, never raw NATS client.
|
|
67
|
-
|
|
68
|
-
### Publishing Events
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
import { publish } from '@crossdelta/cloudevents'
|
|
72
|
-
|
|
73
|
-
await publish('orders.created', { orderId, customerId, total }, {
|
|
74
|
-
source: '{{projectName}}://orders-service',
|
|
75
|
-
subject: orderId
|
|
76
|
-
})
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
The `publish` function automatically:
|
|
80
|
-
- Constructs the CloudEvent type from the subject (e.g., `orders.created` → `{{projectName}}.orders.created`)
|
|
81
|
-
- Adds required CloudEvent metadata (id, time, specversion, datacontenttype)
|
|
82
|
-
- Publishes to NATS JetStream
|
|
83
|
-
|
|
84
|
-
### Consuming Events (Auto-Discovery)
|
|
85
|
-
|
|
86
|
-
Create handler files in `src/handlers/*.event.ts`:
|
|
87
|
-
|
|
88
|
-
```ts
|
|
89
|
-
// services/notifications/src/handlers/order-created.event.ts
|
|
90
|
-
import { handleEvent } from '@crossdelta/cloudevents'
|
|
91
|
-
import { z } from 'zod'
|
|
92
|
-
import { sendNotification } from '../use-cases/send-notification.use-case'
|
|
93
|
-
|
|
94
|
-
const OrderCreatedSchema = z.object({
|
|
95
|
-
orderId: z.string(),
|
|
96
|
-
customerId: z.string(),
|
|
97
|
-
total: z.number(),
|
|
98
|
-
items: z.array(
|
|
99
|
-
z.object({
|
|
100
|
-
productId: z.string(),
|
|
101
|
-
quantity: z.number(),
|
|
102
|
-
price: z.number(),
|
|
103
|
-
}),
|
|
104
|
-
).optional(),
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
// Export type for reuse in other services
|
|
108
|
-
export type OrderCreatedEvent = z.infer<typeof OrderCreatedSchema>
|
|
109
|
-
|
|
110
|
-
export default handleEvent(
|
|
111
|
-
{
|
|
112
|
-
schema: OrderCreatedSchema,
|
|
113
|
-
type: 'orders.created',
|
|
114
|
-
},
|
|
115
|
-
async (data) => {
|
|
116
|
-
await sendNotification(data)
|
|
117
|
-
},
|
|
118
|
-
)
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
**Important:**
|
|
122
|
-
- **Schema** validates only the event data payload (without `type` field)
|
|
123
|
-
- **Event type** is declared in the options object, not in the schema
|
|
124
|
-
- Event type matches the first parameter of `publish()` (e.g., `'orders.created'`)
|
|
125
|
-
- Do NOT include `type: z.literal('...')` in the schema - it's redundant and causes validation errors
|
|
126
|
-
- **Always export the inferred type** using `export type EventName = z.infer<typeof EventSchema>` for use in use-cases
|
|
127
|
-
|
|
128
|
-
**Naming conventions:**
|
|
129
|
-
- Schema constants: PascalCase with `Schema` suffix (e.g., `OrderCreatedSchema`, `UserUpdatedSchema`)
|
|
130
|
-
- Exported types: PascalCase with `Event` suffix (e.g., `OrderCreatedEvent`, `UserUpdatedEvent`)
|
|
131
|
-
|
|
132
|
-
### Service Folder Structure
|
|
133
|
-
|
|
134
|
-
Organize service code with this structure:
|
|
135
|
-
|
|
136
|
-
```
|
|
137
|
-
src/
|
|
138
|
-
├── index.ts # Entry point with telemetry, Hono app, NATS setup
|
|
139
|
-
├── handlers/ # Event handlers (*.event.ts files for NATS events)
|
|
140
|
-
│ └── order-created.event.ts
|
|
141
|
-
└── use-cases/ # Business logic / use cases
|
|
142
|
-
└── send-notification.use-case.ts
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
- **`src/handlers/`**: Event handlers that consume CloudEvents from NATS
|
|
146
|
-
- **`src/use-cases/`**: Reusable business logic (pure functions or classes)
|
|
147
|
-
- Keep `src/index.ts` focused on wiring (server, consumers, routes)
|
|
148
|
-
|
|
149
|
-
Start consumption in `src/index.ts` (NOT in a separate file):
|
|
150
|
-
|
|
151
|
-
```ts
|
|
152
|
-
// src/index.ts - complete example with event consumption
|
|
153
|
-
import '@crossdelta/telemetry'
|
|
154
|
-
|
|
155
|
-
import { consumeJetStreamEvents } from '@crossdelta/cloudevents'
|
|
156
|
-
import { Hono } from 'hono'
|
|
157
|
-
|
|
158
|
-
const port = Number(process.env.PORT || process.env.NOTIFICATIONS_PORT) || 4002
|
|
159
|
-
const app = new Hono()
|
|
160
|
-
|
|
161
|
-
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
162
|
-
|
|
163
|
-
// Start NATS consumer - handlers are auto-discovered
|
|
164
|
-
consumeJetStreamEvents({
|
|
165
|
-
stream: 'ORDERS',
|
|
166
|
-
subjects: ['orders.>'],
|
|
167
|
-
consumer: 'notifications',
|
|
168
|
-
discover: './src/handlers/**/*.event.ts',
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
Bun.serve({ port, fetch: app.fetch })
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
## Storefront: RxDB + Supabase Replication
|
|
175
|
-
|
|
176
|
-
### Adding a New Collection
|
|
177
|
-
|
|
178
|
-
1. Create schema in `apps/storefront/src/db/schemas/generated/`
|
|
179
|
-
2. Add collection definition in `apps/storefront/src/db/collections/definitions/`:
|
|
180
|
-
|
|
181
|
-
```ts
|
|
182
|
-
export const ordersCollectionDefinition = defineCollection({
|
|
183
|
-
name: 'orders',
|
|
184
|
-
creator: { schema: ordersRxdbSchema },
|
|
185
|
-
setup: async (collection) => {
|
|
186
|
-
await replicateSupabaseTable(collection, 'orders', { tenant: createTenantScopeGuard() })
|
|
187
|
-
},
|
|
188
|
-
useCollectionQuery,
|
|
189
|
-
})
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
3. Register in `apps/storefront/src/db/collections/definitions/index.ts`
|
|
193
|
-
|
|
194
|
-
**Supabase tables require:** `id`, `updated_at`, `_deleted` (boolean for soft-delete).
|
|
195
|
-
|
|
196
|
-
## Infrastructure (Pulumi + Kubernetes)
|
|
197
|
-
|
|
198
|
-
### Modern Port Configuration (Fluent API)
|
|
199
|
-
|
|
200
|
-
**Use the fluent `ports()` builder** for defining service ports:
|
|
201
|
-
|
|
202
|
-
```ts
|
|
203
|
-
import { ports } from '@crossdelta/infrastructure'
|
|
204
|
-
import type { K8sServiceConfig } from '@crossdelta/infrastructure'
|
|
205
|
-
|
|
206
|
-
// Simple internal HTTP service
|
|
207
|
-
const config: K8sServiceConfig = {
|
|
208
|
-
name: 'orders',
|
|
209
|
-
ports: ports().http(4001).build(),
|
|
210
|
-
replicas: 1,
|
|
211
|
-
healthCheck: { httpPath: '/health' }, // HTTP health check endpoint
|
|
212
|
-
resources: {
|
|
213
|
-
requests: { cpu: '50m', memory: '64Mi' },
|
|
214
|
-
limits: { cpu: '150m', memory: '128Mi' }
|
|
215
|
-
},
|
|
216
|
-
env: { PUBLIC_SUPABASE_URL: supabaseUrl },
|
|
217
|
-
secrets: { SUPABASE_SERVICE_ROLE_KEY: supabaseServiceRoleKey },
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Public HTTP service
|
|
221
|
-
const publicConfig: K8sServiceConfig = {
|
|
222
|
-
name: 'api-gateway',
|
|
223
|
-
ports: ports().http(4000).public().build(),
|
|
224
|
-
ingress: { path: '/api', host: 'api.example.com' },
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Service with multiple ports
|
|
228
|
-
const natsConfig: K8sServiceConfig = {
|
|
229
|
-
name: 'nats',
|
|
230
|
-
ports: ports()
|
|
231
|
-
.primary(4222, 'client')
|
|
232
|
-
.addHttp(8222, 'monitoring').public()
|
|
233
|
-
.add(6222, 'routing')
|
|
234
|
-
.build(),
|
|
235
|
-
}
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
**Port Builder Methods:**
|
|
239
|
-
- `.http(port)` - HTTP primary port
|
|
240
|
-
- `.https(port)` - HTTPS primary port
|
|
241
|
-
- `.grpc(port)` - gRPC primary port
|
|
242
|
-
- `.primary(port, name?)` - Custom primary port
|
|
243
|
-
- `.add(port, name?, protocol?)` - Add additional port
|
|
244
|
-
- `.addHttp(port, name?)` - Add HTTP additional port
|
|
245
|
-
- `.addGrpc(port, name?)` - Add gRPC additional port
|
|
246
|
-
- `.public()` - Mark last port as public (exposed via ingress)
|
|
247
|
-
- `.protocol(type)` - Set protocol for last port
|
|
248
|
-
- `.build()` - Build final config
|
|
249
|
-
|
|
250
|
-
**Legacy `containerPort` is deprecated** - use the fluent API instead.
|
|
251
|
-
|
|
252
|
-
### Health Checks
|
|
253
|
-
|
|
254
|
-
Services should expose health check endpoints:
|
|
255
|
-
|
|
256
|
-
```ts
|
|
257
|
-
// HTTP health check (recommended)
|
|
258
|
-
healthCheck: {
|
|
259
|
-
httpPath: '/health', // GET endpoint
|
|
260
|
-
initialDelaySeconds: 10, // Wait before first check
|
|
261
|
-
periodSeconds: 10, // Check interval
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// For services without HTTP endpoints, Kubernetes will use TCP checks automatically
|
|
265
|
-
// based on the primary port
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
Health check endpoint implementation:
|
|
269
|
-
|
|
270
|
-
```ts
|
|
271
|
-
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
### Smart Service Discovery
|
|
275
|
-
|
|
276
|
-
**Use `discoverServiceConfigs()` for automatic service discovery:**
|
|
277
|
-
|
|
278
|
-
```ts
|
|
279
|
-
import { discoverServiceConfigs, discoverServiceConfigsWithOptions } from '@crossdelta/infrastructure'
|
|
280
|
-
|
|
281
|
-
// Simple discovery (backward compatible)
|
|
282
|
-
const configs = discoverServiceConfigs('services')
|
|
283
|
-
|
|
284
|
-
// Advanced discovery with filtering
|
|
285
|
-
const result = discoverServiceConfigsWithOptions({
|
|
286
|
-
servicesDir: 'services',
|
|
287
|
-
filter: /^api-/, // Pattern matching
|
|
288
|
-
exclude: ['test-service'], // Exclude services
|
|
289
|
-
env: { // Inject env vars
|
|
290
|
-
NODE_ENV: 'production',
|
|
291
|
-
NATS_URL: natsUrl,
|
|
292
|
-
},
|
|
293
|
-
validate: true, // Port conflict checks
|
|
294
|
-
})
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
**Discovery Options:**
|
|
298
|
-
- `filter` - Regex or string pattern to match service names
|
|
299
|
-
- `include` - Array of service names to include
|
|
300
|
-
- `exclude` - Array of service names to exclude
|
|
301
|
-
- `tags` - Filter by service tags
|
|
302
|
-
- `env` - Inject environment variables into all services
|
|
303
|
-
- `validate` - Enable validation (port conflicts, missing fields)
|
|
304
|
-
- `sort` - Sort services by name (default: true)
|
|
305
|
-
- `registry` - Custom registry for auto-generated images
|
|
306
|
-
|
|
307
|
-
Follow existing configs in `infra/services/`. Don't invent new providers or patterns.
|
|
308
|
-
|
|
309
|
-
## Code Style & Conventions
|
|
310
|
-
|
|
311
|
-
### Biome Configuration
|
|
312
|
-
|
|
313
|
-
**CRITICAL:** All code MUST follow the Biome rules configured in the root `biome.json`:
|
|
314
|
-
|
|
315
|
-
- **Formatting:** Single quotes, no semicolons, 2-space indent, 120 char width, trailing commas
|
|
316
|
-
- **Import Organization:** Imports and exports MUST be sorted alphabetically (use `organizeImports: "on"`)
|
|
317
|
-
- **Unused Imports:** Remove all unused imports (lint error)
|
|
318
|
-
- **Code Quality:** Follow all Biome linter rules (security, style, complexity)
|
|
319
|
-
|
|
320
|
-
**Always run before committing:**
|
|
321
|
-
```bash
|
|
322
|
-
bun lint # Check for issues
|
|
323
|
-
bun format # Auto-fix formatting and organize imports
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
**In your editor:** Enable Biome's "Organize Imports on Save" for automatic sorting.
|
|
327
|
-
|
|
328
|
-
### General Conventions
|
|
329
|
-
|
|
330
|
-
- **Functions:** Prefer **arrow functions** and **functional programming patterns** (map, filter, reduce) over imperative loops
|
|
331
|
-
- **Immutability:** Use `const` over `let`, avoid mutations, prefer spread operators and array methods
|
|
332
|
-
- **Composition:** Small, composable functions over large classes
|
|
333
|
-
- **Ports:** Always `process.env.PORT || process.env.SERVICE_PORT || default`
|
|
334
|
-
- **Health:** All services expose `GET /health → { status: 'ok' }`
|
|
335
|
-
- **Commits:** Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`)
|
|
336
|
-
- **Tests:** Use `bun:test` — see `packages/cloudevents/test/*.test.ts` for patterns
|
|
337
|
-
|
|
338
|
-
### Functional Programming Examples
|
|
339
|
-
|
|
340
|
-
**Prefer:**
|
|
341
|
-
```ts
|
|
342
|
-
// Arrow functions
|
|
343
|
-
const add = (a: number, b: number) => a + b
|
|
344
|
-
|
|
345
|
-
// Map/filter/reduce over loops
|
|
346
|
-
const activeUsers = users.filter(user => user.active)
|
|
347
|
-
const userNames = users.map(user => user.name)
|
|
348
|
-
const totalAge = users.reduce((sum, user) => sum + user.age, 0)
|
|
349
|
-
|
|
350
|
-
// Composition
|
|
351
|
-
const processData = (data: Data[]) =>
|
|
352
|
-
data
|
|
353
|
-
.filter(isValid)
|
|
354
|
-
.map(transform)
|
|
355
|
-
.reduce(aggregate, initialValue)
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
**Avoid:**
|
|
359
|
-
```ts
|
|
360
|
-
// Function declarations (unless needed for hoisting)
|
|
361
|
-
function add(a: number, b: number) { return a + b }
|
|
362
|
-
|
|
363
|
-
// Imperative loops
|
|
364
|
-
const activeUsers = []
|
|
365
|
-
for (let i = 0; i < users.length; i++) {
|
|
366
|
-
if (users[i].active) {
|
|
367
|
-
activeUsers.push(users[i])
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
## Key Files Reference
|
|
373
|
-
|
|
374
|
-
| Pattern | Example |
|
|
375
|
-
|---------|---------|
|
|
376
|
-
| Hono service entry | `services/orders/src/index.ts` |
|
|
377
|
-
| Event handler | `services/notifications/src/handlers/order-created.event.ts` |
|
|
378
|
-
| RxDB collection | `apps/storefront/src/db/collections/definitions/orders.ts` |
|
|
379
|
-
| Service infra config | `infra/services/orders.ts` |
|
|
380
|
-
| Supabase migrations | `apps/storefront/supabase/migrations/` |
|
|
381
|
-
|
|
382
|
-
## AI Code Generation Format
|
|
383
|
-
|
|
384
|
-
When generating code via `pf ai generate-service`, use this output format:
|
|
385
|
-
|
|
386
|
-
### Commands to Execute
|
|
387
|
-
|
|
388
|
-
List commands that should be run BEFORE the source files are created:
|
|
389
|
-
|
|
390
|
-
```commands
|
|
391
|
-
pf new hono-micro services/my-service -y
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
These commands will be executed automatically by the CLI.
|
|
395
|
-
|
|
396
|
-
**CRITICAL:** The service path in the `pf new` command MUST match the service name provided by the user exactly. If the user specifies `services/my-service`, use `services/my-service`. Do NOT modify or prepend paths.
|
|
397
|
-
|
|
398
|
-
### Dependencies (Optional)
|
|
399
|
-
|
|
400
|
-
If the service requires additional npm packages beyond what's scaffolded, list them in a `dependencies` block:
|
|
401
|
-
|
|
402
|
-
```dependencies
|
|
403
|
-
@pusher/push-notifications-server
|
|
404
|
-
zod
|
|
405
|
-
drizzle-orm
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
**Format:**
|
|
409
|
-
- One package per line
|
|
410
|
-
- Use exact package names from npm (e.g., `@scope/package-name`)
|
|
411
|
-
- Comments starting with `#` are ignored
|
|
412
|
-
- These packages will be installed using the existing integration system
|
|
413
|
-
|
|
414
|
-
### Source Files
|
|
415
|
-
|
|
416
|
-
Format source files with path headers. Paths are relative to the service directory (e.g., `src/index.ts`, NOT `services/my-service/src/index.ts`):
|
|
417
|
-
|
|
418
|
-
#### `src/index.ts`
|
|
419
|
-
```typescript
|
|
420
|
-
// code here
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
#### `src/handlers/event-name.event.ts`
|
|
424
|
-
```typescript
|
|
425
|
-
import { handleEvent } from '@crossdelta/cloudevents'
|
|
426
|
-
import { z } from 'zod'
|
|
427
|
-
import { businessLogic } from '../use-cases/business-logic.use-case'
|
|
428
|
-
|
|
429
|
-
const EventDataSchema = z.object({
|
|
430
|
-
id: z.string(),
|
|
431
|
-
// ... other fields
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
// Export type for use in use-cases
|
|
435
|
-
export type EventDataType = z.infer<typeof EventDataSchema>
|
|
436
|
-
|
|
437
|
-
export default handleEvent(
|
|
438
|
-
{
|
|
439
|
-
schema: EventDataSchema,
|
|
440
|
-
type: 'resource.action', // e.g., 'orders.created', 'users.updated'
|
|
441
|
-
},
|
|
442
|
-
async (data) => {
|
|
443
|
-
await businessLogic(data)
|
|
444
|
-
},
|
|
445
|
-
)
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
#### `src/use-cases/business-logic.use-case.ts`
|
|
449
|
-
```typescript
|
|
450
|
-
import type { EventDataType } from '../handlers/event-name.event'
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Business logic that processes the event data.
|
|
454
|
-
* Uses the exported type from the handler for type safety.
|
|
455
|
-
*/
|
|
456
|
-
export async function businessLogic(data: EventDataType): Promise<void> {
|
|
457
|
-
// Full type inference from the handler schema
|
|
458
|
-
console.log('Processing:', data.id)
|
|
459
|
-
|
|
460
|
-
// All business logic here
|
|
461
|
-
// - Validation
|
|
462
|
-
// - External API calls
|
|
463
|
-
// - Database operations
|
|
464
|
-
}
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
### Tests
|
|
468
|
-
|
|
469
|
-
Always generate tests for the service. Use Bun's native test runner (`bun:test`):
|
|
470
|
-
|
|
471
|
-
#### `src/index.test.ts`
|
|
472
|
-
```typescript
|
|
473
|
-
import { describe, expect, it } from 'bun:test'
|
|
474
|
-
|
|
475
|
-
describe('Service Health Check', () => {
|
|
476
|
-
it('should respond to health check', async () => {
|
|
477
|
-
const app = new Hono()
|
|
478
|
-
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
479
|
-
|
|
480
|
-
const req = new Request('http://localhost/health')
|
|
481
|
-
const res = await app.fetch(req)
|
|
482
|
-
|
|
483
|
-
expect(res.status).toBe(200)
|
|
484
|
-
const json = await res.json()
|
|
485
|
-
expect(json).toEqual({ status: 'ok' })
|
|
486
|
-
})
|
|
487
|
-
})
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
#### `src/use-cases/business-logic.use-case.test.ts`
|
|
491
|
-
```typescript
|
|
492
|
-
import { describe, expect, it } from 'bun:test'
|
|
493
|
-
import { myUseCase } from './business-logic.use-case'
|
|
494
|
-
|
|
495
|
-
describe('BusinessLogic Use Case', () => {
|
|
496
|
-
it('should handle valid input correctly', async () => {
|
|
497
|
-
const result = await myUseCase({ id: '123' })
|
|
498
|
-
expect(result).toBeDefined()
|
|
499
|
-
})
|
|
500
|
-
|
|
501
|
-
it('should throw error for invalid input', async () => {
|
|
502
|
-
await expect(myUseCase({ id: '' })).rejects.toThrow('Invalid input')
|
|
503
|
-
})
|
|
504
|
-
})
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
**Test Guidelines:**
|
|
508
|
-
- **Keep tests simple and lightweight** - Bun's test runner is minimal, not a full-featured test framework
|
|
509
|
-
- Write tests ONLY for use cases (NOT event handlers)
|
|
510
|
-
- Event handlers are thin wrappers - the use cases contain the testable logic
|
|
511
|
-
- Use Bun's native test runner: `import { describe, expect, it, beforeEach, afterEach } from 'bun:test'`
|
|
512
|
-
- **NO mocking available** - Bun's test runner does NOT have `vi`, `mock`, or similar utilities
|
|
513
|
-
- ❌ NO `vi` object (that's Vitest, not Bun)
|
|
514
|
-
- ❌ NO `jest.fn()` or `jest.mock()`
|
|
515
|
-
- ❌ NO module mocking
|
|
516
|
-
- ✅ Use simple, direct testing without mocks
|
|
517
|
-
- Use descriptive test names in English
|
|
518
|
-
- Test both happy paths and error cases
|
|
519
|
-
- Focus on validation and error handling (input params, env vars)
|
|
520
|
-
- **DO NOT mock external libraries** - Bun doesn't support mocking, keep tests simple
|
|
521
|
-
- Test environment variable validation and input validation
|
|
522
|
-
- Keep cleanup simple in `afterEach`: restore `process.env` only
|
|
523
|
-
|
|
524
|
-
**Example: Simple validation and error handling tests**
|
|
525
|
-
```typescript
|
|
526
|
-
import { describe, expect, it, beforeEach, afterEach } from 'bun:test'
|
|
527
|
-
import { sendNotification } from './send-notification.use-case'
|
|
528
|
-
|
|
529
|
-
describe('Send Notification Use Case', () => {
|
|
530
|
-
const originalEnv = process.env
|
|
531
|
-
|
|
532
|
-
beforeEach(() => {
|
|
533
|
-
// Save original environment
|
|
534
|
-
process.env = { ...originalEnv }
|
|
535
|
-
})
|
|
536
|
-
|
|
537
|
-
afterEach(() => {
|
|
538
|
-
// Restore original environment (no vi.resetModules needed)
|
|
539
|
-
process.env = originalEnv
|
|
540
|
-
})
|
|
541
|
-
|
|
542
|
-
it('should throw error if orderId is missing', async () => {
|
|
543
|
-
await expect(
|
|
544
|
-
sendNotification({ orderId: '', customerId: 'cust-1' })
|
|
545
|
-
).rejects.toThrow('Missing orderId')
|
|
546
|
-
})
|
|
547
|
-
|
|
548
|
-
it('should throw error if credentials are not set', async () => {
|
|
549
|
-
delete process.env.PUSHER_BEAMS_INSTANCE_ID
|
|
550
|
-
|
|
551
|
-
await expect(
|
|
552
|
-
sendNotification({ orderId: '123', customerId: 'cust-1' })
|
|
553
|
-
).rejects.toThrow('Missing Pusher Beams credentials')
|
|
554
|
-
})
|
|
555
|
-
})
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
**What to test:**
|
|
559
|
-
- ✅ Input validation (missing/empty parameters)
|
|
560
|
-
- ✅ Environment variable validation
|
|
561
|
-
- ✅ Error messages and error types
|
|
562
|
-
- ✅ Basic logic flows
|
|
563
|
-
- ❌ Complex mocking scenarios
|
|
564
|
-
- ❌ External API integrations (Pusher, AWS, etc.)
|
|
565
|
-
- ❌ Module reloading/resetting
|
|
566
|
-
|
|
567
|
-
**Test Strategy:**
|
|
568
|
-
- Focus on **validation** (input parameters, environment variables)
|
|
569
|
-
- Test **error handling** and edge cases
|
|
570
|
-
- Keep tests **simple** - avoid complex mocking of external libraries
|
|
571
|
-
- Test the **business logic**, not the external API calls
|
|
572
|
-
- If a function calls an external API, test the validation BEFORE the call, not the call itself
|
|
573
|
-
|
|
574
|
-
**Example of what NOT to do:**
|
|
575
|
-
```typescript
|
|
576
|
-
// ❌ BAD - Trying to mock Pusher (not possible in Bun)
|
|
577
|
-
import { describe, expect, it, beforeEach, afterEach, vi } from 'bun:test'
|
|
578
|
-
|
|
579
|
-
describe('Bad Test', () => {
|
|
580
|
-
afterEach(() => {
|
|
581
|
-
vi.resetModules() // ❌ This doesn't exist in Bun!
|
|
582
|
-
})
|
|
583
|
-
|
|
584
|
-
it('should send notification', async () => {
|
|
585
|
-
// ❌ Trying to mock external library
|
|
586
|
-
const mockPusher = vi.fn() // ❌ vi doesn't exist!
|
|
587
|
-
})
|
|
588
|
-
})
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
**Example of what TO do:**
|
|
592
|
-
```typescript
|
|
593
|
-
// ✅ GOOD - Simple validation tests
|
|
594
|
-
import { describe, expect, it, beforeEach, afterEach } from 'bun:test'
|
|
595
|
-
|
|
596
|
-
describe('Good Test', () => {
|
|
597
|
-
const originalEnv = process.env
|
|
598
|
-
|
|
599
|
-
beforeEach(() => {
|
|
600
|
-
process.env = { ...originalEnv }
|
|
601
|
-
})
|
|
602
|
-
|
|
603
|
-
afterEach(() => {
|
|
604
|
-
process.env = originalEnv
|
|
605
|
-
})
|
|
606
|
-
|
|
607
|
-
it('should throw error if orderId is missing', async () => {
|
|
608
|
-
await expect(
|
|
609
|
-
sendNotification({ orderId: '', customerId: 'cust-1' })
|
|
610
|
-
).rejects.toThrow('Missing orderId')
|
|
611
|
-
})
|
|
612
|
-
|
|
613
|
-
it('should throw error if credentials not set', async () => {
|
|
614
|
-
delete process.env.PUSHER_BEAMS_INSTANCE_ID
|
|
615
|
-
|
|
616
|
-
await expect(
|
|
617
|
-
sendNotification({ orderId: '123', customerId: 'cust-1' })
|
|
618
|
-
).rejects.toThrow('Missing Pusher Beams credentials')
|
|
619
|
-
})
|
|
620
|
-
})
|
|
621
|
-
```
|
|
622
|
-
|
|
623
|
-
### README
|
|
624
|
-
|
|
625
|
-
Generate a service-specific README documenting:
|
|
626
|
-
|
|
627
|
-
#### `README.md`
|
|
628
|
-
```markdown
|
|
629
|
-
# Service Name
|
|
630
|
-
|
|
631
|
-
Brief description of what this service does.
|
|
632
|
-
|
|
633
|
-
## Features
|
|
634
|
-
|
|
635
|
-
- Feature 1
|
|
636
|
-
- Feature 2
|
|
637
|
-
|
|
638
|
-
## Environment Variables
|
|
639
|
-
|
|
640
|
-
| Variable | Description | Required | Default |
|
|
641
|
-
|----------|-------------|----------|---------|
|
|
642
|
-
| `SERVICE_PORT` | Port the service runs on | No | 4001 |
|
|
643
|
-
| `NATS_URL` | NATS server URL | Yes | - |
|
|
644
|
-
|
|
645
|
-
## Events
|
|
646
|
-
|
|
647
|
-
### Publishes
|
|
648
|
-
- `{{projectName}}.service.event` - Description
|
|
649
|
-
|
|
650
|
-
### Consumes
|
|
651
|
-
- `{{projectName}}.other.event` - Description
|
|
652
|
-
|
|
653
|
-
## API Endpoints
|
|
654
|
-
|
|
655
|
-
### `GET /health`
|
|
656
|
-
Health check endpoint.
|
|
657
|
-
|
|
658
|
-
**Response:**
|
|
659
|
-
\`\`\`json
|
|
660
|
-
{ "status": "ok" }
|
|
661
|
-
\`\`\`
|
|
662
|
-
|
|
663
|
-
## Development
|
|
664
|
-
|
|
665
|
-
\`\`\`bash
|
|
666
|
-
bun dev # Start in development mode
|
|
667
|
-
bun test # Run tests
|
|
668
|
-
\`\`\`
|
|
669
|
-
```
|
|
670
|
-
|
|
671
|
-
**CRITICAL - You MUST generate ALL of these:**
|
|
672
|
-
1. ✅ `pf new` command (first step - scaffolds the service)
|
|
673
|
-
2. ✅ Source files (`src/index.ts`, `src/handlers/*.event.ts`, `src/use-cases/*.use-case.ts`)
|
|
674
|
-
3. ✅ **Test files for EVERY use case** (`src/use-cases/*.test.ts`) - DO NOT test event handlers
|
|
675
|
-
4. ✅ **Complete README.md** with:
|
|
676
|
-
- Service description & features
|
|
677
|
-
- Environment variables table
|
|
678
|
-
- Events (published/consumed)
|
|
679
|
-
- API endpoints documentation
|
|
680
|
-
- Development commands
|
|
681
|
-
|
|
682
|
-
**DO NOT skip tests or README** - they are REQUIRED, not optional.
|
|
683
|
-
|
|
684
|
-
**Test Strategy:**
|
|
685
|
-
- Focus on validation (input parameters, environment variables)
|
|
686
|
-
- Test error handling and edge cases
|
|
687
|
-
- Keep tests simple - avoid complex mocking of external libraries
|
|
688
|
-
- Test the business logic, not the external API calls
|
|
689
|
-
|
|
690
|
-
**Format notes:**
|
|
691
|
-
- Follow the Service Entry Point Pattern and folder structure above
|
|
692
|
-
- Biome lint/format runs automatically after generation – don't worry about minor formatting
|
|
693
|
-
- **ALL code comments, documentation, and README files MUST be written in English**
|
|
694
|
-
- **Tests MUST be compatible with Bun's test runner** (`bun:test` module with `describe`, `it`, `expect`, `beforeEach`, `afterEach`, `vi` for mocking)
|
|
1
|
+
# Copilot Instructions (Project-Agnostic, TypeScript-Focused)
|
|
2
|
+
|
|
3
|
+
You are assisting in TypeScript-first monorepos using modern TypeScript tooling such as Bun or Node, Turborepo, Hono, NestJS, Zod, event-driven patterns, and Pulumi/Kubernetes infrastructure.
|
|
4
|
+
|
|
5
|
+
Always generate **minimal diffs**, never full rewrites.
|
|
6
|
+
|
|
7
|
+
## General Behavior
|
|
8
|
+
- Produce short, focused, code-first answers.
|
|
9
|
+
- Modify only the necessary parts.
|
|
10
|
+
- Analyze only the opened file unless explicitly asked.
|
|
11
|
+
- Follow existing architecture and naming conventions.
|
|
12
|
+
- Prefer strict typing; avoid `any`.
|
|
13
|
+
|
|
14
|
+
## Code Style
|
|
15
|
+
- Single quotes, no semicolons, 2-space indent, trailing commas.
|
|
16
|
+
- Alphabetically sorted imports; no unused imports.
|
|
17
|
+
- Arrow functions over `function` declarations.
|
|
18
|
+
- Prefer pure and functional programming patterns (map/filter/reduce).
|
|
19
|
+
- Avoid mutable state and imperative loops.
|
|
20
|
+
- No decorative section header blocks (no ASCII art separators like `// ─────────────`).
|
|
21
|
+
- Use blank lines to separate logical sections naturally.
|
|
22
|
+
- Organize code: types → helpers → higher-order functions.
|
|
23
|
+
- Use JSDoc for exported functions and complex logic; keep inline comments minimal.
|
|
24
|
+
- Self-documenting code over comments: clear naming, pure functions, obvious control flow.
|
|
25
|
+
|
|
26
|
+
## Validation & Types
|
|
27
|
+
- Use Zod for schemas.
|
|
28
|
+
- Export inferred types using `z.infer`.
|
|
29
|
+
- Do NOT include literal event types inside schemas.
|
|
30
|
+
- Do not duplicate validation logic across layers.
|
|
31
|
+
|
|
32
|
+
## Service & Module Structure
|
|
33
|
+
- Entry points handle wiring (server start, telemetry, routing, consumers).
|
|
34
|
+
- Business logic lives in use-case modules.
|
|
35
|
+
- Event handlers must be thin wrappers around use-cases.
|
|
36
|
+
|
|
37
|
+
## Event Handling
|
|
38
|
+
- Use shared CloudEvents/messaging libraries, not raw clients.
|
|
39
|
+
- Handlers: Schema → inferred type → thin handler → use-case delegation.
|
|
40
|
+
- Handlers named `*.event.ts`.
|
|
41
|
+
|
|
42
|
+
## Infrastructure
|
|
43
|
+
- Use fluent port builders when available.
|
|
44
|
+
- Expose a `/health` endpoint.
|
|
45
|
+
- Avoid legacy containerPort usage.
|
|
46
|
+
|
|
47
|
+
## Testing
|
|
48
|
+
- Test use-cases, not handlers or frameworks.
|
|
49
|
+
- Prefer simple direct tests, no mocks.
|
|
50
|
+
- Validate both error and success paths.
|
|
51
|
+
|
|
52
|
+
## AI Output Format
|
|
53
|
+
- Provide only necessary files.
|
|
54
|
+
- Use relative paths.
|
|
55
|
+
- Keep comments minimal.
|
|
56
|
+
- Do not generate abstractions not already present.
|