dynara 0.0.1 → 0.0.3
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 +19 -11
- package/dist/index.d.ts +15 -15
- package/dist/index.js +15 -11
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Dynara
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/dynara)
|
|
4
4
|
|
|
5
5
|
An extremely simple HTTP framework for Bun — practically a typed wrapper around `Bun.serve`, with Fastify-style routing and fast schema validation. Made for people switching over from Fastify.
|
|
6
6
|
|
|
@@ -11,15 +11,15 @@ An extremely simple HTTP framework for Bun — practically a typed wrapper aroun
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
bun add
|
|
14
|
+
bun add dynara
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## Quick start
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
|
-
import {
|
|
20
|
+
import { Router } from 'dynara'
|
|
21
21
|
|
|
22
|
-
const app = new
|
|
22
|
+
const app = new Router()
|
|
23
23
|
|
|
24
24
|
app.get('/', () => {
|
|
25
25
|
return { hello: 'world' }
|
|
@@ -59,8 +59,16 @@ A few conveniences:
|
|
|
59
59
|
|
|
60
60
|
- **Array params** are comma-split: `GET /items/1,2,3` with `{ itemIds: { type: 'array', items: 'number' } }` yields `[1, 2, 3]`.
|
|
61
61
|
- **Query booleans** accept `?flag=true` or a bare `?flag`.
|
|
62
|
+
- **Dates**: a `"date"` field accepts an ISO-8601 string (`"2020-01-02"`, optionally with a time part) or epoch milliseconds, and is decoded into a JS `Date` before your handler runs — `req.body.startsAt instanceof Date`. Works in bodies, query strings, and params; `"date?"` / `"date??"` behave like every other type.
|
|
62
63
|
- `req.raw` exposes the underlying `BunRequest`, and `req.server` the Bun `Server`.
|
|
63
64
|
|
|
65
|
+
```ts
|
|
66
|
+
const event = schema({ title: 'string', startsAt: 'date', endsAt: 'date??' })
|
|
67
|
+
app.post('/events', { body: event }, (req) => {
|
|
68
|
+
return { day: req.body.startsAt.getDay() } // startsAt is a Date
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
64
72
|
## Hooks
|
|
65
73
|
|
|
66
74
|
```ts
|
|
@@ -80,22 +88,22 @@ app.addHook('onListen', (server) => {
|
|
|
80
88
|
```ts
|
|
81
89
|
type Ctx = { user: { id: number } }
|
|
82
90
|
|
|
83
|
-
app.register((users:
|
|
91
|
+
app.register((users: Router<Ctx>) => {
|
|
84
92
|
users.addHook('onRequest', (req) => { req.user = { id: 1 } })
|
|
85
93
|
users.get('/me', (req) => req.user)
|
|
86
94
|
}, { prefix: '/users' })
|
|
87
95
|
```
|
|
88
96
|
|
|
89
|
-
For composable, reusable plugins there is the `
|
|
97
|
+
For composable, reusable plugins there is the `dynara()` builder. `use` adds plugins, `routes` adds handlers, and the result can be passed to `register`:
|
|
90
98
|
|
|
91
99
|
```ts
|
|
92
|
-
import {
|
|
100
|
+
import { dynara, Router } from 'dynara'
|
|
93
101
|
|
|
94
|
-
const useAuth = (app:
|
|
102
|
+
const useAuth = (app: Router<Ctx>) => {
|
|
95
103
|
app.addHook('onRequest', (req) => { req.user = { id: 1 } })
|
|
96
104
|
}
|
|
97
105
|
|
|
98
|
-
const users =
|
|
106
|
+
const users = dynara<Ctx>()
|
|
99
107
|
.use(useAuth)
|
|
100
108
|
.routes((app) => {
|
|
101
109
|
app.get('/me', (req) => req.user)
|
|
@@ -109,7 +117,7 @@ app.register(users, { prefix: '/users' })
|
|
|
109
117
|
Throw `HTTPError` to send an explicit status code; validation failures are turned into `400` responses automatically.
|
|
110
118
|
|
|
111
119
|
```ts
|
|
112
|
-
import { HTTPError } from '
|
|
120
|
+
import { HTTPError } from 'dynara'
|
|
113
121
|
|
|
114
122
|
app.get('/secret', () => {
|
|
115
123
|
throw new HTTPError('Forbidden', 403) // text body
|
package/dist/index.d.ts
CHANGED
|
@@ -15,21 +15,21 @@ type GetRouteOptions = {
|
|
|
15
15
|
params?: SchemaItem
|
|
16
16
|
query?: SchemaItem
|
|
17
17
|
};
|
|
18
|
-
interface
|
|
19
|
-
type
|
|
18
|
+
interface DynaraContext {}
|
|
19
|
+
type DynaraRequest<
|
|
20
20
|
R extends object = {},
|
|
21
21
|
T extends RouteOptions = {}
|
|
22
|
-
> =
|
|
22
|
+
> = DynaraContext & R & {
|
|
23
23
|
params: T["params"] extends object ? SchemaType<T["params"]> : unknown
|
|
24
24
|
query: T["query"] extends object ? SchemaType<T["query"]> : unknown
|
|
25
25
|
body: T["body"] extends object ? SchemaType<T["body"]> : unknown
|
|
26
26
|
raw: BunRequest
|
|
27
27
|
server: Server
|
|
28
28
|
};
|
|
29
|
-
type
|
|
29
|
+
type GetDynaraRequest<
|
|
30
30
|
R extends object = {},
|
|
31
31
|
T extends GetRouteOptions = {}
|
|
32
|
-
> =
|
|
32
|
+
> = DynaraContext & R & {
|
|
33
33
|
params: T["params"] extends object ? SchemaType<T["params"]> : unknown
|
|
34
34
|
query: T["query"] extends object ? SchemaType<T["query"]> : unknown
|
|
35
35
|
raw: BunRequest
|
|
@@ -38,11 +38,11 @@ type GetMarciRequest<
|
|
|
38
38
|
type RouteAction<
|
|
39
39
|
T extends RouteOptions,
|
|
40
40
|
R extends object = {}
|
|
41
|
-
> = (req:
|
|
41
|
+
> = (req: DynaraRequest<R, T>) => (any | Promise<any>);
|
|
42
42
|
type GetRouteAction<
|
|
43
43
|
T extends GetRouteOptions,
|
|
44
44
|
R extends object = {}
|
|
45
|
-
> = (req:
|
|
45
|
+
> = (req: GetDynaraRequest<R, T>) => (any | Promise<any>);
|
|
46
46
|
type RegisterPluginOptions = {
|
|
47
47
|
prefix?: string
|
|
48
48
|
};
|
|
@@ -67,7 +67,7 @@ type PostOptionsFromSchemaList<T extends readonly SchemaItem2[]> = T extends [Sc
|
|
|
67
67
|
params: T[0]
|
|
68
68
|
body: T[1]
|
|
69
69
|
} : {};
|
|
70
|
-
declare class
|
|
70
|
+
declare class Router<R extends object = {}> {
|
|
71
71
|
private routes;
|
|
72
72
|
private promises;
|
|
73
73
|
private prefix;
|
|
@@ -77,7 +77,7 @@ declare class MarciApp<R extends object = {}> {
|
|
|
77
77
|
private onRequestHooks;
|
|
78
78
|
private add;
|
|
79
79
|
addHook(where: "onListen", callback: (server: Bun.Server2) => void): void;
|
|
80
|
-
addHook(where: "onRequest", callback: (ctx:
|
|
80
|
+
addHook(where: "onRequest", callback: (ctx: DynaraRequest<R>) => void): void;
|
|
81
81
|
get(path: string, callback: GetRouteAction<{}, R>): void;
|
|
82
82
|
get<T extends GetRouteOptions>(path: string, options: T, callback: GetRouteAction<T, R>): void;
|
|
83
83
|
get<T extends readonly SchemaItem3[]>(path: string, schemas: [...T], callback: GetRouteAction<GetOptionsFromSchemaList<T>, R>): void;
|
|
@@ -93,7 +93,7 @@ declare class MarciApp<R extends object = {}> {
|
|
|
93
93
|
delete(path: string, callback: RouteAction<{}, R>): void;
|
|
94
94
|
delete<T extends RouteOptions>(path: string, options: T, callback: RouteAction<T, R>): void;
|
|
95
95
|
delete<T extends readonly SchemaItem3[]>(path: string, schemas: [...T], callback: RouteAction<PostOptionsFromSchemaList<T>, R>): void;
|
|
96
|
-
register(plugin: (app:
|
|
96
|
+
register(plugin: (app: Router<any>) => void | Promise<void>, options?: RegisterPluginOptions): void;
|
|
97
97
|
private websocket?;
|
|
98
98
|
private websocketPath?;
|
|
99
99
|
private websocketFetch?;
|
|
@@ -108,12 +108,12 @@ declare class MarciApp<R extends object = {}> {
|
|
|
108
108
|
/**\\n\\t\\n\\t* Dispatches a request through the app's routes in-process — no server, no\\n\\t\\n\\t* socket. Matches the route the same way Bun would, then runs the onRequest\\n\\t\\n\\t* hooks, validation, handler, and error mapping, returning the Response.\\n\\t\\n\\t* Intended for integration testing.\\n\\t\\n\\t*\\n\\t\\n\\t* @example\\n\\t\\n\\t* const res = await app.inject("/users/1")\\n\\t\\n\\t* const res = await app.inject({ method: "POST", url: "/users", body: { name: "Alice" } })\\n\\t\\n\\t* expect(res.status).toBe(200)\\n\\t\\n\\t* expect(await res.json()).toEqual({ ... })\\n\\t\\n\\t*/
|
|
109
109
|
inject(options: string | InjectOptions): Promise<Response>;
|
|
110
110
|
}
|
|
111
|
-
type
|
|
111
|
+
type Plugin<R extends object = {}> = ((app: Router<any>) => Promise<void>) & {
|
|
112
112
|
use<
|
|
113
113
|
S extends object = {},
|
|
114
114
|
A extends any[] = []
|
|
115
|
-
>(plugin: (app:
|
|
116
|
-
routes(app: (app:
|
|
115
|
+
>(plugin: (app: Router<R & S>, ...args: A) => Promise<void> | void, ...args: A): Plugin<R & S>
|
|
116
|
+
routes(app: (app: Router<R>) => Promise<void> | void): Plugin<R>
|
|
117
117
|
};
|
|
118
|
-
declare const
|
|
119
|
-
export {
|
|
118
|
+
declare const dynara: <R extends object = {}>() => Plugin<R>;
|
|
119
|
+
export { dynara, Router, Plugin, InjectOptions, HTTPError, DynaraRequest };
|
package/dist/index.js
CHANGED
|
@@ -28,8 +28,8 @@ class ValidationError extends Error {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// src/
|
|
32
|
-
var
|
|
31
|
+
// src/plugin.ts
|
|
32
|
+
var dynara = () => {
|
|
33
33
|
const plugins = [];
|
|
34
34
|
const handlers = [];
|
|
35
35
|
const app = Object.assign(async (app2) => {
|
|
@@ -51,7 +51,7 @@ var marci = () => {
|
|
|
51
51
|
});
|
|
52
52
|
return app;
|
|
53
53
|
};
|
|
54
|
-
// src/
|
|
54
|
+
// src/Router.ts
|
|
55
55
|
import { unfoldTypeBoxSchema } from "compact-json-schema";
|
|
56
56
|
import { TypeBoxError } from "@sinclair/typebox";
|
|
57
57
|
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
|
@@ -59,7 +59,7 @@ import { TypeCompiler } from "@sinclair/typebox/compiler";
|
|
|
59
59
|
// src/request.ts
|
|
60
60
|
import { Value } from "@sinclair/typebox/value";
|
|
61
61
|
|
|
62
|
-
class
|
|
62
|
+
class DynaraRequestInternal {
|
|
63
63
|
server;
|
|
64
64
|
raw;
|
|
65
65
|
params;
|
|
@@ -131,6 +131,8 @@ var getValidationError = (obj, step) => {
|
|
|
131
131
|
return `{"cause":"Validation error","fields":{"${obj.path.slice(1)}":{"message":"${obj.message}"}}}`;
|
|
132
132
|
}
|
|
133
133
|
};
|
|
134
|
+
var DATE_PATTERN = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d{1,6})?)?(Z|[+-]\d{2}:\d{2})?)?$/;
|
|
135
|
+
var dateType = (options) => Type.Transform(Type.Union([Type.String({ pattern: DATE_PATTERN.source }), Type.Number()], options)).Decode((value) => new Date(value)).Encode((value) => value instanceof Date ? value.toISOString() : value);
|
|
134
136
|
provideTypeBoxMap({
|
|
135
137
|
string: Type.String,
|
|
136
138
|
boolean: Type.Boolean,
|
|
@@ -139,6 +141,7 @@ provideTypeBoxMap({
|
|
|
139
141
|
object: Type.Object,
|
|
140
142
|
array: Type.Array,
|
|
141
143
|
bigint: Type.BigInt,
|
|
144
|
+
date: dateType,
|
|
142
145
|
union: Type.Union,
|
|
143
146
|
null: Type.Null,
|
|
144
147
|
literal: Type.Literal,
|
|
@@ -165,8 +168,9 @@ var parseBody = (schema, req) => {
|
|
|
165
168
|
const error = schema.Errors(resp).First();
|
|
166
169
|
const err = new ValidationError(error, "body");
|
|
167
170
|
rej(err);
|
|
171
|
+
return;
|
|
168
172
|
}
|
|
169
|
-
res(resp);
|
|
173
|
+
res(schema.Decode(resp));
|
|
170
174
|
});
|
|
171
175
|
});
|
|
172
176
|
};
|
|
@@ -220,8 +224,8 @@ var matchRoute = (routes, method, pathname) => {
|
|
|
220
224
|
return best;
|
|
221
225
|
};
|
|
222
226
|
|
|
223
|
-
// src/
|
|
224
|
-
class
|
|
227
|
+
// src/Router.ts
|
|
228
|
+
class Router {
|
|
225
229
|
routes = {};
|
|
226
230
|
promises = [];
|
|
227
231
|
prefix = "";
|
|
@@ -245,7 +249,7 @@ class MarciApp {
|
|
|
245
249
|
}
|
|
246
250
|
}
|
|
247
251
|
this.routes[fullPath][method] = async (req) => {
|
|
248
|
-
const request = new
|
|
252
|
+
const request = new DynaraRequestInternal(req, this.root?.server ?? this.server, paramsSchema, querySchema);
|
|
249
253
|
for (const callback2 of this.onRequestHooks) {
|
|
250
254
|
await callback2(request);
|
|
251
255
|
}
|
|
@@ -304,7 +308,7 @@ class MarciApp {
|
|
|
304
308
|
}
|
|
305
309
|
}
|
|
306
310
|
register(plugin, options = {}) {
|
|
307
|
-
const app = new
|
|
311
|
+
const app = new Router;
|
|
308
312
|
app.root = this.root ?? this;
|
|
309
313
|
app.routes = this.routes;
|
|
310
314
|
app.onListenHooks = this.onListenHooks;
|
|
@@ -420,7 +424,7 @@ class MarciApp {
|
|
|
420
424
|
}
|
|
421
425
|
}
|
|
422
426
|
export {
|
|
423
|
-
|
|
424
|
-
|
|
427
|
+
dynara,
|
|
428
|
+
Router,
|
|
425
429
|
HTTPError
|
|
426
430
|
};
|
package/package.json
CHANGED
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"license": "MIT",
|
|
14
|
-
"version": "0.0.
|
|
14
|
+
"version": "0.0.3",
|
|
15
15
|
"description": "Simple HTTP framework powered by Bun",
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|
|
18
|
-
"url": "https://github.com/den59k/
|
|
19
|
-
"directory": "packages/
|
|
18
|
+
"url": "https://github.com/den59k/dynara.git",
|
|
19
|
+
"directory": "packages/dynara"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "bunx bunup src/index.ts --format esm",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@sinclair/typebox": "^0.34.37",
|
|
36
|
-
"compact-json-schema": "^0.1.
|
|
36
|
+
"compact-json-schema": "^0.1.6"
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
39
|
"dist"
|