@wabot-dev/create 0.0.3 → 0.0.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/lib/cli.mjs +1 -0
- package/lib/create-project.mjs +161 -74
- package/lib/i18n.mjs +167 -0
- package/lib/skills/install.mjs +60 -1
- package/lib/skills/registry.mjs +53 -6
- package/package.json +1 -1
- package/skills/wabot-async/SKILL.md +139 -0
- package/skills/wabot-auth/SKILL.md +149 -0
- package/skills/wabot-chat/SKILL.md +136 -0
- package/skills/wabot-di-config/SKILL.md +117 -0
- package/skills/wabot-framework/SKILL.md +69 -22
- package/skills/wabot-framework/references/quickstart.md +71 -42
- package/skills/wabot-mindset/SKILL.md +155 -0
- package/skills/wabot-ops/SKILL.md +151 -0
- package/skills/wabot-persistence/SKILL.md +155 -0
- package/skills/wabot-rest-socket/SKILL.md +163 -0
- package/skills/wabot-validation/SKILL.md +104 -0
- package/skills/wabot-framework/agents/openai.yaml +0 -5
- package/skills/wabot-framework/references/async-and-cron.md +0 -57
- package/skills/wabot-framework/references/controllers-and-auth.md +0 -104
- package/skills/wabot-framework/references/core-patterns.md +0 -83
- package/skills/wabot-framework/references/full-example.md +0 -278
- package/skills/wabot-framework/references/mindset-and-chat.md +0 -97
- package/skills/wabot-framework/references/ops.md +0 -58
- package/skills/wabot-framework/references/persistence.md +0 -85
- package/skills/wabot-framework/references/validation-and-data.md +0 -88
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wabot-persistence
|
|
3
|
+
description: Use when defining entities, repositories, queries, or per-adapter query extensions in Wabot. Covers Entity / IEntityData, @repository, the @query method-name DSL (find/findOne/count/exists/delete + And/Or + operators), @queryExtension, @memExtension / MemoryRepositoryExtension, @pgExtension / PgRepositoryExtension, and how the active adapter is chosen automatically by the project runner.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Persistence
|
|
7
|
+
|
|
8
|
+
Wabot ships a small ORM. You write a domain entity, a `@repository`-decorated class with method-name queries, and (optionally) an adapter-specific extension. The project runner picks the adapter automatically based on `DATABASE_URL`.
|
|
9
|
+
|
|
10
|
+
## Entity
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { Entity, IEntityData } from '@wabot-dev/framework'
|
|
14
|
+
|
|
15
|
+
export type IGameStatus = 'backlog' | 'playing' | 'finished' | 'abandoned'
|
|
16
|
+
|
|
17
|
+
export interface IGameData extends IEntityData {
|
|
18
|
+
userId: string
|
|
19
|
+
title: string
|
|
20
|
+
status: IGameStatus
|
|
21
|
+
hoursPlayed: number
|
|
22
|
+
addedAt: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class Game extends Entity<IGameData> {}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`IEntityData` adds `id?: string`, `createdAt?: number | null`, `discardedAt?: number | null`. The id/createdAt are filled in by the repository on `create`.
|
|
29
|
+
|
|
30
|
+
`Entity` exposes:
|
|
31
|
+
- `id` (throws if not yet created)
|
|
32
|
+
- `createdAt: Date`
|
|
33
|
+
- `update(partial)` — merges allowed fields and sets `updatedAt`; rejects writes to id/createdAt/discardedAt
|
|
34
|
+
- `wasCreated()`, `validate()`
|
|
35
|
+
- `lockerKey()` — returns the id, so any `Entity` can be passed to `Locker.withKey(...)`
|
|
36
|
+
|
|
37
|
+
Date values stored on entity data should be epoch milliseconds (`number`) — the `Mapper` and `Storable` deep-copy convert `Date` to ms automatically.
|
|
38
|
+
|
|
39
|
+
## Repository
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { CrudRepository, query, queryExtension, repository } from '@wabot-dev/framework'
|
|
43
|
+
import { Game, IGameStatus } from './Game'
|
|
44
|
+
import { IGameRepositoryExtensions } from './IGameRepositoryExtensions'
|
|
45
|
+
|
|
46
|
+
@repository({ table: 'game', constructor: Game })
|
|
47
|
+
export class GameRepository
|
|
48
|
+
extends CrudRepository<Game, IGameRepositoryExtensions>
|
|
49
|
+
implements IGameRepositoryExtensions
|
|
50
|
+
{
|
|
51
|
+
@query() declare findByUserIdAndStatus: (userId: string, status: IGameStatus) => Promise<Game[]>
|
|
52
|
+
@query() declare countByUserIdAndStatus: (userId: string, status: IGameStatus) => Promise<number>
|
|
53
|
+
@queryExtension() declare findLongestInBacklog: (userId: string, limit: number) => Promise<Game[]>
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The decorator generates concrete implementations for `CrudRepository`'s methods (`find`, `findOrThrow`, `findByIds`, `findAll`, `create`, `update`, `delete`) and for every `@query()` declare. It also installs a lazy `.extension` accessor backed by the active adapter.
|
|
58
|
+
|
|
59
|
+
`@repository` applies `@singleton()` for you. Inject `GameRepository` directly anywhere.
|
|
60
|
+
|
|
61
|
+
## `@query` method-name DSL
|
|
62
|
+
|
|
63
|
+
`@query()` reads the method name and turns it into a structured query. Supported grammar:
|
|
64
|
+
|
|
65
|
+
- Prefix: `find`, `findOne`, `count`, `exists`, `delete`.
|
|
66
|
+
- Conditions: PascalCase field names joined by `And` / `Or`.
|
|
67
|
+
- Operators (suffix on the field): `Equals` (default), `Not`, `Like`, `NotLike`, `In`, `NotIn`, `Gt`, `Gte`, `Lt`, `Lte`, `IsNull`, `IsNotNull`.
|
|
68
|
+
- Ordering: append `OrderByField[Asc|Desc]` (chain multiple with `And`).
|
|
69
|
+
- Limit: append `LimitN`.
|
|
70
|
+
|
|
71
|
+
Examples that work:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
@query() declare findByUserId: (userId: string) => Promise<Game[]>
|
|
75
|
+
@query() declare findOneByUserIdAndStatus: (userId: string, status: IGameStatus) => Promise<Game | null>
|
|
76
|
+
@query() declare countByStatusInAndAddedAtGt: (statuses: IGameStatus[], addedAt: number) => Promise<number>
|
|
77
|
+
@query() declare existsByTitleLike: (title: string) => Promise<boolean>
|
|
78
|
+
@query() declare deleteByDiscardedAtIsNotNull: () => Promise<void>
|
|
79
|
+
@query() declare findByUserIdOrderByAddedAtAscLimit10: (userId: string) => Promise<Game[]>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Argument count and order must match the conditions left-to-right. `IsNull`/`IsNotNull` take no argument; `In`/`NotIn` take an array.
|
|
83
|
+
|
|
84
|
+
If your query cannot be expressed by the DSL (joins, aggregates, JSON paths) use `@queryExtension()` instead — see below.
|
|
85
|
+
|
|
86
|
+
## Adapter extensions
|
|
87
|
+
|
|
88
|
+
The runner picks one adapter per process:
|
|
89
|
+
|
|
90
|
+
- `MemoryRepositoryAdapter` when `DATABASE_URL` is missing or not a `postgres://` URL.
|
|
91
|
+
- `PgJsonRepositoryAdapter` when `DATABASE_URL` is a Postgres URL (data is stored as a JSON blob per row).
|
|
92
|
+
|
|
93
|
+
For each `@queryExtension()` method you must provide one implementation per adapter you support, in classes decorated with `@memExtension(Repo)` and/or `@pgExtension(Repo)`:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { memExtension, MemoryRepositoryExtension, pgExtension, PgRepositoryExtension } from '@wabot-dev/framework'
|
|
97
|
+
|
|
98
|
+
@memExtension(GameRepository)
|
|
99
|
+
export class GameMemoryQueries
|
|
100
|
+
extends MemoryRepositoryExtension<Game>
|
|
101
|
+
implements IGameRepositoryExtensions
|
|
102
|
+
{
|
|
103
|
+
async findLongestInBacklog(userId: string, limit: number) {
|
|
104
|
+
return [...this.items.values()]
|
|
105
|
+
.filter((g) => g['data'].userId === userId && g['data'].status === 'backlog')
|
|
106
|
+
.sort((a, b) => a['data'].addedAt - b['data'].addedAt)
|
|
107
|
+
.slice(0, limit)
|
|
108
|
+
.map((g) => this.clone(g))
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@pgExtension(GameRepository)
|
|
113
|
+
export class GamePgQueries
|
|
114
|
+
extends PgRepositoryExtension<Game>
|
|
115
|
+
implements IGameRepositoryExtensions
|
|
116
|
+
{
|
|
117
|
+
async findLongestInBacklog(userId: string, limit: number) {
|
|
118
|
+
const sql = `
|
|
119
|
+
SELECT ${this['columns']} FROM ${this['table']}
|
|
120
|
+
WHERE "data"->>'userId' = $1 AND "data"->>'status' = 'backlog'
|
|
121
|
+
ORDER BY ("data"->>'addedAt')::numeric ASC
|
|
122
|
+
LIMIT $2
|
|
123
|
+
`
|
|
124
|
+
return this['query'](sql, [userId, limit])
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- The extension class must extend `MemoryRepositoryExtension<T>` / `PgRepositoryExtension<T>`. The decorator throws otherwise.
|
|
130
|
+
- Inside a Pg extension, use `this['columns']` / `this['table']` / `this['query'](sql, params)` from `PgRepositoryBase`. The Pg JSON adapter stores fields under a `data` JSONB column.
|
|
131
|
+
- A repository may opt in to only one adapter — if you only ship `@memExtension`, the repository will throw when invoked under Postgres.
|
|
132
|
+
|
|
133
|
+
## Transactions
|
|
134
|
+
|
|
135
|
+
Wrap business methods with `@transaction(['pg'])` (or `@transaction()` to run in every registered adapter):
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { transaction } from '@wabot-dev/framework'
|
|
139
|
+
|
|
140
|
+
class CheckoutService {
|
|
141
|
+
@transaction(['pg'])
|
|
142
|
+
async checkout(orderId: string) {
|
|
143
|
+
// all repository writes here run inside one PG transaction
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The framework injects a `PgTransactionAdapter` automatically under Postgres mode. See `wabot-async` for command-level transactions.
|
|
149
|
+
|
|
150
|
+
## Rules
|
|
151
|
+
|
|
152
|
+
- Always declare the full type on `@query() declare` properties — the decorator uses the method name only, but TypeScript needs the signature.
|
|
153
|
+
- Don't write SQL or memory iteration inside `@query()` methods — that's what `@queryExtension` is for.
|
|
154
|
+
- Never mutate `entity['data']` directly outside the entity class. Use `entity.update(...)` or domain methods.
|
|
155
|
+
- Repository classes are singletons; do not store per-request state on them.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wabot-rest-socket
|
|
3
|
+
description: Use when exposing HTTP endpoints or Socket.IO namespaces from a Wabot app. Covers @restController, @onGet / @onPost / @onPut / @onDelete, @middleware and IMiddleware, the EXPRESS_REQ / EXPRESS_RES tokens, the RestRequest base for raw-request access, plus @socketController, @onSocketEvent, @handshakeMiddlewares and IHandshakeMiddleware. Argument validation is shared with wabot-validation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# REST and Socket controllers
|
|
7
|
+
|
|
8
|
+
Wabot's HTTP and Socket layers sit on top of Express and Socket.IO. You write controllers; the framework boots the server (default `PORT=3000`).
|
|
9
|
+
|
|
10
|
+
## REST controller
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import {
|
|
14
|
+
injectable,
|
|
15
|
+
isNotEmpty,
|
|
16
|
+
isNumber,
|
|
17
|
+
isString,
|
|
18
|
+
middleware,
|
|
19
|
+
onGet,
|
|
20
|
+
onPost,
|
|
21
|
+
restController,
|
|
22
|
+
} from '@wabot-dev/framework'
|
|
23
|
+
|
|
24
|
+
class CreateOrderRequest {
|
|
25
|
+
@isString() @isNotEmpty() productId!: string
|
|
26
|
+
@isNumber() quantity!: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@injectable()
|
|
30
|
+
export class AuditMiddleware implements IMiddleware {
|
|
31
|
+
async handle(req, res, container) {
|
|
32
|
+
// record, throw CustomError to short-circuit
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@restController('/orders')
|
|
37
|
+
export class OrdersController {
|
|
38
|
+
constructor(private orders: OrderService) {}
|
|
39
|
+
|
|
40
|
+
@onGet('/:id')
|
|
41
|
+
async getOne(req: { id: string }) {
|
|
42
|
+
return this.orders.find(req.id)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@onPost()
|
|
46
|
+
@middleware(AuditMiddleware)
|
|
47
|
+
async create(req: CreateOrderRequest) {
|
|
48
|
+
return this.orders.create(req)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Path resolution: `restController('/orders')` + `onPost()` → `POST /orders`. `onGet('/:id')` → `GET /orders/:id`. Either decorator accepts a `string` path or `{ path, disableJsonParser?, disableUrlEncodedParser? }`.
|
|
54
|
+
|
|
55
|
+
Argument binding:
|
|
56
|
+
- 0 parameters → no binding.
|
|
57
|
+
- 1 parameter typed as a validator class → framework merges `req.body`, `req.query`, `req.params` and runs `validateAndTransform` against the class. Validation errors produce HTTP 400.
|
|
58
|
+
- 1 parameter typed as `IncomingMessage` (Node's `http` type) → raw request injected.
|
|
59
|
+
- 1 parameter that extends `RestRequest` → the full Express `Request` is passed (no merge). Use this for streaming or multipart.
|
|
60
|
+
|
|
61
|
+
`@restController` applies `@injectable()`. Endpoint methods may be `async`; the return value is `res.status(200).json(value ?? null)`. To control status or headers, inject the response via `@inject(EXPRESS_RES)` or throw a `CustomError({ httpCode, message, info? })`.
|
|
62
|
+
|
|
63
|
+
### Middleware
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { IMiddleware, injectable, middleware } from '@wabot-dev/framework'
|
|
67
|
+
|
|
68
|
+
@injectable()
|
|
69
|
+
export class RateLimit implements IMiddleware {
|
|
70
|
+
async handle(req, res, container) {
|
|
71
|
+
if (overLimit(req.ip)) throw new CustomError({ httpCode: 429, message: 'Slow down' })
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@restController('/api')
|
|
76
|
+
class ApiController {
|
|
77
|
+
@onGet('/me')
|
|
78
|
+
@middleware(RateLimit)
|
|
79
|
+
async me() { /* ... */ }
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Multiple `@middleware(X)` decorations run in declaration order. Middlewares are resolved out of the request's child container, so per-request injectables (e.g. `Auth`) work.
|
|
84
|
+
|
|
85
|
+
### Inheritance
|
|
86
|
+
|
|
87
|
+
Endpoint and middleware metadata is inherited. A subclass automatically picks up parent endpoints; the subclass's `@restController(path)` becomes the base path. Child endpoints override parents on a per-method-name basis.
|
|
88
|
+
|
|
89
|
+
### Tokens
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { container, EXPRESS_REQ, EXPRESS_RES, inject, injectable } from '@wabot-dev/framework'
|
|
93
|
+
import type { Request, Response } from 'express'
|
|
94
|
+
|
|
95
|
+
@injectable()
|
|
96
|
+
class CookieReader {
|
|
97
|
+
constructor(
|
|
98
|
+
@inject(EXPRESS_REQ) private req: Request,
|
|
99
|
+
@inject(EXPRESS_RES) private res: Response,
|
|
100
|
+
) {}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
These tokens live in the per-request child container only — never resolve them from the root.
|
|
105
|
+
|
|
106
|
+
## Socket controller
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import {
|
|
110
|
+
handshakeMiddlewares,
|
|
111
|
+
IHandshakeMiddleware,
|
|
112
|
+
injectable,
|
|
113
|
+
isNumber,
|
|
114
|
+
onSocketEvent,
|
|
115
|
+
socketController,
|
|
116
|
+
} from '@wabot-dev/framework'
|
|
117
|
+
import { Socket } from 'socket.io'
|
|
118
|
+
|
|
119
|
+
class JoinRoomReq {
|
|
120
|
+
@isNumber() roomId!: number
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@injectable()
|
|
124
|
+
class CookieAuthMw implements IHandshakeMiddleware {
|
|
125
|
+
async handle(socket: Socket, container) {
|
|
126
|
+
// throw CustomError to refuse the connection
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@socketController('chat')
|
|
131
|
+
@handshakeMiddlewares([CookieAuthMw])
|
|
132
|
+
export class ChatSocketController {
|
|
133
|
+
@onSocketEvent('connection')
|
|
134
|
+
async onConnect(socket: Socket) {
|
|
135
|
+
socket.emit('hello', { ok: true })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@onSocketEvent('join')
|
|
139
|
+
async onJoin(req: JoinRoomReq, socket: Socket) {
|
|
140
|
+
socket.join(`room:${req.roomId}`)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- `@socketController('chat')` → namespace `/chat`. Pass `undefined` or `''` for the default namespace `/`.
|
|
146
|
+
- `@onSocketEvent('join')` registers a listener. Method signature: `(req, socket?)`. If the first arg type is `Socket`, no body validation is done. Otherwise the body is validated against that DTO class. A `connection` event handler runs once when the socket connects.
|
|
147
|
+
- `@handshakeMiddlewares([Mw, ...])` runs the middlewares during the Socket.IO `use(...)` phase, before `connection`. Throw a `CustomError` to refuse the handshake.
|
|
148
|
+
|
|
149
|
+
If the client uses `socket.emit('join', payload, ack)` the return value of the handler is passed to `ack`. Throwing produces `ack({ error: ... })`.
|
|
150
|
+
|
|
151
|
+
## Boot
|
|
152
|
+
|
|
153
|
+
Both layers boot automatically when the project runner finds at least one `@restController` (REST) or `@socketController` (sockets). Manually they expose `runRestControllers([...])` / `runSocketControllers([...])`.
|
|
154
|
+
|
|
155
|
+
Both share one Express + HTTP server via `ExpressProvider` / `HttpServerProvider` (singleton). Set `PORT` in `.env` to change the listen port.
|
|
156
|
+
|
|
157
|
+
## Rules
|
|
158
|
+
|
|
159
|
+
- Always type the endpoint/socket parameter — the framework relies on `design:paramtypes` reflection to bind it.
|
|
160
|
+
- REST endpoints can have 0 or 1 parameter. More than one throws at boot.
|
|
161
|
+
- Socket events can have up to 2 parameters: `(req, socket)`. The framework injects the active `Socket`.
|
|
162
|
+
- Don't disable JSON parsing globally — disable per-endpoint via `@onPost({ path, disableJsonParser: true })` when you need raw bytes.
|
|
163
|
+
- For auth, prefer `@jwtGuard()` / `@apiKeyGuard()` (see `wabot-auth`) over hand-rolled middleware.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wabot-validation
|
|
3
|
+
description: Use when defining request DTOs, mindset module argument classes, or any class that must be validated and transformed by Wabot — including REST/socket inputs and tool function parameters. Covers the validator decorators (@isString, @isNumber, @isBoolean, @isDate, @isIn, @isNotEmpty, @isPresent, @isRecord, @isOptional, @isModel, @isArray, @min, @max), @description, validateAndTransform, and Mapper.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Validation, description, mapping
|
|
7
|
+
|
|
8
|
+
Wabot validates request shapes through decorator-driven metadata. The same metadata feeds:
|
|
9
|
+
|
|
10
|
+
- REST endpoint argument binding (`@onPost`, etc.).
|
|
11
|
+
- Socket event payloads (`@onSocketEvent`).
|
|
12
|
+
- Mindset tool function parameters (the LLM sees the validator type and the `@description`).
|
|
13
|
+
- `Async.runCommand` / `Async.scheduleCommand` payloads (validated against the `@command` class).
|
|
14
|
+
|
|
15
|
+
## Field validators
|
|
16
|
+
|
|
17
|
+
All decorators are exported from `@wabot-dev/framework`. Apply them to public class fields.
|
|
18
|
+
|
|
19
|
+
| Decorator | Effect |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `@isString()` | Value must be `string` |
|
|
22
|
+
| `@isNumber()` | Value must be `number` |
|
|
23
|
+
| `@isBoolean()` | Value must be `boolean` |
|
|
24
|
+
| `@isDate()` | Value must parse as `Date` (ISO 8601 input) |
|
|
25
|
+
| `@isIn([...values])` | Value must be one of the listed literals |
|
|
26
|
+
| `@isNotEmpty()` | String must be non-empty after trim |
|
|
27
|
+
| `@isPresent()` | Field must be present (not `undefined`) |
|
|
28
|
+
| `@isRecord(keyType, valueType)` | Plain object with typed keys/values; `keyType: 'number' \| 'string'`, `valueType: 'number' \| 'string' \| 'boolean'` |
|
|
29
|
+
| `@isOptional()` | Skip remaining validators when the value is null/undefined |
|
|
30
|
+
| `@isModel(OtherClass)` | Nested model validated against `OtherClass` |
|
|
31
|
+
| `@isArray({ minLength?, maxLength? })` | Field must be an array |
|
|
32
|
+
| `@min(n)` | Numeric / length minimum |
|
|
33
|
+
| `@max(n)` | Numeric / length maximum |
|
|
34
|
+
|
|
35
|
+
Validators chain top-to-bottom. Put `@isOptional()` **above** the type validator if the field is optional.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { description, isIn, isNotEmpty, isNumber, isOptional, isString, max, min } from '@wabot-dev/framework'
|
|
39
|
+
|
|
40
|
+
export class ListGamesRequest {
|
|
41
|
+
@isString()
|
|
42
|
+
@isIn(['backlog', 'playing', 'finished', 'abandoned'])
|
|
43
|
+
@description('Filter by status')
|
|
44
|
+
status: 'backlog' | 'playing' | 'finished' | 'abandoned' = 'backlog'
|
|
45
|
+
|
|
46
|
+
@isNumber()
|
|
47
|
+
@min(1)
|
|
48
|
+
@max(20)
|
|
49
|
+
@description('Maximum number of games to return')
|
|
50
|
+
limit: number = 5
|
|
51
|
+
|
|
52
|
+
@isOptional()
|
|
53
|
+
@isString()
|
|
54
|
+
cursor?: string
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The `default` value on the field becomes the value when the input is missing and the field is optional.
|
|
59
|
+
|
|
60
|
+
## `@description`
|
|
61
|
+
|
|
62
|
+
`@description('...')` annotates a field or method. Two roles:
|
|
63
|
+
|
|
64
|
+
1. On a request-class field: it becomes the LLM-facing parameter description for mindset tool functions.
|
|
65
|
+
2. On a `@mindsetModule()` method: it becomes the tool's description (without it the function is not exposed as a tool).
|
|
66
|
+
|
|
67
|
+
Mindset module functions **must** have a single request-object parameter (or no parameter), and every property of that request class must have both a type validator (e.g. `@isString()`) and a `@description(...)`. The framework throws at boot otherwise.
|
|
68
|
+
|
|
69
|
+
## `validateAndTransform`
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { validateAndTransform } from '@wabot-dev/framework'
|
|
73
|
+
|
|
74
|
+
const { value, error } = validateAndTransform(rawInput, ListGamesRequest)
|
|
75
|
+
if (error) {
|
|
76
|
+
// error.description + error.properties (per-field errors)
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Use this when you receive a payload outside a framework-managed boundary (e.g. a manually parsed file or queue message). Inside a REST/socket controller or a command handler the framework already validates the parameter for you.
|
|
81
|
+
|
|
82
|
+
## `Mapper`
|
|
83
|
+
|
|
84
|
+
`Mapper` is an injectable utility that combines deep-copy + `validateAndTransform`. It is useful when you need to convert a database row, a remote response, or a Storable into a typed model and you want a hard failure on shape mismatch (it throws `CustomError` with `httpCode: 500`).
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { container, Mapper } from '@wabot-dev/framework'
|
|
88
|
+
|
|
89
|
+
const mapper = container.resolve(Mapper)
|
|
90
|
+
const game: GameDto = mapper.map(rawRow, GameDto)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`Mapper.map(data, ctor)` will:
|
|
94
|
+
|
|
95
|
+
- Deep-copy `data` (Storables → plain objects, Dates → epoch ms).
|
|
96
|
+
- Run all validators on the target class.
|
|
97
|
+
- Return a typed instance, or throw.
|
|
98
|
+
|
|
99
|
+
## Rules
|
|
100
|
+
|
|
101
|
+
- Every public DTO/Request field needs at least one type validator. Without it, the framework can't introspect the type at runtime (TypeScript types are erased).
|
|
102
|
+
- Always pair `@description` with a type validator on mindset module request fields.
|
|
103
|
+
- Prefer field defaults over `@isOptional()` when you have a sensible default; reserve `@isOptional()` for fields that may genuinely be absent.
|
|
104
|
+
- Don't subclass `RestRequest` unless you want the raw Express `Request` injected into the endpoint — see `wabot-rest-socket`.
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# Async And Cron
|
|
2
|
-
|
|
3
|
-
Use this when scheduling work in the background or recurring jobs.
|
|
4
|
-
|
|
5
|
-
## Command and handler
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
import { command, commandHandler, type ICommandHandler } from '@wabot-dev/framework'
|
|
9
|
-
import { isString } from '@wabot-dev/framework'
|
|
10
|
-
|
|
11
|
-
@command('save-test-tag')
|
|
12
|
-
export class SaveTestTag {
|
|
13
|
-
@isString()
|
|
14
|
-
value!: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
@commandHandler(SaveTestTag)
|
|
18
|
-
export class SaveTestTagHandler implements ICommandHandler<SaveTestTag> {
|
|
19
|
-
constructor(private tags: TagRepository) {}
|
|
20
|
-
|
|
21
|
-
async handle(input: SaveTestTag) {
|
|
22
|
-
await this.tags.save(input.value)
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Run or schedule a command
|
|
28
|
-
|
|
29
|
-
```typescript
|
|
30
|
-
import { Async } from '@wabot-dev/framework'
|
|
31
|
-
|
|
32
|
-
const job = await async.runCommand(SaveTestTag, { value: 'hello' })
|
|
33
|
-
await async.scheduleCommand(SaveTestTag, { value: 'later' }, { minutes: 10 })
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Cron handler
|
|
37
|
-
|
|
38
|
-
```typescript
|
|
39
|
-
import { cron } from '@wabot-dev/framework'
|
|
40
|
-
|
|
41
|
-
@cron({ name: 'sync-leads', cron: '0 * * * *' })
|
|
42
|
-
export class SyncLeadsCron {
|
|
43
|
-
constructor(private leadService: LeadService) {}
|
|
44
|
-
|
|
45
|
-
async handle() {
|
|
46
|
-
await this.leadService.sync()
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Good practice
|
|
52
|
-
|
|
53
|
-
- Use commands for one-off background work.
|
|
54
|
-
- Use cron for repeated schedules.
|
|
55
|
-
- Keep command DTOs validated and small.
|
|
56
|
-
- Keep cron handlers idempotent because they can run many times.
|
|
57
|
-
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
# Controllers And Auth
|
|
2
|
-
|
|
3
|
-
Use this when exposing HTTP endpoints, socket events, or protected routes.
|
|
4
|
-
|
|
5
|
-
## REST controller
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
import { IncomingMessage } from 'http'
|
|
9
|
-
import { onPost, restController } from '@wabot-dev/framework'
|
|
10
|
-
|
|
11
|
-
class PaymentService {
|
|
12
|
-
processWebhook(rawBody: string) {
|
|
13
|
-
return { ok: true, rawBody }
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
@restController('/webhooks/bold')
|
|
18
|
-
export class BoldWebhookController {
|
|
19
|
-
constructor(private paymentService: PaymentService) {}
|
|
20
|
-
|
|
21
|
-
@onPost({
|
|
22
|
-
disableJsonParser: true,
|
|
23
|
-
disableUrlEncodedParser: true,
|
|
24
|
-
})
|
|
25
|
-
async receiveWebhook(request: IncomingMessage) {
|
|
26
|
-
const rawBody = await this.readRawBody(request)
|
|
27
|
-
return this.paymentService.processWebhook(rawBody)
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Socket controller
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
import { jwtHandshakeGuard, onSocketEvent, socketController } from '@wabot-dev/framework'
|
|
36
|
-
|
|
37
|
-
@jwtHandshakeGuard()
|
|
38
|
-
@socketController('/sales')
|
|
39
|
-
export class SalesSocketController {
|
|
40
|
-
@onSocketEvent('message')
|
|
41
|
-
async onMessage(payload: { text: string }) {
|
|
42
|
-
return { ok: true, received: payload.text }
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## JWT and API key auth
|
|
48
|
-
|
|
49
|
-
Use JWT when the client already has a signed token. Use API keys when the client should present a shared secret.
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
import {
|
|
53
|
-
apiKeyGuard,
|
|
54
|
-
apiKeyHandshakeGuard,
|
|
55
|
-
jwtGuard,
|
|
56
|
-
jwtHandshakeGuard,
|
|
57
|
-
onGet,
|
|
58
|
-
onPost,
|
|
59
|
-
restController,
|
|
60
|
-
socketController,
|
|
61
|
-
} from '@wabot-dev/framework'
|
|
62
|
-
|
|
63
|
-
class CreateLeadDto {
|
|
64
|
-
name!: string
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
class LeadService {
|
|
68
|
-
async create(body: CreateLeadDto) {
|
|
69
|
-
return { created: true, lead: body }
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
@restController('/api/leads')
|
|
74
|
-
export class LeadApiController {
|
|
75
|
-
constructor(private leadService: LeadService) {}
|
|
76
|
-
|
|
77
|
-
@apiKeyGuard()
|
|
78
|
-
@onGet()
|
|
79
|
-
async listLeads() {
|
|
80
|
-
return []
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
@jwtGuard()
|
|
84
|
-
@onPost()
|
|
85
|
-
async createLead(body: CreateLeadDto) {
|
|
86
|
-
return this.leadService.create(body)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
@socketController('/lead-room')
|
|
91
|
-
@jwtHandshakeGuard()
|
|
92
|
-
export class LeadRoomSocketController {}
|
|
93
|
-
|
|
94
|
-
@apiKeyHandshakeGuard()
|
|
95
|
-
@socketController('/api-room')
|
|
96
|
-
export class ApiRoomSocketController {}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Rule of thumb
|
|
100
|
-
|
|
101
|
-
- REST for request/response workflows, webhooks, and admin APIs.
|
|
102
|
-
- Socket for live events and long-lived sessions.
|
|
103
|
-
- JWT when the client already has a signed token.
|
|
104
|
-
- API key when the client should present a shared secret.
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# Core Patterns
|
|
2
|
-
|
|
3
|
-
Use this when deciding how to register and inject services.
|
|
4
|
-
|
|
5
|
-
## Lifecycles
|
|
6
|
-
|
|
7
|
-
| Decorator | Use for | Example |
|
|
8
|
-
| --- | --- | --- |
|
|
9
|
-
| `@singleton()` | Shared clients, repositories, config, caches | one DB repository for the app |
|
|
10
|
-
| `@injectable()` | Stateless services and modules | calculators, adapters, helpers |
|
|
11
|
-
| `@scoped(Lifecycle.ContainerScoped)` | Request/chat/socket-local state | auth context, request context |
|
|
12
|
-
|
|
13
|
-
## Singleton example
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
import { singleton } from '@wabot-dev/framework'
|
|
17
|
-
|
|
18
|
-
@singleton()
|
|
19
|
-
export class ProductRepository {
|
|
20
|
-
private products = new Map<string, string>()
|
|
21
|
-
|
|
22
|
-
save(id: string, name: string) {
|
|
23
|
-
this.products.set(id, name)
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Injectable example
|
|
29
|
-
|
|
30
|
-
```typescript
|
|
31
|
-
import { injectable } from '@wabot-dev/framework'
|
|
32
|
-
|
|
33
|
-
@injectable()
|
|
34
|
-
export class QuoteCalculator {
|
|
35
|
-
total(base: number, discount: number) {
|
|
36
|
-
return base - base * discount
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Scoped example
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
import { Lifecycle, scoped } from '@wabot-dev/framework'
|
|
45
|
-
|
|
46
|
-
@scoped(Lifecycle.ContainerScoped)
|
|
47
|
-
export class RequestContext {
|
|
48
|
-
private userId?: string
|
|
49
|
-
|
|
50
|
-
setUserId(userId: string) {
|
|
51
|
-
this.userId = userId
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
requireUserId() {
|
|
55
|
-
if (!this.userId) throw new Error('Unauthorized')
|
|
56
|
-
return this.userId
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Token injection
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
import { container, inject, injectable } from '@wabot-dev/framework'
|
|
65
|
-
|
|
66
|
-
container.register('SMTP_HOST', { useValue: 'smtp.example.com' })
|
|
67
|
-
container.register('SMTP_PORT', { useValue: 587 })
|
|
68
|
-
|
|
69
|
-
@injectable()
|
|
70
|
-
export class Mailer {
|
|
71
|
-
constructor(
|
|
72
|
-
@inject('SMTP_HOST') private smtpHost: string,
|
|
73
|
-
@inject('SMTP_PORT') private smtpPort: number,
|
|
74
|
-
) {}
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
## Rule of thumb
|
|
79
|
-
|
|
80
|
-
- Use singleton for shared infrastructure.
|
|
81
|
-
- Use injectable for pure business logic.
|
|
82
|
-
- Use scoped only when state must not leak across requests or chats.
|
|
83
|
-
|