@crossdelta/platform-sdk 0.13.2 → 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.
- package/bin/cli.js +312 -0
- package/bin/docs/generators/README.md +56 -0
- package/bin/docs/generators/code-style.md +96 -0
- package/bin/docs/generators/hono-bun.md +181 -0
- package/bin/docs/generators/hono-node.md +194 -0
- package/bin/docs/generators/nest.md +358 -0
- package/bin/docs/generators/service.md +564 -0
- package/bin/docs/generators/testing.md +97 -0
- package/bin/integration.collection.json +18 -0
- package/bin/templates/hono-microservice/Dockerfile.hbs +16 -0
- package/bin/templates/hono-microservice/biome.json.hbs +3 -0
- package/bin/templates/hono-microservice/src/index.ts.hbs +18 -0
- package/bin/templates/hono-microservice/tsconfig.json.hbs +14 -0
- package/bin/templates/nest-microservice/Dockerfile.hbs +37 -0
- package/bin/templates/nest-microservice/biome.json.hbs +3 -0
- package/bin/templates/nest-microservice/src/app.context.ts.hbs +17 -0
- package/bin/templates/nest-microservice/src/events/events.module.ts.hbs +8 -0
- package/bin/templates/nest-microservice/src/events/events.service.ts.hbs +22 -0
- package/bin/templates/nest-microservice/src/main.ts.hbs +34 -0
- package/bin/templates/workspace/biome.json.hbs +62 -0
- package/bin/templates/workspace/bunfig.toml.hbs +5 -0
- package/bin/templates/workspace/editorconfig.hbs +9 -0
- package/bin/templates/workspace/gitignore.hbs +15 -0
- package/bin/templates/workspace/infra/Pulumi.dev.yaml.hbs +5 -0
- package/bin/templates/workspace/infra/Pulumi.yaml.hbs +6 -0
- package/bin/templates/workspace/infra/index.ts.hbs +56 -0
- package/bin/templates/workspace/infra/package.json.hbs +23 -0
- package/bin/templates/workspace/infra/tsconfig.json.hbs +15 -0
- package/bin/templates/workspace/npmrc.hbs +2 -0
- package/bin/templates/workspace/package.json.hbs +51 -0
- package/bin/templates/workspace/packages/contracts/README.md.hbs +166 -0
- package/bin/templates/workspace/packages/contracts/package.json.hbs +22 -0
- package/bin/templates/workspace/packages/contracts/src/events/index.ts +16 -0
- package/bin/templates/workspace/packages/contracts/src/index.ts +10 -0
- package/bin/templates/workspace/packages/contracts/src/stream-policies.ts.hbs +40 -0
- package/bin/templates/workspace/packages/contracts/tsconfig.json.hbs +7 -0
- package/bin/templates/workspace/pnpm-workspace.yaml.hbs +5 -0
- package/bin/templates/workspace/turbo.json +38 -0
- package/bin/templates/workspace/turbo.json.hbs +29 -0
- package/install.sh +46 -8
- package/package.json +1 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// IMPORTANT: telemetry must be imported first to patch modules before they're loaded
|
|
2
|
+
import '@crossdelta/telemetry'
|
|
3
|
+
|
|
4
|
+
import { Hono } from 'hono'
|
|
5
|
+
|
|
6
|
+
const port = Number(process.env.{{envKey}}_PORT) || 8080
|
|
7
|
+
const app = new Hono()
|
|
8
|
+
|
|
9
|
+
app.get('/health', (c) => {
|
|
10
|
+
return c.json({ status: 'ok' })
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
Bun.serve({
|
|
14
|
+
port,
|
|
15
|
+
fetch: app.fetch,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
console.log(`🚀 Service ready at http://localhost:${port}`)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"lib": ["ES2022"],
|
|
8
|
+
"types": ["bun"],
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"jsxImportSource": "hono/jsx",
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
ARG NODE_VERSION={{nodeVersion}}
|
|
2
|
+
ARG BUN_VERSION={{bunVersion}}
|
|
3
|
+
|
|
4
|
+
# STAGE 1 — Build
|
|
5
|
+
FROM oven/bun:${BUN_VERSION}-alpine AS builder
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
|
|
8
|
+
COPY package.json tsconfig*.json nest-cli.json ./
|
|
9
|
+
COPY src ./src
|
|
10
|
+
|
|
11
|
+
RUN --mount=type=secret,id=NPM_TOKEN \
|
|
12
|
+
export NPM_TOKEN="$(cat /run/secrets/NPM_TOKEN)" && \
|
|
13
|
+
bun install
|
|
14
|
+
|
|
15
|
+
RUN bun run build
|
|
16
|
+
|
|
17
|
+
# STAGE 2 — Production dependencies
|
|
18
|
+
FROM oven/bun:${BUN_VERSION}-alpine AS deps
|
|
19
|
+
WORKDIR /app
|
|
20
|
+
|
|
21
|
+
COPY package.json ./
|
|
22
|
+
|
|
23
|
+
RUN --mount=type=secret,id=NPM_TOKEN \
|
|
24
|
+
export NPM_TOKEN="$(cat /run/secrets/NPM_TOKEN)" && \
|
|
25
|
+
bun install --production --omit=optional
|
|
26
|
+
|
|
27
|
+
# STAGE 3 — Runtime
|
|
28
|
+
FROM node:${NODE_VERSION}-alpine AS production
|
|
29
|
+
WORKDIR /app
|
|
30
|
+
|
|
31
|
+
COPY --from=builder /app/dist ./dist
|
|
32
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
33
|
+
|
|
34
|
+
USER node
|
|
35
|
+
ENV NODE_ENV=production
|
|
36
|
+
|
|
37
|
+
CMD ["node", "dist/main.js"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { INestApplication, Type } from '@nestjs/common'
|
|
2
|
+
|
|
3
|
+
let app: INestApplication
|
|
4
|
+
|
|
5
|
+
export const setAppContext = (nestApp: INestApplication): void => {
|
|
6
|
+
app = nestApp
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const getAppContext = (): INestApplication => {
|
|
10
|
+
if (!app) throw new Error('App context not initialized')
|
|
11
|
+
return app
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const getService = <T>(serviceClass: Type<T>): T => {
|
|
15
|
+
return getAppContext().get(serviceClass)
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Injectable, Logger } from '@nestjs/common'
|
|
2
|
+
import { consumeJetStreams } from '@crossdelta/cloudevents'
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class EventsService {
|
|
6
|
+
private readonly logger = new Logger(EventsService.name)
|
|
7
|
+
|
|
8
|
+
async startConsumers(): Promise<void> {
|
|
9
|
+
// Services NEVER create streams!
|
|
10
|
+
// - Development: pf dev auto-creates ephemeral streams from contracts
|
|
11
|
+
// - Production: Pulumi materializes persistent streams
|
|
12
|
+
|
|
13
|
+
// Uncomment and configure when you have events:
|
|
14
|
+
// consumeJetStreams({
|
|
15
|
+
// streams: ['ORDERS'],
|
|
16
|
+
// consumer: '{{serviceName}}',
|
|
17
|
+
// discover: './src/events/**/*.event.ts',
|
|
18
|
+
// })
|
|
19
|
+
|
|
20
|
+
this.logger.log('Event consumers ready')
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// IMPORTANT: telemetry must be imported first to patch modules before they're loaded
|
|
2
|
+
import '@crossdelta/telemetry'
|
|
3
|
+
|
|
4
|
+
import { env } from 'node:process'
|
|
5
|
+
import { ConsoleLogger } from '@nestjs/common'
|
|
6
|
+
import { NestFactory } from '@nestjs/core'
|
|
7
|
+
import { AppModule } from './app.module'
|
|
8
|
+
import { setAppContext } from './app.context'
|
|
9
|
+
import { EventsService } from './events/events.service'
|
|
10
|
+
|
|
11
|
+
const port = Number(process.env.{{envKey}}_PORT) || {{defaultPort}}
|
|
12
|
+
const serviceName = '{{displayName}}'
|
|
13
|
+
|
|
14
|
+
const logger = new ConsoleLogger({
|
|
15
|
+
json: env.JSON_LOGS === 'true',
|
|
16
|
+
prefix: serviceName,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
async function bootstrap() {
|
|
20
|
+
const app = await NestFactory.create(AppModule, { logger })
|
|
21
|
+
|
|
22
|
+
// Make app context available to event handlers
|
|
23
|
+
setAppContext(app)
|
|
24
|
+
|
|
25
|
+
// Start NATS event consumers
|
|
26
|
+
const eventsService = app.get(EventsService)
|
|
27
|
+
await eventsService.startConsumers()
|
|
28
|
+
|
|
29
|
+
await app
|
|
30
|
+
.listen(port)
|
|
31
|
+
.then(() => logger.log(`${serviceName} is running on port ${port}`))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
bootstrap()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
|
|
3
|
+
"formatter": {
|
|
4
|
+
"indentStyle": "space"
|
|
5
|
+
},
|
|
6
|
+
"assist": {
|
|
7
|
+
"actions": {
|
|
8
|
+
"source": {
|
|
9
|
+
"organizeImports": "on"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"linter": {
|
|
14
|
+
"enabled": true,
|
|
15
|
+
"rules": {
|
|
16
|
+
"security": {
|
|
17
|
+
"recommended": true
|
|
18
|
+
},
|
|
19
|
+
"style": {
|
|
20
|
+
"recommended": true,
|
|
21
|
+
"useAsConstAssertion": "warn",
|
|
22
|
+
"noUselessElse": "error",
|
|
23
|
+
"useConst": "error",
|
|
24
|
+
"useImportType": "off"
|
|
25
|
+
},
|
|
26
|
+
"complexity": {
|
|
27
|
+
"recommended": true,
|
|
28
|
+
"noUselessEmptyExport": "error",
|
|
29
|
+
"noExcessiveCognitiveComplexity": {
|
|
30
|
+
"level": "warn",
|
|
31
|
+
"options": {
|
|
32
|
+
"maxAllowedComplexity": 15
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"javascript": {
|
|
39
|
+
"formatter": {
|
|
40
|
+
"indentWidth": 2,
|
|
41
|
+
"quoteStyle": "single",
|
|
42
|
+
"semicolons": "asNeeded",
|
|
43
|
+
"lineWidth": 120,
|
|
44
|
+
"trailingCommas": "all",
|
|
45
|
+
"arrowParentheses": "always"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"files": {
|
|
49
|
+
"includes": [
|
|
50
|
+
"**/src/**/*.ts",
|
|
51
|
+
"infra/**/*.ts",
|
|
52
|
+
"!apps/storefront",
|
|
53
|
+
"!**/vendor",
|
|
54
|
+
"!**/dist",
|
|
55
|
+
"!**/node_modules",
|
|
56
|
+
"!reports",
|
|
57
|
+
"!coverage",
|
|
58
|
+
"!stryker-tmp",
|
|
59
|
+
"!**/.stryker-tmp"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import {
|
|
3
|
+
buildExternalUrls,
|
|
4
|
+
buildIngressRules,
|
|
5
|
+
buildInternalUrls,
|
|
6
|
+
buildServicePortEnvs,
|
|
7
|
+
buildServices,
|
|
8
|
+
buildServiceUrlEnvs,
|
|
9
|
+
discoverServices,
|
|
10
|
+
} from '@crossdelta/infrastructure'
|
|
11
|
+
import { App } from '@pulumi/digitalocean'
|
|
12
|
+
import { Config, getStack } from '@pulumi/pulumi'
|
|
13
|
+
|
|
14
|
+
const allServiceConfigs = discoverServices(join(__dirname, 'services'))
|
|
15
|
+
const serviceConfigs = allServiceConfigs.filter((config) => !config.skip)
|
|
16
|
+
const cfg = new Config()
|
|
17
|
+
const logtailToken = cfg.requireSecret('logtailToken')
|
|
18
|
+
const registryCredentials = cfg.requireSecret('registryCredentials')
|
|
19
|
+
|
|
20
|
+
const stack = getStack()
|
|
21
|
+
|
|
22
|
+
const doAppName = `{{projectName}}-${stack}`
|
|
23
|
+
|
|
24
|
+
const app = new App('{{projectName}}', {
|
|
25
|
+
spec: {
|
|
26
|
+
name: doAppName,
|
|
27
|
+
region: 'fra',
|
|
28
|
+
|
|
29
|
+
alerts: [{ rule: 'DEPLOYMENT_FAILED' }, { rule: 'DOMAIN_FAILED' }],
|
|
30
|
+
|
|
31
|
+
features: ['buildpack-stack=ubuntu-22'],
|
|
32
|
+
|
|
33
|
+
envs: [
|
|
34
|
+
...buildServiceUrlEnvs(serviceConfigs),
|
|
35
|
+
...buildServicePortEnvs(serviceConfigs),
|
|
36
|
+
],
|
|
37
|
+
|
|
38
|
+
services: buildServices({
|
|
39
|
+
serviceConfigs,
|
|
40
|
+
registryCredentials,
|
|
41
|
+
logtailToken,
|
|
42
|
+
}),
|
|
43
|
+
|
|
44
|
+
ingress: {
|
|
45
|
+
rules: buildIngressRules(serviceConfigs),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Outputs
|
|
51
|
+
export const appId = app.id
|
|
52
|
+
export const appDefaultIngress = app.defaultIngress
|
|
53
|
+
export const internalUrls = buildInternalUrls(serviceConfigs)
|
|
54
|
+
export const serviceUrls = app.defaultIngress.apply((baseUrl) =>
|
|
55
|
+
buildExternalUrls(serviceConfigs, baseUrl ?? ''),
|
|
56
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "infra",
|
|
3
|
+
"private": true,
|
|
4
|
+
"scripts": {
|
|
5
|
+
"generate-env": "bunx generate-env",
|
|
6
|
+
"lint": "biome lint --fix",
|
|
7
|
+
"pulumi": "pulumi"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@crossdelta/cloudevents": "^0.5.3",
|
|
11
|
+
"@crossdelta/infrastructure": "^0.5.2",
|
|
12
|
+
"{{scope}}/contracts": "workspace:*",
|
|
13
|
+
"@pulumi/digitalocean": "^4.55.0",
|
|
14
|
+
"@pulumi/kubernetes": "^4.21.0",
|
|
15
|
+
"@pulumi/pulumi": "^3.208.0",
|
|
16
|
+
"tsx": "^4.19.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@biomejs/biome": "2.3.8",
|
|
20
|
+
"@types/node": "^24.10.1",
|
|
21
|
+
"typescript": "^5.9.3"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"outDir": "./dist"
|
|
12
|
+
},
|
|
13
|
+
"include": ["**/*.ts"],
|
|
14
|
+
"exclude": ["node_modules"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "pf dev",
|
|
6
|
+
"generate-env": "cd infra && bun run generate-env",
|
|
7
|
+
"build": "dotenv -e .env.local -- turbo run build",
|
|
8
|
+
"preview": "dotenv -e .env.local -- turbo run preview",
|
|
9
|
+
"lint": "turbo run lint",
|
|
10
|
+
"format": "turbo run format",
|
|
11
|
+
"pulumi": "cd infra && pulumi",
|
|
12
|
+
"test": "dotenv -e .env.local -- turbo run test"
|
|
13
|
+
},
|
|
14
|
+
"pf": {
|
|
15
|
+
"registry": "{{projectName}}/platform",
|
|
16
|
+
"commands": {
|
|
17
|
+
"pulumi": {
|
|
18
|
+
"cwd": "infra"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"paths": {
|
|
22
|
+
"services": {
|
|
23
|
+
"path": "services",
|
|
24
|
+
"watch": true
|
|
25
|
+
},
|
|
26
|
+
"apps": {
|
|
27
|
+
"path": "apps",
|
|
28
|
+
"watch": true
|
|
29
|
+
},
|
|
30
|
+
"packages": {
|
|
31
|
+
"path": "packages",
|
|
32
|
+
"watch": true
|
|
33
|
+
},
|
|
34
|
+
"contracts": {
|
|
35
|
+
"path": "packages/contracts"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@crossdelta/cloudevents": "latest",
|
|
41
|
+
"@crossdelta/telemetry": "latest"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@biomejs/biome": "2.3.7",
|
|
45
|
+
"@crossdelta/platform-sdk": "latest",
|
|
46
|
+
"dotenv-cli": "^8.0.0",
|
|
47
|
+
"turbo": "^2.5.6"
|
|
48
|
+
},
|
|
49
|
+
"packageManager": "{{packageManagerVersion}}",
|
|
50
|
+
"workspaces": ["packages/*", "apps/*", "services/*", "infra"]
|
|
51
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# {{scope}}/contracts
|
|
2
|
+
|
|
3
|
+
Shared event contracts for cross-service event consumption.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This package contains **shared event definitions** (contracts) for events that are consumed by multiple services. When an event is used by more than one service, define it here to ensure type consistency across the platform.
|
|
8
|
+
|
|
9
|
+
## Structure
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
src/
|
|
13
|
+
├── events/ # Event contracts grouped by domain
|
|
14
|
+
│ ├── orders/ # Orders domain
|
|
15
|
+
│ │ ├── created.ts # orders.created event
|
|
16
|
+
│ │ ├── updated.ts # orders.updated event
|
|
17
|
+
│ │ └── index.ts # Re-exports
|
|
18
|
+
│ ├── customers/ # Customers domain
|
|
19
|
+
│ │ ├── updated.ts # customers.updated event
|
|
20
|
+
│ │ └── index.ts # Re-exports
|
|
21
|
+
│ └── index.ts # Re-exports all domains
|
|
22
|
+
├── stream-policies.ts # NATS JetStream retention policies
|
|
23
|
+
└── index.ts # Main export file
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Contract files contain:**
|
|
27
|
+
- Zod schema definition
|
|
28
|
+
- Contract object (type + schema + **channel metadata**)
|
|
29
|
+
- TypeScript type inference
|
|
30
|
+
|
|
31
|
+
**Channel metadata** defines which NATS JetStream stream the event belongs to. This enables infrastructure-as-code workflows where streams are auto-created in dev (`pf dev`) and materialized via Pulumi in production.
|
|
32
|
+
|
|
33
|
+
## Adding New Events
|
|
34
|
+
|
|
35
|
+
### Using the CLI (Recommended)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pf event add products.created --fields "productId:string,name:string,price:number"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This will create `src/events/products/created.ts` with proper domain structure.
|
|
42
|
+
|
|
43
|
+
### Manual Creation
|
|
44
|
+
|
|
45
|
+
See [Adding Events](#manual-creation) section below.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### In Event Handlers (Consumers)
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { handleEvent } from '@crossdelta/cloudevents'
|
|
53
|
+
import { OrdersCreatedContract, type OrdersCreatedData } from '{{scope}}/contracts'
|
|
54
|
+
|
|
55
|
+
export default handleEvent(OrdersCreatedContract, async (data: OrdersCreatedData) => {
|
|
56
|
+
// data is fully typed from contract
|
|
57
|
+
console.log(data.orderId, data.customerId)
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### In Publishers (REST APIs)
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { publish } from '@crossdelta/cloudevents'
|
|
65
|
+
import { OrdersCreatedContract } from '{{scope}}/contracts'
|
|
66
|
+
|
|
67
|
+
// Type-safe publishing with contract
|
|
68
|
+
await publish(OrdersCreatedContract, {
|
|
69
|
+
orderId: 'order-123',
|
|
70
|
+
customerId: 'cust-456',
|
|
71
|
+
total: 99.99,
|
|
72
|
+
items: [...]
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### In Use-Cases
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import type { OrdersCreatedData } from '{{scope}}/contracts'
|
|
80
|
+
|
|
81
|
+
export const processOrder = async (data: OrdersCreatedData) => {
|
|
82
|
+
// Use typed event data
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Adding New Contracts
|
|
87
|
+
|
|
88
|
+
Contracts are **auto-generated** when you create event handlers:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# 1. Create service with event handler
|
|
92
|
+
pf new hono-micro notifications --ai -d "Sends emails on orders.created events"
|
|
93
|
+
|
|
94
|
+
# 2. Add event (creates contract, mock, handler)
|
|
95
|
+
pf event add orders.created --service services/notifications
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This creates:
|
|
99
|
+
- `packages/contracts/src/events/orders-created.ts` (contract)
|
|
100
|
+
- `packages/contracts/src/events/orders-created.mock.json` (mock)
|
|
101
|
+
- Updates `packages/contracts/src/index.ts` (exports)
|
|
102
|
+
|
|
103
|
+
### Manual Contract Creation
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// packages/contracts/src/events/orders/created.ts
|
|
107
|
+
import { createContract } from '@crossdelta/cloudevents'
|
|
108
|
+
import { z } from 'zod'
|
|
109
|
+
|
|
110
|
+
export const OrdersCreatedSchema = z.object({
|
|
111
|
+
orderId: z.string(),
|
|
112
|
+
customerId: z.string(),
|
|
113
|
+
total: z.number(),
|
|
114
|
+
items: z.array(z.object({
|
|
115
|
+
productId: z.string(),
|
|
116
|
+
quantity: z.number(),
|
|
117
|
+
price: z.number(),
|
|
118
|
+
})),
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
export const OrdersCreatedContract = createContract({
|
|
122
|
+
type: 'orders.created',
|
|
123
|
+
channel: { stream: 'ORDERS' }, // Stream routing metadata
|
|
124
|
+
schema: OrdersCreatedSchema,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
export type OrdersCreatedData = z.infer<typeof OrdersCreatedContract.schema>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Channel Metadata:**
|
|
131
|
+
- `stream` - NATS JetStream stream name (e.g., `ORDERS`)
|
|
132
|
+
- `subject` - Optional, defaults to event type (e.g., `orders.created`)
|
|
133
|
+
|
|
134
|
+
**Stream Materialization:**
|
|
135
|
+
1. **Development**: `pf dev` scans contracts and auto-creates ephemeral streams from channel metadata
|
|
136
|
+
2. **Production**: Pulumi collects streams from contracts and materializes with retention policies
|
|
137
|
+
|
|
138
|
+
See [`infra/streams/README.md`](../../infra/streams/README.md) for details.
|
|
139
|
+
|
|
140
|
+
## Testing with Mocks
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# List all available events
|
|
144
|
+
pf event list
|
|
145
|
+
|
|
146
|
+
# Publish mock event
|
|
147
|
+
pf event publish orders.created
|
|
148
|
+
|
|
149
|
+
# Publish with custom data
|
|
150
|
+
pf event publish orders.created --data '{"orderId":"test-123"}'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Guidelines
|
|
154
|
+
|
|
155
|
+
- ✅ Use contracts for **shared events** (multi-consumer)
|
|
156
|
+
- ✅ Export both contract and inferred type
|
|
157
|
+
- ✅ Keep contracts minimal and focused
|
|
158
|
+
- ✅ Generate mocks for all contracts
|
|
159
|
+
- ❌ Don't put business logic in contracts
|
|
160
|
+
- ❌ Don't include event handlers in this package
|
|
161
|
+
|
|
162
|
+
**Naming conventions:**
|
|
163
|
+
- Contracts: `OrdersCreatedContract` (plural namespace)
|
|
164
|
+
- Types: `OrdersCreatedData`
|
|
165
|
+
- Files: `orders-created.ts`
|
|
166
|
+
- Event types: `orders.created` (plural namespace, dot notation)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{scope}}/contracts",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./src/index.ts",
|
|
10
|
+
"default": "./src/index.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@crossdelta/cloudevents": "^0.5.3",
|
|
15
|
+
"@crossdelta/infrastructure": "^0.5.2",
|
|
16
|
+
"zod": "^4.0.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@biomejs/biome": "^1.9.4",
|
|
20
|
+
"typescript": "^5.7.2"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Contracts Index
|
|
3
|
+
*
|
|
4
|
+
* Re-exports all event contracts for convenient importing.
|
|
5
|
+
* Services can import from '{{scope}}/contracts' instead of deep imports.
|
|
6
|
+
*
|
|
7
|
+
* Structure:
|
|
8
|
+
* - events/<domain>/<event>.ts - Individual event contracts
|
|
9
|
+
* - events/<domain>/index.ts - Domain-specific exports
|
|
10
|
+
* - This file: Re-exports all domains
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Add your event domains here as you create them:
|
|
14
|
+
// export * from './orders'
|
|
15
|
+
// export * from './customers'
|
|
16
|
+
// export * from './products'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Export your event contracts and schemas here
|
|
2
|
+
// Use the CLI to generate contracts:
|
|
3
|
+
//
|
|
4
|
+
// pf event add orders.created --fields "orderId:string,total:number"
|
|
5
|
+
//
|
|
6
|
+
// This will create: src/events/orders/created.ts
|
|
7
|
+
// Then uncomment the exports below:
|
|
8
|
+
|
|
9
|
+
// export * from './events'
|
|
10
|
+
// export * from './stream-policies'
|