Add TypeScript/Bun scaffold specification
This commit is contained in:
252
docs/ts-scaffold-spec.md
Normal file
252
docs/ts-scaffold-spec.md
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
Scaffold the Tessera ticketing system as a TypeScript/Bun project at /home/gjermund/projects/tessera.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
- Runtime: Bun 1.3+
|
||||||
|
- Framework: Hono (web framework)
|
||||||
|
- Database: PostgreSQL + Drizzle ORM
|
||||||
|
- Validation: Zod
|
||||||
|
- Templating: Handlebars
|
||||||
|
- Logging: Pino
|
||||||
|
- Testing: Bun test
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Initialize the project
|
||||||
|
```bash
|
||||||
|
cd /home/gjermund/projects/tessera
|
||||||
|
bun init -y
|
||||||
|
```
|
||||||
|
Set name to "tessera", type to "module".
|
||||||
|
|
||||||
|
### 2. Install dependencies
|
||||||
|
```bash
|
||||||
|
bun add hono zod drizzle-orm pg handlebars pino dotenv
|
||||||
|
bun add -d drizzle-kit @types/pg @types/handlebars bun-types
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Create tsconfig.json
|
||||||
|
- target: ES2022
|
||||||
|
- module: ESNext
|
||||||
|
- moduleResolution: bundler
|
||||||
|
- strict: true
|
||||||
|
- paths: "@/*" → "./src/*"
|
||||||
|
|
||||||
|
### 4. Create the directory structure
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
index.ts
|
||||||
|
config.ts
|
||||||
|
db/
|
||||||
|
schema.ts
|
||||||
|
index.ts
|
||||||
|
migrate.ts
|
||||||
|
models/
|
||||||
|
ticket.ts
|
||||||
|
queue.ts
|
||||||
|
transaction.ts
|
||||||
|
scrip.ts
|
||||||
|
lifecycle.ts
|
||||||
|
custom-field.ts
|
||||||
|
user.ts
|
||||||
|
scrip/
|
||||||
|
engine.ts
|
||||||
|
conditions.ts
|
||||||
|
actions.ts
|
||||||
|
templates.ts
|
||||||
|
lifecycle/
|
||||||
|
validator.ts
|
||||||
|
routes/
|
||||||
|
health.ts
|
||||||
|
tickets.ts
|
||||||
|
queues.ts
|
||||||
|
scrips.ts
|
||||||
|
custom-fields.ts
|
||||||
|
lifecycles.ts
|
||||||
|
middleware/
|
||||||
|
error.ts
|
||||||
|
logging.ts
|
||||||
|
drizzle/
|
||||||
|
migrations/
|
||||||
|
tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Create each file with actual working code
|
||||||
|
|
||||||
|
#### src/index.ts
|
||||||
|
- Create Hono app
|
||||||
|
- Register routes from ./routes/*
|
||||||
|
- Register middleware (error handler, request logging)
|
||||||
|
- Export the app
|
||||||
|
|
||||||
|
#### src/config.ts
|
||||||
|
- Parse DATABASE_URL, SERVER_HOST, SERVER_PORT from env
|
||||||
|
- Zod schema with defaults: host=127.0.0.1, port=8080
|
||||||
|
- Export typed config object
|
||||||
|
|
||||||
|
#### src/db/schema.ts
|
||||||
|
Define Drizzle tables matching these PostgreSQL tables:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
tickets: id (uuid pk default gen_random_uuid), subject (text not null), queue_id (uuid not null ref queues.id), status (text not null), owner_id (uuid ref users.id), creator_id (uuid not null ref users.id), created_at (timestamptz default now), updated_at (timestamptz default now), started_at (timestamptz), resolved_at (timestamptz)
|
||||||
|
|
||||||
|
queues: id (uuid pk default gen_random_uuid), name (text not null unique), description (text), lifecycle_id (uuid ref lifecycles.id), created_at (timestamptz default now)
|
||||||
|
|
||||||
|
lifecycles: id (uuid pk default gen_random_uuid), name (text not null unique), definition (jsonb not null), created_at (timestamptz default now)
|
||||||
|
|
||||||
|
transactions: id (uuid pk default gen_random_uuid), ticket_id (uuid not null ref tickets.id on delete cascade), transaction_type (text not null), field (text), old_value (text), new_value (text), data (jsonb), creator_id (uuid not null ref users.id), created_at (timestamptz default now)
|
||||||
|
|
||||||
|
scrips: id (uuid pk default gen_random_uuid), queue_id (uuid ref queues.id, null=global), name (text not null), description (text), condition_type (text not null), condition_config (jsonb not null default '{}'), action_type (text not null), action_config (jsonb not null default '{}'), template_id (uuid ref templates.id), stage (text not null default 'TransactionCreate'), sort_order (int not null default 0), disabled (bool not null default false), created_at (timestamptz default now)
|
||||||
|
|
||||||
|
templates: id (uuid pk default gen_random_uuid), name (text not null), queue_id (uuid ref queues.id, null=global), subject_template (text not null), body_template (text not null), created_at (timestamptz default now)
|
||||||
|
|
||||||
|
custom_fields: id (uuid pk default gen_random_uuid), name (text not null), field_type (text not null), values (jsonb), max_values (int not null default 1), pattern (text), created_at (timestamptz default now)
|
||||||
|
|
||||||
|
queue_custom_fields: id (uuid pk default gen_random_uuid), queue_id (uuid not null ref queues.id), custom_field_id (uuid not null ref custom_fields.id on delete cascade), sort_order (int not null default 0), unique(queue_id, custom_field_id)
|
||||||
|
|
||||||
|
custom_field_values: id (uuid pk default gen_random_uuid), custom_field_id (uuid not null ref custom_fields.id on delete cascade), ticket_id (uuid not null ref tickets.id on delete cascade), value (text not null), created_at (timestamptz default now), unique(custom_field_id, ticket_id, value)
|
||||||
|
|
||||||
|
users: id (uuid pk default gen_random_uuid), username (text not null unique), email (text), created_at (timestamptz default now)
|
||||||
|
```
|
||||||
|
|
||||||
|
Add indexes on: tickets(queue_id), tickets(status), transactions(ticket_id), transactions(created_at), scrips(queue_id), custom_field_values(ticket_id), custom_field_values(custom_field_id).
|
||||||
|
|
||||||
|
#### src/db/index.ts
|
||||||
|
- Create and export a Drizzle connection pool function
|
||||||
|
- Accept DATABASE_URL parameter, return drizzle instance
|
||||||
|
|
||||||
|
#### src/db/migrate.ts
|
||||||
|
- Standalone script: `bun run src/db/migrate.ts`
|
||||||
|
- Run Drizzle migrations from the drizzle/migrations directory
|
||||||
|
|
||||||
|
#### src/models/*.ts
|
||||||
|
Each model file exports TypeScript interfaces/types (from Drizzle inference) and Zod schemas for validation.
|
||||||
|
|
||||||
|
ticket.ts: Ticket interface, CreateTicketSchema (subject, queue_id), UpdateTicketSchema (subject?, status?, owner_id?)
|
||||||
|
queue.ts: Queue interface, CreateQueueSchema
|
||||||
|
transaction.ts: Transaction interface, TransactionType enum
|
||||||
|
scrip.ts: Scrip interface, ScripStage enum, CreateScripSchema
|
||||||
|
lifecycle.ts: Lifecycle interface
|
||||||
|
custom-field.ts: CustomField interface, CustomFieldType enum
|
||||||
|
|
||||||
|
#### src/scrip/engine.ts
|
||||||
|
ScripEngine class:
|
||||||
|
- constructor(db: Drizzle instance)
|
||||||
|
- prepare(ticketId: string, transactions: Transaction[]): PreparedScrip[]
|
||||||
|
- Load scrips from DB (global + queue-specific, filtered by condition_type matching transaction type, sorted by sort_order)
|
||||||
|
- For each: evaluate condition → if matches, render template → build PreparedScrip
|
||||||
|
- commit(prepared: PreparedScrip[]): ScripResult[]
|
||||||
|
- Execute each action
|
||||||
|
- Returns results
|
||||||
|
- PreparedScrip type: { scripId, scripName, actionType, actionPayload, dryRun }
|
||||||
|
- ScripResult type: { scripId, success, message }
|
||||||
|
- For MVP, conditions and actions are mocked (log what would happen)
|
||||||
|
|
||||||
|
#### src/scrip/conditions.ts
|
||||||
|
- ConditionEvaluator interface: evaluate(ticket, transactions) → boolean
|
||||||
|
- OnCreate: true if any transaction is type "Create"
|
||||||
|
- OnStatusChange: true if any transaction is type "StatusChange"
|
||||||
|
- OnResolve: true if any status change goes to a "resolved"-like status
|
||||||
|
|
||||||
|
#### src/scrip/actions.ts
|
||||||
|
- ActionExecutor interface: execute(payload) → { success, message }
|
||||||
|
- SendEmail: logs the email it would send (subject, body, recipients from payload)
|
||||||
|
- Webhook: logs the webhook it would fire (url, method, headers, body from payload)
|
||||||
|
- SetCustomField: logs the CF it would set (field_id, value from payload)
|
||||||
|
|
||||||
|
#### src/scrip/templates.ts
|
||||||
|
- TemplateRenderer class using Handlebars
|
||||||
|
- render(template, context) → { subject, body }
|
||||||
|
- Context includes: ticket (id, subject, status, etc.), queue (name), transaction (type, field, old_value, new_value), custom_fields (map of name→value)
|
||||||
|
|
||||||
|
#### src/lifecycle/validator.ts
|
||||||
|
- LifecycleValidator class
|
||||||
|
- validateTransition(lifecycleDef, fromStatus, toStatus) → { valid, error? }
|
||||||
|
- Check that toStatus is in the lifecycle's valid statuses
|
||||||
|
- Check that fromStatus→toStatus is an allowed transition
|
||||||
|
- Handle wildcard transitions (*)
|
||||||
|
- isResolvedStatus(lifecycleDef, status) → boolean
|
||||||
|
- A status is "resolved" if it's in the inactive category
|
||||||
|
- For MVP, lifecycle definition is a JSON object: { statuses: { initial: [...], active: [...], inactive: [...] }, transitions: { "from": ["to", ...], ... } }
|
||||||
|
|
||||||
|
#### src/routes/*.ts
|
||||||
|
Each route file exports a Hono router:
|
||||||
|
|
||||||
|
health.ts: GET /health → { status: "ok", version: "0.1.0" }
|
||||||
|
|
||||||
|
tickets.ts:
|
||||||
|
- GET / → list tickets (query params: queue_id?, status?)
|
||||||
|
- POST / → create ticket (validate body, insert, return ticket)
|
||||||
|
- GET /:id → get ticket with custom field values
|
||||||
|
- PATCH /:id → update ticket (validate transition, create transaction, run scrips, return result)
|
||||||
|
- POST /:id/preview → dry-run (validate transition, run prepare only, return prepared scrips)
|
||||||
|
- GET /:id/transactions → list transactions for ticket
|
||||||
|
|
||||||
|
queues.ts:
|
||||||
|
- GET / → list queues
|
||||||
|
- POST / → create queue
|
||||||
|
|
||||||
|
scrips.ts:
|
||||||
|
- GET / → list scrips
|
||||||
|
- POST / → create scrip
|
||||||
|
- GET /:id → get scrip
|
||||||
|
- PATCH /:id → update scrip
|
||||||
|
|
||||||
|
custom-fields.ts:
|
||||||
|
- GET / → list custom fields
|
||||||
|
- POST / → create custom field
|
||||||
|
|
||||||
|
lifecycles.ts:
|
||||||
|
- GET / → list lifecycles
|
||||||
|
- POST / → create lifecycle
|
||||||
|
|
||||||
|
### 6. Create drizzle.config.ts
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "./src/db/schema.ts",
|
||||||
|
out: "./drizzle/migrations",
|
||||||
|
dialect: "postgresql",
|
||||||
|
dbCredentials: {
|
||||||
|
url: process.env.DATABASE_URL!,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Update .gitignore
|
||||||
|
Add: node_modules/, .env, dist/, bun.lock (if not already ignored)
|
||||||
|
|
||||||
|
### 8. Create .env.example
|
||||||
|
```
|
||||||
|
DATABASE_URL=postgres://tessera:password@localhost:5432/tessera
|
||||||
|
SERVER_HOST=127.0.0.1
|
||||||
|
SERVER_PORT=8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Create a drizzle migration
|
||||||
|
```bash
|
||||||
|
bun run drizzle-kit generate
|
||||||
|
```
|
||||||
|
Then verify a migration SQL file was created in drizzle/migrations/
|
||||||
|
|
||||||
|
### 10. Verify the project compiles
|
||||||
|
```bash
|
||||||
|
bun run src/index.ts --help 2>&1 | head -5
|
||||||
|
```
|
||||||
|
Or just check TypeScript: `bun run --print "import './src/index.ts'"` to verify no syntax errors.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
- Use `bun init` and `bun add` — not npm/yarn/pnpm
|
||||||
|
- All code must type-check. Do `bun run --print` to verify no TS errors
|
||||||
|
- Use proper TypeScript types — no `any` unless truly unavoidable
|
||||||
|
- Drizzle schemas must match the architecture exactly
|
||||||
|
- Routes must be functional — not just stubs returning 501
|
||||||
|
- The scrip engine must have the prepare/commit two-phase pattern even if actions are mocked
|
||||||
|
- Lifecycle validator must handle wildcard transitions (*)
|
||||||
|
- Commit to git after each major step
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
1. `ls -la src/` — all directories exist with proper files
|
||||||
|
2. `bun run src/db/schema.ts` — Drizzle schema parses without errors (or use drizzle-kit check)
|
||||||
|
3. `bun run src/index.ts` — should start a server on port 8080 (or at least not crash with import errors)
|
||||||
|
4. `curl http://localhost:8080/health` — should return {"status":"ok","version":"0.1.0"}
|
||||||
Reference in New Issue
Block a user