@vyckr/tachyon 1.2.0 → 1.3.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
CHANGED
|
@@ -12,13 +12,14 @@ Tachyon is a **polyglot, file-system-routed full-stack framework for [Bun](https
|
|
|
12
12
|
- **Hot Module Replacement** — watches `routes/` and `components/` and reloads on change
|
|
13
13
|
- **Custom 404 page** — drop a `404.html` in your project root to override the default
|
|
14
14
|
- **Schema validation** — per-route request/response validation via `OPTIONS` files
|
|
15
|
+
- **Status code routing** — map response schemas to HTTP status codes; the framework picks the code automatically
|
|
15
16
|
- **Auth** — built-in Basic Auth and JWT decoding
|
|
16
17
|
- **Streaming** — SSE responses via `Accept: text/event-stream`
|
|
17
18
|
|
|
18
19
|
## Installation
|
|
19
20
|
|
|
20
21
|
```bash
|
|
21
|
-
|
|
22
|
+
bun add @vyckr/tachyon
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
## Quick Start
|
|
@@ -172,6 +173,28 @@ Place an `OPTIONS` file in any route directory to enable validation:
|
|
|
172
173
|
|
|
173
174
|
Nullable fields are suffixed with `?`. Set `VALIDATE=true` in your `.env` to enable.
|
|
174
175
|
|
|
176
|
+
### Status Code Routing
|
|
177
|
+
|
|
178
|
+
Instead of `res`/`err`, you can key response schemas by HTTP status code. Tachyon matches the handler's JSON output against each schema in ascending order — the first match determines the response status code.
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"POST": {
|
|
183
|
+
"req": { "name": "string" },
|
|
184
|
+
"201": { "id": "string", "name": "string" },
|
|
185
|
+
"400": { "detail": "string" },
|
|
186
|
+
"503": { "detail": "string", "retryAfter": 0 }
|
|
187
|
+
},
|
|
188
|
+
"DELETE": {
|
|
189
|
+
"204": {}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Handlers write their normal JSON to stdout — no changes required. The framework determines the status code from whichever schema the output matches. If no numeric schemas are defined, the default behaviour applies (stdout → 200, stderr → 500).
|
|
195
|
+
|
|
196
|
+
When `VALIDATE=true` is set, the matched schema is also used for strict validation.
|
|
197
|
+
|
|
175
198
|
## Front-end Pages (Yon)
|
|
176
199
|
|
|
177
200
|
Create an `HTML` file inside any route directory to define a front-end page:
|
package/package.json
CHANGED
|
@@ -170,15 +170,19 @@ export default class Tach {
|
|
|
170
170
|
|
|
171
171
|
const { body, status } = await Tach.getResponse([handler], stdin, context, config)
|
|
172
172
|
|
|
173
|
+
const matchedStatus = body ? Validate.matchStatusCode(handler, body) : null
|
|
174
|
+
const finalStatus = matchedStatus ?? status
|
|
175
|
+
|
|
173
176
|
if (process.env.VALIDATE !== undefined) {
|
|
177
|
+
const ioKey = matchedStatus ? String(matchedStatus) : (status === 200 ? "res" : "err")
|
|
174
178
|
try {
|
|
175
|
-
await Validate.validateData(handler,
|
|
179
|
+
await Validate.validateData(handler, ioKey, body!)
|
|
176
180
|
} catch (e) {
|
|
177
181
|
return Response.json({ detail: (e as Error).message }, { status: 422, headers: Router.headers })
|
|
178
182
|
}
|
|
179
183
|
}
|
|
180
184
|
|
|
181
|
-
return new Response(body, { status, headers: Router.headers })
|
|
185
|
+
return new Response(body, { status: finalStatus, headers: Router.headers })
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
/**
|
|
@@ -140,7 +140,8 @@ export default class Router {
|
|
|
140
140
|
const blob = await request.blob()
|
|
141
141
|
|
|
142
142
|
if (blob.size > 0) {
|
|
143
|
-
|
|
143
|
+
const contentType = request.headers.get('content-type') ?? ''
|
|
144
|
+
stdin.body = contentType.includes('json') ? await blob.json() : await blob.text()
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
const searchParams = new URL(request.url).searchParams
|
|
@@ -122,9 +122,52 @@ export default class Validate {
|
|
|
122
122
|
* populated during `validateRoutes()` — instead of re-reading OPTIONS
|
|
123
123
|
* files from disk.
|
|
124
124
|
*/
|
|
125
|
+
/**
|
|
126
|
+
* Tries to match `body` against each numeric status-code schema defined for
|
|
127
|
+
* the route/method in the OPTIONS file. Returns the first matching code, or
|
|
128
|
+
* `null` if no numeric schemas are defined or none match.
|
|
129
|
+
*/
|
|
130
|
+
static matchStatusCode(handler: string, body: string): number | null {
|
|
131
|
+
const parts = handler.split('/')
|
|
132
|
+
const method = parts.pop()!
|
|
133
|
+
const absoluteDir = parts.join('/')
|
|
134
|
+
const relativeRoute = absoluteDir.replace(Router.routesPath, '') || '/'
|
|
135
|
+
|
|
136
|
+
const schema = Router.routeConfigs[relativeRoute] as unknown as Record<string, Record<string, Record<string, unknown>>>
|
|
137
|
+
if (!schema) return null
|
|
138
|
+
|
|
139
|
+
const methodSchema = schema[method]
|
|
140
|
+
if (!methodSchema) return null
|
|
141
|
+
|
|
142
|
+
const statusCodes = Object.keys(methodSchema)
|
|
143
|
+
.filter(k => /^\d{3}$/.test(k))
|
|
144
|
+
.map(Number)
|
|
145
|
+
.sort((a, b) => a - b)
|
|
146
|
+
|
|
147
|
+
if (statusCodes.length === 0) return null
|
|
148
|
+
|
|
149
|
+
let parsed: Record<string, unknown>
|
|
150
|
+
try {
|
|
151
|
+
const p = JSON.parse(body)
|
|
152
|
+
if (typeof p !== 'object' || p === null || Array.isArray(p)) return null
|
|
153
|
+
parsed = p
|
|
154
|
+
} catch {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const code of statusCodes) {
|
|
159
|
+
try {
|
|
160
|
+
Validate.validateObject({ ...parsed }, methodSchema[String(code)], relativeRoute, relativeRoute)
|
|
161
|
+
return code
|
|
162
|
+
} catch {}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
125
168
|
static async validateData(
|
|
126
169
|
route: string,
|
|
127
|
-
io:
|
|
170
|
+
io: string,
|
|
128
171
|
data: Record<string, unknown> | string,
|
|
129
172
|
) {
|
|
130
173
|
const parts = route.split('/')
|