asterflow 0.0.6 → 2.0.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 +43 -87
- package/dist/cjs/index.cjs +60 -45
- package/dist/mjs/index.js +51 -36
- package/dist/types/controllers/Asterflow.d.ts +33 -17
- package/dist/types/types/paths.d.ts +2 -8
- package/dist/types/types/reminist.d.ts +13 -14
- package/dist/types/types/routes.d.ts +5 -3
- package/package.json +16 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# AsterFlow
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|

|
|
@@ -10,122 +10,78 @@
|
|
|
10
10
|
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
|
-
> The
|
|
13
|
+
> The core framework - ties together adapters, routing, plugins and responses into one typed `AsterFlow` app.
|
|
14
14
|
|
|
15
15
|
## 📦 Installation
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm install asterflow
|
|
19
|
-
# or
|
|
20
18
|
bun install asterflow
|
|
21
19
|
```
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
### ✨ Features
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
- **`new AsterFlow(options)`** - creates an app around a `driver` (an `@asterflow/adapter` instance, defaults to `adapters.node`)
|
|
24
|
+
- **`.method(...)` / `.router(...)`** - define a single-verb route or a multi-verb route group directly on the app, backed by `@asterflow/router`'s `Method`/`Router`
|
|
25
|
+
- **`.controller(route)`** - registers an already-built `Method` or `Router` instance
|
|
26
|
+
- **`.middleware({ basePath, controllers })`** - registers a group of controllers under a shared path prefix
|
|
27
|
+
- **Route `use` middlewares run first** - a route's middleware chain runs before schema validation, and any middleware can return a response to short-circuit the request before the body is even parsed
|
|
28
|
+
- **Schema validation** - if the route has a schema, the parsed body is validated after middlewares pass and before the handler runs
|
|
29
|
+
- **Plugin system** - `.use(plugin, config)` registers an `@asterflow/plugin` instance, applies its instance extensions, and wires up its `beforeInitialize`/`afterInitialize`/`onRequest`/`onResponse` hooks
|
|
30
|
+
- **Merged plugin context** - every plugin's context is resolved once, when `.listen()` is called, and handed to every handler as `context.plugins` (not recomputed per request)
|
|
31
|
+
- **Trie-based route matching** - routes are stored and matched with `reminist`, keyed by HTTP method
|
|
32
|
+
- **Full type inference** - registering a route narrows the app's type so its path, params, schema and middleware context are known at every call site
|
|
26
33
|
|
|
27
|
-
##
|
|
34
|
+
## ❓ How to Use
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
- **Routing System:** Typed routing with support for dynamic parameters
|
|
31
|
-
- **Middleware:** Flexible middleware system with typed context
|
|
32
|
-
- **Type Safety:** Full TypeScript support with type inference
|
|
33
|
-
- **High Performance:** Optimized routing system using prefix tree (trie)
|
|
34
|
-
- **URL Analysis:** Integrated URL parser with support for dynamic parameters
|
|
36
|
+
Build routes with `.method()`/`.router()` and start the server with `.listen()`. This route validates its body with a middleware-provided context before running:
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
### Basic Setup
|
|
39
|
-
|
|
40
|
-
```typescript
|
|
38
|
+
```ts
|
|
41
39
|
import { AsterFlow } from 'asterflow'
|
|
42
|
-
import {
|
|
43
|
-
import
|
|
44
|
-
|
|
45
|
-
const server = fastify()
|
|
46
|
-
const aster = new AsterFlow({
|
|
47
|
-
driver: adapters.fastify
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
aster.listen(server, { port: 3000 })
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Defining Routes
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
import { Router } from '@asterflow/router'
|
|
57
|
-
|
|
58
|
-
const router = new Router({
|
|
59
|
-
path: '/:id=number?query#fragment',
|
|
60
|
-
methods: {
|
|
61
|
-
get({ response, url }) {
|
|
62
|
-
const params = url.getParams() // params.id is typed as number
|
|
63
|
-
const query = url.getSearchParams()
|
|
64
|
-
return response.send('Hello World')
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
aster.controller(router)
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### Using Middleware
|
|
73
|
-
|
|
74
|
-
```typescript
|
|
75
|
-
import { Middleware, Router } from '@asterflow/router'
|
|
40
|
+
import { Middleware } from '@asterflow/router'
|
|
41
|
+
import { z } from 'zod'
|
|
76
42
|
|
|
77
43
|
const auth = new Middleware({
|
|
78
44
|
name: 'auth',
|
|
79
|
-
onRun({ next }) {
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
})
|
|
45
|
+
onRun({ request, response, next }) {
|
|
46
|
+
if (!request.getHeaders().authorization) return response.unauthorized({ message: 'Missing token' })
|
|
47
|
+
return next({ userId: 42 })
|
|
83
48
|
}
|
|
84
49
|
})
|
|
85
50
|
|
|
86
|
-
const
|
|
87
|
-
|
|
51
|
+
const app = new AsterFlow() // defaults to the Node adapter
|
|
52
|
+
|
|
53
|
+
app.method('post', {
|
|
54
|
+
path: '/users',
|
|
88
55
|
use: [auth],
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return response.unauthorized({
|
|
93
|
-
message: 'Unauthorized'
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
return response.send('Protected area')
|
|
97
|
-
}
|
|
56
|
+
schema: z.object({ name: z.string() }),
|
|
57
|
+
handler({ schema, middleware, response }) {
|
|
58
|
+
return response.created({ id: middleware.userId, name: schema.name })
|
|
98
59
|
}
|
|
99
60
|
})
|
|
61
|
+
|
|
62
|
+
app.listen({ port: 3000 })
|
|
100
63
|
```
|
|
101
64
|
|
|
102
|
-
|
|
65
|
+
Plugins register onto the same instance with `.use()` and can add their own instance methods:
|
|
103
66
|
|
|
104
|
-
```
|
|
105
|
-
import {
|
|
67
|
+
```ts
|
|
68
|
+
import { AsterFlow } from 'asterflow'
|
|
69
|
+
import { fsRoutingPlugin } from '@asterflow/fs'
|
|
106
70
|
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
method: 'get',
|
|
110
|
-
handler: ({ response, url }) => {
|
|
111
|
-
const { id } = url.getParams() // id is typed as number
|
|
112
|
-
return response.send({ id })
|
|
113
|
-
}
|
|
114
|
-
})
|
|
71
|
+
const app = new AsterFlow()
|
|
72
|
+
.use(fsRoutingPlugin, { routes: [] }) // routes: AnyRouter[]
|
|
115
73
|
|
|
116
|
-
|
|
74
|
+
app.listen({ port: 3000 })
|
|
117
75
|
```
|
|
118
76
|
|
|
119
77
|
## 🔗 Related Packages
|
|
120
78
|
|
|
121
|
-
- [@asterflow/adapter](https://www.npmjs.com/package/@asterflow/adapter) -
|
|
122
|
-
- [@asterflow/router](https://www.npmjs.com/package/@asterflow/router) -
|
|
123
|
-
- [@asterflow/
|
|
124
|
-
- [@asterflow/response](https://www.npmjs.com/package/@asterflow/response) -
|
|
125
|
-
- [
|
|
126
|
-
- [@asterflow/url-parser](https://www.npmjs.com/package/@asterflow/url-parser) - High-performance typed URL parser with automatic type casting
|
|
127
|
-
- [@asterflow/plugin](https://www.npmjs.com/package/@asterflow/plugin) - A modular and typed plugin system
|
|
79
|
+
- [@asterflow/adapter](https://www.npmjs.com/package/@asterflow/adapter) - supplies the `driver` (Bun, Node, Express, Fastify) that `.listen()` delegates to
|
|
80
|
+
- [@asterflow/router](https://www.npmjs.com/package/@asterflow/router) - `Method`/`Router`/`Middleware` classes that back `.method()`, `.router()` and `.controller()`
|
|
81
|
+
- [@asterflow/plugin](https://www.npmjs.com/package/@asterflow/plugin) - plugin instances and types consumed by `.use()`
|
|
82
|
+
- [@asterflow/response](https://www.npmjs.com/package/@asterflow/response) - `AsterResponse` is what every request handler works with and returns
|
|
83
|
+
- [@asterflow/request](https://www.npmjs.com/package/@asterflow/request) - supplies the `Request` type passed into every route and middleware handler
|
|
128
84
|
|
|
129
85
|
## 📄 License
|
|
130
86
|
|
|
131
|
-
|
|
87
|
+
This project is licensed under the [MIT License](../LICENSE).
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -1,46 +1,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var y = Object.defineProperty;
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
var
|
|
6
|
-
var
|
|
3
|
+
var w = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var A = Object.getOwnPropertyNames;
|
|
5
|
+
var P = Object.prototype.hasOwnProperty;
|
|
6
|
+
var M = (l, e) => {
|
|
7
7
|
for (var t in e)
|
|
8
|
-
y(
|
|
9
|
-
},
|
|
8
|
+
y(l, t, { get: e[t], enumerable: !0 });
|
|
9
|
+
}, C = (l, e, t, s) => {
|
|
10
10
|
if (e && typeof e == "object" || typeof e == "function")
|
|
11
|
-
for (let n of
|
|
12
|
-
!
|
|
13
|
-
return
|
|
11
|
+
for (let n of A(e))
|
|
12
|
+
!P.call(l, n) && n !== t && y(l, n, { get: () => e[n], enumerable: !(s = w(e, n)) || s.enumerable });
|
|
13
|
+
return l;
|
|
14
14
|
};
|
|
15
|
-
var
|
|
15
|
+
var v = (l) => C(y({}, "__esModule", { value: !0 }), l);
|
|
16
16
|
// core/src/index.ts
|
|
17
|
-
var
|
|
18
|
-
|
|
19
|
-
AsterFlow: () =>
|
|
20
|
-
AsterFlowInstance: () =>
|
|
17
|
+
var D = {};
|
|
18
|
+
M(D, {
|
|
19
|
+
AsterFlow: () => I,
|
|
20
|
+
AsterFlowInstance: () => h
|
|
21
21
|
});
|
|
22
|
-
module.exports =
|
|
22
|
+
module.exports = v(D);
|
|
23
23
|
// core/src/controllers/Asterflow.ts
|
|
24
|
-
var
|
|
24
|
+
var x = require("@asterflow/adapter"), g = require("@asterflow/response"), R = require("@asterflow/router"), u = require("@asterflow/url-parser"), f = require("reminist");
|
|
25
25
|
// core/src/utils/parser.ts
|
|
26
|
-
function
|
|
27
|
-
return `${
|
|
26
|
+
function m(l, e) {
|
|
27
|
+
return `${l}${e}`.replace(/\/{2,}/g, "/");
|
|
28
28
|
}
|
|
29
29
|
// core/src/controllers/Asterflow.ts
|
|
30
|
-
var
|
|
30
|
+
var h = class {
|
|
31
31
|
driver;
|
|
32
|
-
reminist = new
|
|
32
|
+
reminist = new f.Reminist({ keys: Object.keys(R.MethodType) });
|
|
33
33
|
middlewares = [];
|
|
34
34
|
plugins = {};
|
|
35
35
|
onRequestPlugins = [];
|
|
36
36
|
onResponsePlugins = [];
|
|
37
37
|
beforeInitializePlugins = [];
|
|
38
38
|
afterInitializePlugins = [];
|
|
39
|
+
pluginContext = {};
|
|
39
40
|
constructor(e) {
|
|
40
|
-
this.driver = e?.driver ??
|
|
41
|
+
this.driver = e?.driver ?? x.adapters.node, this.driver.onRequest = this.handleRequest.bind(this);
|
|
41
42
|
}
|
|
42
43
|
async handleRequest(e, t) {
|
|
43
|
-
t = t ?? new
|
|
44
|
+
t = t ?? new g.AsterResponse();
|
|
44
45
|
let s = () => t.notFound({
|
|
45
46
|
statusCode: 404,
|
|
46
47
|
code: "NOT_FOUND",
|
|
@@ -51,26 +52,26 @@ var c = class {
|
|
|
51
52
|
if (!r?.node?.store) return s();
|
|
52
53
|
let i = r.node.store, a = await this.runHooks("onRequest", this, i, e, t);
|
|
53
54
|
if (a) return a;
|
|
54
|
-
(i.url.ast.expressions.has(
|
|
55
|
+
(i.url.ast.expressions.has(u.InternalExpression.Variable) || i.url.ast.expressions.has(u.InternalExpression.Dynamic) || i.url.ast.expressions.has(u.InternalExpression.DynamicCatchAll) || i.url.ast.expressions.has(u.InternalExpression.DynamicOptionalCatchAll) || i.url.ast.expressions.has(u.InternalExpression.Wildcard)) && (e.url = e.url.setParser(i.url));
|
|
55
56
|
try {
|
|
56
57
|
return await this.runHandler(i, e, t), await this.runHooks("onResponse", this, i, e, t), t;
|
|
57
58
|
} catch (o) {
|
|
58
59
|
return console.log(o), t.badRequest({
|
|
59
60
|
statusCode: 400,
|
|
60
|
-
message: o instanceof
|
|
61
|
-
error: o instanceof
|
|
61
|
+
message: o instanceof u.ErrorLog ? "AST_ERROR" : "ERROR",
|
|
62
|
+
error: o instanceof u.ErrorLog || o instanceof Error ? o.message : o
|
|
62
63
|
});
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
middleware(e) {
|
|
66
67
|
for (let t of e.controllers) {
|
|
67
|
-
let s =
|
|
68
|
+
let s = m(e.basePath, t.path);
|
|
68
69
|
this.addEntry(t, s);
|
|
69
70
|
}
|
|
70
71
|
return this;
|
|
71
72
|
}
|
|
72
73
|
controller(e) {
|
|
73
|
-
let t =
|
|
74
|
+
let t = m("/", e.path);
|
|
74
75
|
return this.addEntry(e, t), this;
|
|
75
76
|
}
|
|
76
77
|
use(e, t) {
|
|
@@ -82,10 +83,10 @@ var c = class {
|
|
|
82
83
|
return this;
|
|
83
84
|
}
|
|
84
85
|
router(e) {
|
|
85
|
-
return this.controller(new
|
|
86
|
+
return this.controller(new R.Router(e)), this;
|
|
86
87
|
}
|
|
87
|
-
method(e) {
|
|
88
|
-
return this.controller(new
|
|
88
|
+
method(e, t) {
|
|
89
|
+
return this.controller(new R.Method(e, t)), this;
|
|
89
90
|
}
|
|
90
91
|
async resolvePluginContexts() {
|
|
91
92
|
for (let e in this.plugins) {
|
|
@@ -96,12 +97,13 @@ var c = class {
|
|
|
96
97
|
Object.assign(t.context, n);
|
|
97
98
|
}
|
|
98
99
|
}
|
|
100
|
+
this.pluginContext = Object.values(this.plugins).reduce((e, t) => ({ ...e, ...t.context }), {});
|
|
99
101
|
}
|
|
100
102
|
async listen(...e) {
|
|
101
103
|
await this.resolvePluginContexts(), await this.runHooks("beforeInitialize", this), await this.driver.listen(...e), await this.runHooks("afterInitialize", this);
|
|
102
104
|
}
|
|
103
105
|
addEntry(e, t) {
|
|
104
|
-
let s = e instanceof
|
|
106
|
+
let s = e instanceof R.Method ? [e.method] : Object.keys(e.methods), n = { path: t, route: e, methods: s, url: new u.Analyze(t) };
|
|
105
107
|
for (let r of s)
|
|
106
108
|
this.reminist.add(r, t, n);
|
|
107
109
|
}
|
|
@@ -124,9 +126,9 @@ var c = class {
|
|
|
124
126
|
if (!o) break;
|
|
125
127
|
if (!n || !r || !s) return;
|
|
126
128
|
for (let d of o) {
|
|
127
|
-
let
|
|
128
|
-
if (
|
|
129
|
-
return
|
|
129
|
+
let c = await d({ instance: t, router: s, request: n, response: r, plugin: a });
|
|
130
|
+
if (c && typeof c == "object" && c.constructor?.name === "AsterResponse")
|
|
131
|
+
return c;
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
134
|
break;
|
|
@@ -141,31 +143,44 @@ var c = class {
|
|
|
141
143
|
break;
|
|
142
144
|
}
|
|
143
145
|
}
|
|
146
|
+
async runMiddlewares(e, t, s, n) {
|
|
147
|
+
let r = {};
|
|
148
|
+
if (!e || e.length === 0) return { context: r };
|
|
149
|
+
let a = { request: t, response: s, schema: n, next: (o) => o };
|
|
150
|
+
for (let o of e) {
|
|
151
|
+
let d = await o.onRun(a);
|
|
152
|
+
if (d instanceof g.AsterResponse) return { context: r, response: d };
|
|
153
|
+
d && typeof d == "object" && Object.assign(r, d);
|
|
154
|
+
}
|
|
155
|
+
return { context: r };
|
|
156
|
+
}
|
|
144
157
|
async runHandler({ route: e }, t, s) {
|
|
145
|
-
let n = t.getMethod().toLowerCase(), r = e instanceof
|
|
158
|
+
let n = t.getMethod().toLowerCase(), r = e instanceof R.Method ? e.handler : e.methods[n], i = e instanceof R.Method ? e.schema : e.schema?.[n];
|
|
146
159
|
if (!r) return null;
|
|
160
|
+
let a = await t.getBody(), { context: o, response: d } = await this.runMiddlewares(e.use, t, s, a);
|
|
161
|
+
if (d) return d;
|
|
147
162
|
if (i) {
|
|
148
|
-
let
|
|
149
|
-
if (!
|
|
163
|
+
let p = i.safeParse(a);
|
|
164
|
+
if (!p.success)
|
|
150
165
|
return s.validationError({
|
|
151
166
|
statusCode: 422,
|
|
152
167
|
message: "VALIDATION_ERROR",
|
|
153
|
-
error: JSON.parse(
|
|
168
|
+
error: JSON.parse(p.error)
|
|
154
169
|
});
|
|
155
|
-
s.send(
|
|
170
|
+
s.send(p.data);
|
|
156
171
|
}
|
|
157
|
-
let
|
|
172
|
+
let c = {
|
|
158
173
|
instance: this,
|
|
159
174
|
request: t,
|
|
160
175
|
response: s,
|
|
161
176
|
url: t.url,
|
|
162
|
-
schema:
|
|
163
|
-
middleware:
|
|
164
|
-
plugins:
|
|
177
|
+
schema: a,
|
|
178
|
+
middleware: o,
|
|
179
|
+
plugins: this.pluginContext
|
|
165
180
|
};
|
|
166
|
-
return r(
|
|
181
|
+
return r(c);
|
|
167
182
|
}
|
|
168
|
-
},
|
|
183
|
+
}, I = h;
|
|
169
184
|
0 && (module.exports = {
|
|
170
185
|
AsterFlow,
|
|
171
186
|
AsterFlowInstance
|
package/dist/mjs/index.js
CHANGED
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
// core/src/controllers/Asterflow.ts
|
|
2
|
-
import { adapters as
|
|
3
|
-
import { AsterResponse as
|
|
2
|
+
import { adapters as x } from "@asterflow/adapter";
|
|
3
|
+
import { AsterResponse as m } from "@asterflow/response";
|
|
4
4
|
import {
|
|
5
|
-
Method as
|
|
6
|
-
MethodType as
|
|
7
|
-
Router as
|
|
5
|
+
Method as R,
|
|
6
|
+
MethodType as f,
|
|
7
|
+
Router as w
|
|
8
8
|
} from "@asterflow/router";
|
|
9
|
-
import { Analyze as
|
|
10
|
-
import { Reminist as
|
|
9
|
+
import { Analyze as A, ErrorLog as g, InternalExpression as l } from "@asterflow/url-parser";
|
|
10
|
+
import { Reminist as P } from "reminist";
|
|
11
11
|
// core/src/utils/parser.ts
|
|
12
|
-
function
|
|
13
|
-
return `${
|
|
12
|
+
function h(y, e) {
|
|
13
|
+
return `${y}${e}`.replace(/\/{2,}/g, "/");
|
|
14
14
|
}
|
|
15
15
|
// core/src/controllers/Asterflow.ts
|
|
16
|
-
var
|
|
16
|
+
var p = class {
|
|
17
17
|
driver;
|
|
18
|
-
reminist = new
|
|
18
|
+
reminist = new P({ keys: Object.keys(f) });
|
|
19
19
|
middlewares = [];
|
|
20
20
|
plugins = {};
|
|
21
21
|
onRequestPlugins = [];
|
|
22
22
|
onResponsePlugins = [];
|
|
23
23
|
beforeInitializePlugins = [];
|
|
24
24
|
afterInitializePlugins = [];
|
|
25
|
+
pluginContext = {};
|
|
25
26
|
constructor(e) {
|
|
26
|
-
this.driver = e?.driver ??
|
|
27
|
+
this.driver = e?.driver ?? x.node, this.driver.onRequest = this.handleRequest.bind(this);
|
|
27
28
|
}
|
|
28
29
|
async handleRequest(e, t) {
|
|
29
|
-
t = t ?? new
|
|
30
|
+
t = t ?? new m();
|
|
30
31
|
let s = () => t.notFound({
|
|
31
32
|
statusCode: 404,
|
|
32
33
|
code: "NOT_FOUND",
|
|
@@ -37,26 +38,26 @@ var h = class {
|
|
|
37
38
|
if (!r?.node?.store) return s();
|
|
38
39
|
let i = r.node.store, a = await this.runHooks("onRequest", this, i, e, t);
|
|
39
40
|
if (a) return a;
|
|
40
|
-
(i.url.ast.expressions.has(
|
|
41
|
+
(i.url.ast.expressions.has(l.Variable) || i.url.ast.expressions.has(l.Dynamic) || i.url.ast.expressions.has(l.DynamicCatchAll) || i.url.ast.expressions.has(l.DynamicOptionalCatchAll) || i.url.ast.expressions.has(l.Wildcard)) && (e.url = e.url.setParser(i.url));
|
|
41
42
|
try {
|
|
42
43
|
return await this.runHandler(i, e, t), await this.runHooks("onResponse", this, i, e, t), t;
|
|
43
44
|
} catch (o) {
|
|
44
45
|
return console.log(o), t.badRequest({
|
|
45
46
|
statusCode: 400,
|
|
46
|
-
message: o instanceof
|
|
47
|
-
error: o instanceof
|
|
47
|
+
message: o instanceof g ? "AST_ERROR" : "ERROR",
|
|
48
|
+
error: o instanceof g || o instanceof Error ? o.message : o
|
|
48
49
|
});
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
middleware(e) {
|
|
52
53
|
for (let t of e.controllers) {
|
|
53
|
-
let s =
|
|
54
|
+
let s = h(e.basePath, t.path);
|
|
54
55
|
this.addEntry(t, s);
|
|
55
56
|
}
|
|
56
57
|
return this;
|
|
57
58
|
}
|
|
58
59
|
controller(e) {
|
|
59
|
-
let t =
|
|
60
|
+
let t = h("/", e.path);
|
|
60
61
|
return this.addEntry(e, t), this;
|
|
61
62
|
}
|
|
62
63
|
use(e, t) {
|
|
@@ -68,10 +69,10 @@ var h = class {
|
|
|
68
69
|
return this;
|
|
69
70
|
}
|
|
70
71
|
router(e) {
|
|
71
|
-
return this.controller(new
|
|
72
|
+
return this.controller(new w(e)), this;
|
|
72
73
|
}
|
|
73
|
-
method(e) {
|
|
74
|
-
return this.controller(new
|
|
74
|
+
method(e, t) {
|
|
75
|
+
return this.controller(new R(e, t)), this;
|
|
75
76
|
}
|
|
76
77
|
async resolvePluginContexts() {
|
|
77
78
|
for (let e in this.plugins) {
|
|
@@ -82,12 +83,13 @@ var h = class {
|
|
|
82
83
|
Object.assign(t.context, n);
|
|
83
84
|
}
|
|
84
85
|
}
|
|
86
|
+
this.pluginContext = Object.values(this.plugins).reduce((e, t) => ({ ...e, ...t.context }), {});
|
|
85
87
|
}
|
|
86
88
|
async listen(...e) {
|
|
87
89
|
await this.resolvePluginContexts(), await this.runHooks("beforeInitialize", this), await this.driver.listen(...e), await this.runHooks("afterInitialize", this);
|
|
88
90
|
}
|
|
89
91
|
addEntry(e, t) {
|
|
90
|
-
let s = e instanceof
|
|
92
|
+
let s = e instanceof R ? [e.method] : Object.keys(e.methods), n = { path: t, route: e, methods: s, url: new A(t) };
|
|
91
93
|
for (let r of s)
|
|
92
94
|
this.reminist.add(r, t, n);
|
|
93
95
|
}
|
|
@@ -111,7 +113,7 @@ var h = class {
|
|
|
111
113
|
if (!n || !r || !s) return;
|
|
112
114
|
for (let d of o) {
|
|
113
115
|
let u = await d({ instance: t, router: s, request: n, response: r, plugin: a });
|
|
114
|
-
if (u && u
|
|
116
|
+
if (u && typeof u == "object" && u.constructor?.name === "AsterResponse")
|
|
115
117
|
return u;
|
|
116
118
|
}
|
|
117
119
|
}
|
|
@@ -127,32 +129,45 @@ var h = class {
|
|
|
127
129
|
break;
|
|
128
130
|
}
|
|
129
131
|
}
|
|
132
|
+
async runMiddlewares(e, t, s, n) {
|
|
133
|
+
let r = {};
|
|
134
|
+
if (!e || e.length === 0) return { context: r };
|
|
135
|
+
let a = { request: t, response: s, schema: n, next: (o) => o };
|
|
136
|
+
for (let o of e) {
|
|
137
|
+
let d = await o.onRun(a);
|
|
138
|
+
if (d instanceof m) return { context: r, response: d };
|
|
139
|
+
d && typeof d == "object" && Object.assign(r, d);
|
|
140
|
+
}
|
|
141
|
+
return { context: r };
|
|
142
|
+
}
|
|
130
143
|
async runHandler({ route: e }, t, s) {
|
|
131
|
-
let n = t.getMethod().toLowerCase(), r = e instanceof
|
|
144
|
+
let n = t.getMethod().toLowerCase(), r = e instanceof R ? e.handler : e.methods[n], i = e instanceof R ? e.schema : e.schema?.[n];
|
|
132
145
|
if (!r) return null;
|
|
146
|
+
let a = await t.getBody(), { context: o, response: d } = await this.runMiddlewares(e.use, t, s, a);
|
|
147
|
+
if (d) return d;
|
|
133
148
|
if (i) {
|
|
134
|
-
let
|
|
135
|
-
if (!
|
|
149
|
+
let c = i.safeParse(a);
|
|
150
|
+
if (!c.success)
|
|
136
151
|
return s.validationError({
|
|
137
152
|
statusCode: 422,
|
|
138
153
|
message: "VALIDATION_ERROR",
|
|
139
|
-
error: JSON.parse(
|
|
154
|
+
error: JSON.parse(c.error)
|
|
140
155
|
});
|
|
141
|
-
s.send(
|
|
156
|
+
s.send(c.data);
|
|
142
157
|
}
|
|
143
|
-
let
|
|
158
|
+
let u = {
|
|
144
159
|
instance: this,
|
|
145
160
|
request: t,
|
|
146
161
|
response: s,
|
|
147
162
|
url: t.url,
|
|
148
|
-
schema:
|
|
149
|
-
middleware:
|
|
150
|
-
plugins:
|
|
163
|
+
schema: a,
|
|
164
|
+
middleware: o,
|
|
165
|
+
plugins: this.pluginContext
|
|
151
166
|
};
|
|
152
|
-
return r(
|
|
167
|
+
return r(u);
|
|
153
168
|
}
|
|
154
|
-
},
|
|
169
|
+
}, O = p;
|
|
155
170
|
export {
|
|
156
|
-
|
|
157
|
-
|
|
171
|
+
O as AsterFlow,
|
|
172
|
+
p as AsterFlowInstance
|
|
158
173
|
};
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { Runtime, type Adapter, type AnyAdapter } from '@asterflow/adapter';
|
|
2
2
|
import type { AnyPlugins, InferConfigArgument, InferPluginExtension, Plugin, ResolvedPlugin } from '@asterflow/plugin';
|
|
3
3
|
import { type Responders } from '@asterflow/response';
|
|
4
|
-
import { Method, Router, type AnyMiddleware, type AnyMiddlewares, type AnyRouter, type AnySchema, type
|
|
5
|
-
import { type NormalizePath } from '@asterflow/url-parser';
|
|
4
|
+
import { Method, Router, type AnyMiddleware, type AnyMiddlewares, type AnyRouter, type AnySchema, type MethodCallProps, type MethodConstructorOptions, type MethodHandler, type MethodKeys, type Middleware, type MiddlewareOutput, type RouteHandler, type RouterCallProps, type RouterOptions, type SchemaDynamic } from '@asterflow/router';
|
|
6
5
|
import { Reminist } from 'reminist';
|
|
7
6
|
import type { AsterFlowOptions } from '../types/asterflow';
|
|
8
|
-
import type {
|
|
9
|
-
import type { AnyReminist, InferReministContext, InferReministPath } from '../types/reminist';
|
|
7
|
+
import type { AnyReminist, DefaultReminist, InferReministContext } from '../types/reminist';
|
|
10
8
|
import type { BuildRouteContext, BuildRoutesContext, RouteEntry } from '../types/routes';
|
|
11
9
|
import type { AnyRecord } from '../types/utils';
|
|
12
|
-
export declare class AsterFlowInstance<const Drive extends AnyAdapter = Adapter<Runtime.Node>, const Routers extends AnyReminist =
|
|
10
|
+
export declare class AsterFlowInstance<const Drive extends AnyAdapter = Adapter<Runtime.Node>, const Routers extends AnyReminist = DefaultReminist, const Plugins extends AnyPlugins = {}, const Middlewares extends AnyMiddlewares = [], const Extension extends AnyRecord = {}> {
|
|
13
11
|
readonly driver: Drive;
|
|
14
12
|
readonly reminist: Routers;
|
|
15
13
|
readonly middlewares: Middlewares;
|
|
@@ -18,6 +16,8 @@ export declare class AsterFlowInstance<const Drive extends AnyAdapter = Adapter<
|
|
|
18
16
|
private readonly onResponsePlugins;
|
|
19
17
|
private readonly beforeInitializePlugins;
|
|
20
18
|
private readonly afterInitializePlugins;
|
|
19
|
+
/** Merged view of every plugin's context, cached by `resolvePluginContexts()` at `listen()` time - see `runHandler`. */
|
|
20
|
+
private pluginContext;
|
|
21
21
|
constructor(options?: AsterFlowOptions<Drive>);
|
|
22
22
|
/**
|
|
23
23
|
* Handles incoming requests, executing `onRequest` and `onResponse` plugin hooks.
|
|
@@ -31,34 +31,40 @@ export declare class AsterFlowInstance<const Drive extends AnyAdapter = Adapter<
|
|
|
31
31
|
middleware<BasePath extends string, const Routes extends readonly AnyRouter[]>(options: {
|
|
32
32
|
basePath: BasePath;
|
|
33
33
|
controllers: Routes;
|
|
34
|
-
}): AsterFlow<Drive, Reminist<
|
|
34
|
+
}): AsterFlow<Drive, Reminist<InferReministContext<Routers> extends Record<string, RouteEntry<string, AnyRouter>> ? InferReministContext<Routers> & BuildRoutesContext<BasePath, Routes> : BuildRoutesContext<BasePath, Routes>, MethodKeys[]>, Plugins, Middlewares, Extension>;
|
|
35
35
|
/**
|
|
36
36
|
* Adds a single controller to AsterFlow.
|
|
37
37
|
* The controller's path is normalized to be relative to the root.
|
|
38
38
|
*/
|
|
39
|
-
controller<Route extends AnyRouter>(router: Route): AsterFlow<Drive, Reminist<
|
|
39
|
+
controller<Route extends AnyRouter>(router: Route): AsterFlow<Drive, Reminist<InferReministContext<Routers> extends Record<string, RouteEntry<string, AnyRouter>> ? InferReministContext<Routers> & BuildRouteContext<Route> : BuildRouteContext<Route>, MethodKeys[]>, Plugins, Middlewares, Extension>;
|
|
40
40
|
/**
|
|
41
41
|
* Registers a plugin and its configuration with the AsterFlow instance.
|
|
42
42
|
* Applies any instance extensions defined by the plugin.
|
|
43
43
|
*/
|
|
44
|
-
use<Plug extends Plugin<any
|
|
44
|
+
use<Plug extends Plugin<any>>(plugin: Plug, config?: InferConfigArgument<Plug>): AsterFlow<Drive, Routers, Plugins & { [K in Plug["name"]]: ResolvedPlugin<Plug>; }, Middlewares, Extension & InferPluginExtension<Plug>>;
|
|
45
45
|
/**
|
|
46
46
|
* Creates and adds a new router to the AsterFlow instance.
|
|
47
47
|
* A router can contain multiple method handlers for different HTTP verbs.
|
|
48
48
|
*/
|
|
49
|
-
router<Responder extends Responders, const Path extends string = string, const Schema extends SchemaDynamic<MethodKeys> = SchemaDynamic<MethodKeys>, const
|
|
50
|
-
[Method in MethodKeys]?: RouteHandler<Path, Responder, Method, Schema,
|
|
49
|
+
router<Responder extends Responders, const Path extends string = string, const Schema extends SchemaDynamic<MethodKeys> = SchemaDynamic<MethodKeys>, const RouteMiddlewares extends readonly AnyMiddleware[] = [], const Context extends MiddlewareOutput<RouteMiddlewares> = MiddlewareOutput<RouteMiddlewares>, const Routers extends {
|
|
50
|
+
[Method in MethodKeys]?: RouteHandler<Path, Responder, Method, Schema, RouteMiddlewares, Context>;
|
|
51
51
|
} = {
|
|
52
|
-
[Method in MethodKeys]?: RouteHandler<Path, Responder, Method, Schema,
|
|
53
|
-
}, const Route extends Router<Responder, Path, Schema,
|
|
52
|
+
[Method in MethodKeys]?: RouteHandler<Path, Responder, Method, Schema, RouteMiddlewares, Context>;
|
|
53
|
+
}, const Route extends Router<RouterCallProps<Responder, Path, Schema, RouteMiddlewares, Context, Routers>> = Router<RouterCallProps<Responder, Path, Schema, RouteMiddlewares, Context, Routers>>>(options: RouterOptions<RouterCallProps<Responder, Path, Schema, RouteMiddlewares, Context, Routers>>): AsterFlow<Drive, Reminist<InferReministContext<Routers> extends Record<string, RouteEntry<string, AnyRouter>> ? InferReministContext<Routers> & BuildRouteContext<Route> : BuildRouteContext<Route>, MethodKeys[]>, Plugins, Middlewares, Extension>;
|
|
54
54
|
/**
|
|
55
55
|
* Creates and adds a new method handler (route) to the AsterFlow instance.
|
|
56
56
|
* Defines a specific route for an HTTP method (GET, POST, etc.).
|
|
57
57
|
*/
|
|
58
|
-
method<Responder extends Responders, const Path extends string, const Methoder extends MethodKeys, const Schema extends AnySchema, const
|
|
58
|
+
method<Responder extends Responders, const Path extends string = string, const Methoder extends MethodKeys = MethodKeys, const Schema extends AnySchema = AnySchema, const RouteMiddlewares extends readonly Middleware<Responder, Schema, string, Record<string, unknown>>[] = [], const Context extends MiddlewareOutput<RouteMiddlewares> = MiddlewareOutput<RouteMiddlewares>, const Instance extends AsterFlowInstance<Drive, Routers, Plugins, Middlewares, Extension> & AnyAsterflow = AsterFlowInstance<Drive, Routers, Plugins, Middlewares, Extension> & AnyAsterflow, const Handler extends MethodHandler<Path, Drive['runtime'], Responder, Schema, RouteMiddlewares, Context, Instance> = MethodHandler<Path, Drive['runtime'], Responder, Schema, RouteMiddlewares, Context, Instance>, const Route extends Method<MethodCallProps<Responder, Path, Drive['runtime'], Methoder, Schema, RouteMiddlewares, Context, Instance, {}, Handler>> = Method<MethodCallProps<Responder, Path, Drive['runtime'], Methoder, Schema, RouteMiddlewares, Context, Instance, {}, Handler>>>(methodKey: Methoder, options: MethodConstructorOptions<MethodCallProps<Responder, Path, Drive['runtime'], Methoder, Schema, RouteMiddlewares, Context, Instance, {}, Handler>>): AsterFlow<Drive, Reminist<InferReministContext<Routers> extends Record<string, RouteEntry<string, AnyRouter>> ? InferReministContext<Routers> & BuildRouteContext<Route> : BuildRouteContext<Route>, MethodKeys[]>, Plugins, Middlewares, Extension>;
|
|
59
59
|
/**
|
|
60
60
|
* Itera sobre todos os plugins registrados e executa seus resolvers
|
|
61
61
|
* de forma assíncrona, construindo o contexto de cada um.
|
|
62
|
+
*
|
|
63
|
+
* Also caches the merged view of every plugin's context into
|
|
64
|
+
* `this.pluginContext` here, once, instead of re-merging it (via
|
|
65
|
+
* `Object.values(...).reduce(...)`, an O(plugins) allocation) on every
|
|
66
|
+
* single request in `runHandler` - plugin contexts are static after this
|
|
67
|
+
* point.
|
|
62
68
|
*/
|
|
63
69
|
private resolvePluginContexts;
|
|
64
70
|
/**
|
|
@@ -76,14 +82,24 @@ export declare class AsterFlowInstance<const Drive extends AnyAdapter = Adapter<
|
|
|
76
82
|
*/
|
|
77
83
|
private runHooks;
|
|
78
84
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
85
|
+
* Runs a route's `use` middleware chain in order, merging each
|
|
86
|
+
* middleware's `next(params)` output into a single accumulated context
|
|
87
|
+
* object. If a middleware returns an `AsterResponse` instead of calling
|
|
88
|
+
* `next(...)` (e.g. `return response.unauthorized({...})`), the chain
|
|
89
|
+
* stops immediately and that response is propagated back as final.
|
|
90
|
+
*/
|
|
91
|
+
private runMiddlewares;
|
|
92
|
+
/**
|
|
93
|
+
* Executes a route handler, processing the request and response. Runs
|
|
94
|
+
* `use` middlewares first (so an unauthorized/rejected request short-
|
|
95
|
+
* circuits before paying for body validation), then schema validation if
|
|
96
|
+
* present, then invokes the route handler.
|
|
81
97
|
*/
|
|
82
98
|
private runHandler;
|
|
83
99
|
}
|
|
84
|
-
export type AsterFlow<Drive extends AnyAdapter = AnyAdapter, Routers extends AnyReminist =
|
|
100
|
+
export type AsterFlow<Drive extends AnyAdapter = AnyAdapter, Routers extends AnyReminist = DefaultReminist, Plugins extends AnyPlugins = AnyPlugins, Middlewares extends readonly AnyMiddleware[] = AnyMiddleware[], Extension extends Record<string, any> = Record<string, any>> = AsterFlowInstance<Drive, Routers, Plugins, Middlewares, Extension> & Extension;
|
|
85
101
|
export declare const AsterFlow: {
|
|
86
|
-
new <Drive extends AnyAdapter = Adapter<Runtime.Node>>(options?: AsterFlowOptions<Drive>): AsterFlow<Drive,
|
|
102
|
+
new <Drive extends AnyAdapter = Adapter<Runtime.Node>>(options?: AsterFlowOptions<Drive>): AsterFlow<Drive, DefaultReminist, {}, [], {}>;
|
|
87
103
|
};
|
|
88
104
|
/**
|
|
89
105
|
* Represents a generic AsterFlow instance, with all its types defined as `any`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Method, MethodProps, Router, RouterProps } from '@asterflow/router';
|
|
2
2
|
import type { NormalizePath } from '@asterflow/url-parser';
|
|
3
3
|
/**
|
|
4
4
|
* Combines a base path with a relative path and normalizes the result.
|
|
@@ -8,10 +8,4 @@ export type CombinePaths<Base extends string, Path extends string> = NormalizePa
|
|
|
8
8
|
* Infers the path from a `Router` or `Method` type.
|
|
9
9
|
* This normalizes the path and combines it with the root.
|
|
10
10
|
*/
|
|
11
|
-
export type InferPath<T> = T extends Router<
|
|
12
|
-
/**
|
|
13
|
-
* Extracts and normalizes the paths from an array of routers (`AnyRouter[]`), combining them with a base path.
|
|
14
|
-
*/
|
|
15
|
-
export type ExtractPaths<Base extends string, Routes extends readonly AnyRouter[]> = {
|
|
16
|
-
[K in keyof Routes]: Routes[K] extends infer R extends AnyRouter ? CombinePaths<Base, InferPath<R>> : never;
|
|
17
|
-
};
|
|
11
|
+
export type InferPath<T> = T extends Router<infer Props extends RouterProps> ? CombinePaths<'/', Props['path']> : T extends Method<infer Props extends MethodProps> ? CombinePaths<'/', Props['path']> : never;
|
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MethodKeys } from '@asterflow/router';
|
|
2
2
|
import type { Reminist } from 'reminist';
|
|
3
|
-
import type { RouteEntry } from './routes';
|
|
4
3
|
/**
|
|
5
4
|
* Represents a generic Reminist instance, used for managing routes.
|
|
6
|
-
* Includes a
|
|
5
|
+
* Includes a record of route entries and an array of HTTP method keys.
|
|
7
6
|
*/
|
|
8
|
-
export type AnyReminist = Reminist<
|
|
7
|
+
export type AnyReminist = Reminist<any, any>;
|
|
9
8
|
/**
|
|
10
|
-
*
|
|
9
|
+
* The `Routers` type param's actual starting value for a fresh `AsterFlow`
|
|
10
|
+
* instance (before any `.method()`/`.router()` call). Deliberately NOT
|
|
11
|
+
* `AnyReminist`: a bare `any` context here makes
|
|
12
|
+
* `InferReministContext<Routers> extends Record<...> ? A : B` (used by
|
|
13
|
+
* `.method()`/`.router()`/`.controller()`/`.middleware()`'s return type)
|
|
14
|
+
* collapse to `any` - TS special-cases a naked `any` in a conditional
|
|
15
|
+
* type's checked position to `A | B`, and `any & X` is `any`, so the whole
|
|
16
|
+
* union collapses back to `any`. An empty, concrete context sidesteps that.
|
|
11
17
|
*/
|
|
12
|
-
export type
|
|
18
|
+
export type DefaultReminist = Reminist<{}, MethodKeys[]>;
|
|
13
19
|
/**
|
|
14
20
|
* Infers the context from a Reminist instance.
|
|
15
21
|
*/
|
|
16
|
-
export type InferReministContext<T> = T extends Reminist<
|
|
17
|
-
/**
|
|
18
|
-
* Reminist context that preserves specific route information.
|
|
19
|
-
* Each path maps to its specific typed route.
|
|
20
|
-
*/
|
|
21
|
-
export type ReministContext<PathsAndRoutes extends Record<string, AnyRouter>> = {
|
|
22
|
-
readonly [Path in keyof PathsAndRoutes]: RouteEntry<Path extends string ? Path : never, PathsAndRoutes[Path]>;
|
|
23
|
-
};
|
|
22
|
+
export type InferReministContext<T> = T extends Reminist<infer C, any> ? C : never;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyRouter, MethodKeys } from '@asterflow/router';
|
|
1
|
+
import type { AnyRouter, MethodKeys, Prettify } from '@asterflow/router';
|
|
2
2
|
import type { CombinePaths, InferPath } from './paths';
|
|
3
3
|
import type { UnionToIntersection } from './utils';
|
|
4
4
|
import type { Analyze, NormalizePath } from '@asterflow/url-parser';
|
|
@@ -18,10 +18,12 @@ export type BuildRoutesContext<Base extends string, Routes extends readonly AnyR
|
|
|
18
18
|
}[number] & {}> & Record<string, RouteEntry<string, AnyRouter>>;
|
|
19
19
|
/**
|
|
20
20
|
* Defines the specific typing preserved for each route entry in Reminist.
|
|
21
|
+
* Wrapped in `Prettify` so it shows as a labeled `{ path: ..., route: ..., ... }`
|
|
22
|
+
* object on hover instead of `RouteEntry<"...", Method<...>>` by name.
|
|
21
23
|
*/
|
|
22
|
-
export type RouteEntry<Path extends string, Route extends AnyRouter> = {
|
|
24
|
+
export type RouteEntry<Path extends string, Route extends AnyRouter> = Prettify<{
|
|
23
25
|
readonly path: Path;
|
|
24
26
|
readonly route: Route;
|
|
25
27
|
readonly methods: readonly MethodKeys[];
|
|
26
28
|
readonly url: Analyze<string>;
|
|
27
|
-
}
|
|
29
|
+
}>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "asterflow",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "The core framework - ties together adapters, routing, plugins and responses into one typed AsterFlow app.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"asterflow",
|
|
7
|
+
"http",
|
|
8
|
+
"framework",
|
|
9
|
+
"server",
|
|
10
|
+
"router",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
4
13
|
"main": "dist/cjs/index.cjs",
|
|
5
14
|
"module": "dist/mjs/index.js",
|
|
6
15
|
"types": "dist/types/index.d.ts",
|
|
@@ -34,11 +43,11 @@
|
|
|
34
43
|
"typescript": "^5.8.3"
|
|
35
44
|
},
|
|
36
45
|
"dependencies": {
|
|
37
|
-
"@asterflow/adapter": "1.0.
|
|
38
|
-
"@asterflow/plugin": "1.0
|
|
39
|
-
"@asterflow/response": "1.0
|
|
40
|
-
"@asterflow/router": "
|
|
41
|
-
"@asterflow/url-parser": "^
|
|
42
|
-
"reminist": "^1.0.
|
|
46
|
+
"@asterflow/adapter": "^1.0.13",
|
|
47
|
+
"@asterflow/plugin": "^1.1.0",
|
|
48
|
+
"@asterflow/response": "^1.1.0",
|
|
49
|
+
"@asterflow/router": "^2.0.0",
|
|
50
|
+
"@asterflow/url-parser": "^4.1.1",
|
|
51
|
+
"reminist": "^1.0.8"
|
|
43
52
|
}
|
|
44
53
|
}
|