gruber 0.1.0 → 0.2.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/CHANGELOG.md +19 -0
- package/README.md +119 -1
- package/core/configuration.js +30 -25
- package/core/fetch-router.js +36 -23
- package/core/fetch-router.test.js +50 -22
- package/core/http.js +20 -21
- package/core/migrator.test.js +3 -4
- package/core/types.ts +41 -0
- package/core/utilities.js +10 -4
- package/package.json +12 -1
- package/source/node-router.js +8 -1
- package/source/package.json +9 -0
- package/source/postgres.js +18 -1
- package/tsconfig.json +17 -0
- package/types/core/configuration.d.ts +57 -0
- package/types/core/configuration.d.ts.map +1 -0
- package/types/core/configuration.test.d.ts +2 -0
- package/types/core/configuration.test.d.ts.map +1 -0
- package/types/core/fetch-router.d.ts +52 -0
- package/types/core/fetch-router.d.ts.map +1 -0
- package/types/core/fetch-router.test.d.ts +2 -0
- package/types/core/fetch-router.test.d.ts.map +1 -0
- package/types/core/http.d.ts +55 -0
- package/types/core/http.d.ts.map +1 -0
- package/types/core/http.test.d.ts +2 -0
- package/types/core/http.test.d.ts.map +1 -0
- package/types/core/migrator.d.ts +56 -0
- package/types/core/migrator.d.ts.map +1 -0
- package/types/core/migrator.test.d.ts +2 -0
- package/types/core/migrator.test.d.ts.map +1 -0
- package/types/core/mod.d.ts +7 -0
- package/types/core/mod.d.ts.map +1 -0
- package/types/core/postgres.d.ts +43 -0
- package/types/core/postgres.d.ts.map +1 -0
- package/types/core/test-deps.d.ts +2 -0
- package/types/core/test-deps.d.ts.map +1 -0
- package/types/core/types.d.ts +23 -0
- package/types/core/types.d.ts.map +1 -0
- package/types/core/utilities.d.ts +13 -0
- package/types/core/utilities.d.ts.map +1 -0
- package/types/core/utilities.test.d.ts +2 -0
- package/types/core/utilities.test.d.ts.map +1 -0
- package/types/source/configuration.d.ts +24 -0
- package/types/source/configuration.d.ts.map +1 -0
- package/types/source/core.d.ts +2 -0
- package/types/source/core.d.ts.map +1 -0
- package/types/source/express-router.d.ts +28 -0
- package/types/source/express-router.d.ts.map +1 -0
- package/types/source/koa-router.d.ts +33 -0
- package/types/source/koa-router.d.ts.map +1 -0
- package/types/source/mod.d.ts +7 -0
- package/types/source/mod.d.ts.map +1 -0
- package/types/source/node-router.d.ts +42 -0
- package/types/source/node-router.d.ts.map +1 -0
- package/types/source/polyfill.d.ts +2 -0
- package/types/source/polyfill.d.ts.map +1 -0
- package/types/source/postgres.d.ts +32 -0
- package/types/source/postgres.d.ts.map +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
This file documents notable changes to the project
|
|
4
4
|
|
|
5
|
+
## 0.2.0
|
|
6
|
+
|
|
7
|
+
**new**
|
|
8
|
+
|
|
9
|
+
- Added and documented `FetchRouter`
|
|
10
|
+
- Generate typings for Node.js
|
|
11
|
+
- Unhandled route errors are logged to the console
|
|
12
|
+
|
|
13
|
+
**fixes**
|
|
14
|
+
|
|
15
|
+
- Configuration is better typed to be able to infer the final structure
|
|
16
|
+
- Add missing NPM package information
|
|
17
|
+
|
|
18
|
+
**docs**
|
|
19
|
+
|
|
20
|
+
- Add a section on installation
|
|
21
|
+
- Add a section on the releaes process
|
|
22
|
+
- Add information about `loader` and `formatMarkdownTable`
|
|
23
|
+
|
|
5
24
|
## 0.1.0
|
|
6
25
|
|
|
7
26
|
🎉 Everything is new!
|
package/README.md
CHANGED
|
@@ -77,6 +77,27 @@ A Gruber app should be run behind a reverse proxy and that can do those things f
|
|
|
77
77
|
- Minimal — start small, carefully add features and consider removing them
|
|
78
78
|
- No magic — it's confusing when you don't know whats going on
|
|
79
79
|
|
|
80
|
+
## Install
|
|
81
|
+
|
|
82
|
+
**Node.js**
|
|
83
|
+
|
|
84
|
+
Gruber is available on NPM as [gruber](https://www.npmjs.com/package/gruber).
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# cd to/your/project
|
|
88
|
+
npm install gruber
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Deno**
|
|
92
|
+
|
|
93
|
+
> WORK IN PROGRESS
|
|
94
|
+
|
|
95
|
+
Gruber is available at [esm.r0b.io/gruber@0.1.0/mod.ts](https://esm.r0b.io/gruber@0.1.0/mod.ts).
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
import { defineRoute } from "https://esm.r0b.io/gruber@0.1.0/mod.ts";
|
|
99
|
+
```
|
|
100
|
+
|
|
80
101
|
## HTTP server
|
|
81
102
|
|
|
82
103
|
First a HTTP route to do something:
|
|
@@ -797,6 +818,49 @@ teapot.toResponse();
|
|
|
797
818
|
Currently, you can't set the body of the generated Response objects.
|
|
798
819
|
This would be nice to have in the future, but the API should be thoughtfully designed first.
|
|
799
820
|
|
|
821
|
+
### FetchRouter
|
|
822
|
+
|
|
823
|
+
`FetchRouter` is a web-native router for routes defined with `defineRoute`.
|
|
824
|
+
|
|
825
|
+
```js
|
|
826
|
+
import { FetchRouter, defineRoute } from "gruber";
|
|
827
|
+
|
|
828
|
+
const routes = [defineRoute("..."), defineRoute("..."), defineRoute("...")];
|
|
829
|
+
|
|
830
|
+
const router = new FetchRouter({
|
|
831
|
+
routes,
|
|
832
|
+
errorHandler(error, request) {
|
|
833
|
+
console.log("Route error", error);
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
All options to the `FetchRouter` constructor are optional and you can create a router without any options if you want.
|
|
839
|
+
|
|
840
|
+
`routes` are the route definitions you want the router to processes, the router will handle a request based on the first route that matches.
|
|
841
|
+
So order is important.
|
|
842
|
+
|
|
843
|
+
`errorHandler` is called if a non-`HTTPError` or a 5xx `HTTPError` is thrown.
|
|
844
|
+
It is called with the offending error and the request it is associated with.
|
|
845
|
+
|
|
846
|
+
> NOTE: The `errorHandler` could do more in the future, like create it's own Response or mutate the existing response.
|
|
847
|
+
> This has not been design and is left open to future development if it becomes important.
|
|
848
|
+
|
|
849
|
+
**getResponse**
|
|
850
|
+
|
|
851
|
+
`getResponse` is the main method on a router.
|
|
852
|
+
Use it to get a `Response` from the provided request, based on the router's route definitions.
|
|
853
|
+
|
|
854
|
+
```js
|
|
855
|
+
const response = await router.getResponse(new Request("http://localhost"));
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
There are some unstable internal methods too:
|
|
859
|
+
|
|
860
|
+
- `findMatchingRoutes(request)` is a generator function to get the first route definition that matches the supplied request. It's a generator so as few routes are matched as possible and execution can be stopped if you like.
|
|
861
|
+
- `processMatches(request, matches)` attempts to get a `Response` from a request and an Iterator of route definitions.
|
|
862
|
+
- `handleError(error, request)` converts a error into a Response and triggers the `errorHandler`
|
|
863
|
+
|
|
800
864
|
### Postgres
|
|
801
865
|
|
|
802
866
|
#### getPostgresMigratorOptions
|
|
@@ -804,6 +868,52 @@ This would be nice to have in the future, but the API should be thoughtfully des
|
|
|
804
868
|
`getPostgresMigratorOptions` generates the default options for a `PostgresMigrator`.
|
|
805
869
|
You can use it and override parts of it to customise how the postgres migrator works.
|
|
806
870
|
|
|
871
|
+
### Utilities
|
|
872
|
+
|
|
873
|
+
### loader
|
|
874
|
+
|
|
875
|
+
`loader` let's you memoize the result of a function to create a singleton from it.
|
|
876
|
+
It works synchronously or with promises.
|
|
877
|
+
|
|
878
|
+
```js
|
|
879
|
+
import { loader } from "gruber";
|
|
880
|
+
|
|
881
|
+
const useRedis = loader(async () => {
|
|
882
|
+
return "connect to the database somehow...";
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// Then elsewhere
|
|
886
|
+
const redis = await useRedis();
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
#### formatMarkdownTable
|
|
890
|
+
|
|
891
|
+
`formatMarkdownTable` generates a pretty markdown table based on an array of rows and the desired column names.
|
|
892
|
+
|
|
893
|
+
```js
|
|
894
|
+
import { formatMarkdownTable } from "gruber";
|
|
895
|
+
|
|
896
|
+
const table = formatMarkdownTable(
|
|
897
|
+
[
|
|
898
|
+
{ name: "Geoff Testington", age: 42 },
|
|
899
|
+
{ name: "Jess Smith", age: 32 },
|
|
900
|
+
{ name: "Tyler Rockwell" },
|
|
901
|
+
],
|
|
902
|
+
["name", "age"],
|
|
903
|
+
"~",
|
|
904
|
+
);
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
This will generate the table:
|
|
908
|
+
|
|
909
|
+
```
|
|
910
|
+
| name | age |
|
|
911
|
+
| ================ | === |
|
|
912
|
+
| Geoff Testington | 42 |
|
|
913
|
+
| Jess Smith | 32 |
|
|
914
|
+
| Tyler Rockwell | ~ |
|
|
915
|
+
```
|
|
916
|
+
|
|
807
917
|
## Node.js library
|
|
808
918
|
|
|
809
919
|
There are some specific helpers to help use Gruber in Node.js apps.
|
|
@@ -913,6 +1023,15 @@ const server = http.createServer((req) => {
|
|
|
913
1023
|
});
|
|
914
1024
|
```
|
|
915
1025
|
|
|
1026
|
+
## Release process
|
|
1027
|
+
|
|
1028
|
+
1. Generate a new version at the root with `npm version <version>`
|
|
1029
|
+
2. Run the bundle `./bundle.js`
|
|
1030
|
+
3. Publish the node module
|
|
1031
|
+
1. `cd bundle/node`
|
|
1032
|
+
2. `npm publish`
|
|
1033
|
+
4. Copy the deno source to the S3 bucket — `bundle/deno` → `esm.r0b.io/gruber@VERSION/`
|
|
1034
|
+
|
|
916
1035
|
---
|
|
917
1036
|
|
|
918
1037
|
<!-- -->
|
|
@@ -992,7 +1111,6 @@ retryWithBackoff({
|
|
|
992
1111
|
|
|
993
1112
|
## Rob's notes
|
|
994
1113
|
|
|
995
|
-
- should exposing `appConfig` be a best practice?
|
|
996
1114
|
- `core` tests are deno because it's hard to do both and Deno is more web-standards based
|
|
997
1115
|
- json schema for configuration specs?
|
|
998
1116
|
- note or info about loading dot-env files
|
package/core/configuration.js
CHANGED
|
@@ -26,6 +26,8 @@ const _requiredOptions = [
|
|
|
26
26
|
"parse",
|
|
27
27
|
];
|
|
28
28
|
|
|
29
|
+
/** @typedef {Record<string, import("superstruct").Struct<any, any>>} ObjectSchema */
|
|
30
|
+
|
|
29
31
|
export class Configuration {
|
|
30
32
|
static spec = Symbol("Configuration.spec");
|
|
31
33
|
|
|
@@ -39,51 +41,53 @@ export class Configuration {
|
|
|
39
41
|
this.options = options;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* @template {ObjectSchema} T
|
|
46
|
+
* @param {T} spec
|
|
47
|
+
*/
|
|
43
48
|
object(spec) {
|
|
44
|
-
|
|
45
|
-
this.options.superstruct.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
);
|
|
49
|
+
const struct = this.options.superstruct.defaulted(
|
|
50
|
+
this.options.superstruct.object(spec),
|
|
51
|
+
{},
|
|
52
|
+
)
|
|
53
|
+
struct[Configuration.spec]= { type: "object", value: spec }
|
|
54
|
+
return struct
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
/**
|
|
54
58
|
* @template {SpecOptions} Spec @param {Spec} spec
|
|
59
|
+
* @returns {import("superstruct").Struct<string, null>}
|
|
55
60
|
*/
|
|
56
61
|
string(spec = {}) {
|
|
57
62
|
if (typeof spec.fallback !== "string") {
|
|
58
63
|
throw new TypeError("spec.fallback must be a string: " + spec.fallback);
|
|
59
64
|
}
|
|
60
|
-
|
|
61
|
-
this.options.superstruct.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
);
|
|
65
|
+
const struct = this.options.superstruct.defaulted(
|
|
66
|
+
this.options.superstruct.string(),
|
|
67
|
+
this._getValue(spec),
|
|
68
|
+
)
|
|
69
|
+
struct[Configuration.spec] = { type: "string", value: spec }
|
|
70
|
+
return struct
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
/**
|
|
70
74
|
* @template {SpecOptions} Spec @param {Spec} spec
|
|
75
|
+
* @returns {import("superstruct").Struct<URL, null>}
|
|
71
76
|
*/
|
|
72
77
|
url(spec) {
|
|
73
78
|
if (typeof spec.fallback !== "string") {
|
|
74
79
|
throw new TypeError("spec.fallback must be a string");
|
|
75
80
|
}
|
|
76
|
-
|
|
77
|
-
this.options.superstruct.
|
|
78
|
-
this.options.superstruct.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
(value) => new URL(value),
|
|
82
|
-
),
|
|
83
|
-
this._getValue(spec),
|
|
81
|
+
const struct = this.options.superstruct.defaulted(
|
|
82
|
+
this.options.superstruct.coerce(
|
|
83
|
+
this.options.superstruct.instance(URL),
|
|
84
|
+
this.options.superstruct.string(),
|
|
85
|
+
(value) => new URL(value),
|
|
84
86
|
),
|
|
85
|
-
|
|
86
|
-
)
|
|
87
|
+
this._getValue(spec),
|
|
88
|
+
)
|
|
89
|
+
struct[Configuration.spec]= { type: "url", value: spec }
|
|
90
|
+
return struct
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
/** @param {SpecOptions} spec */
|
|
@@ -103,6 +107,7 @@ export class Configuration {
|
|
|
103
107
|
* @template T
|
|
104
108
|
* @param {URL} url
|
|
105
109
|
* @param {import("superstruct").Struct<T>} spec
|
|
110
|
+
* @returns {Promise<T>}
|
|
106
111
|
*/
|
|
107
112
|
async load(url, spec) {
|
|
108
113
|
const file = await this.options.readTextFile(url);
|
package/core/fetch-router.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*/
|
|
1
|
+
import { HTTPError } from "./http.js";
|
|
2
|
+
|
|
3
|
+
/** @typedef {import("./types.ts").RouteDefinition<any>} RouteDefinition */
|
|
4
|
+
|
|
5
|
+
/** @typedef {(error: unknown, request: Request) => unknown} RouteErrorHandler */
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* @typedef {object} MatchedRoute
|
|
@@ -9,32 +11,40 @@
|
|
|
9
11
|
* @property {URLPatternResult} result
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} FetchRouterOptions
|
|
16
|
+
* @property {RouteDefinition[]} [routes]
|
|
17
|
+
* @property {RouteErrorHandler} [errorHandler]
|
|
18
|
+
*/
|
|
13
19
|
|
|
14
|
-
/** A rudimentary HTTP router using fetch Request & Responses with
|
|
20
|
+
/** A rudimentary HTTP router using fetch Request & Responses with RouteDefinitions based on URLPattern */
|
|
15
21
|
export class FetchRouter {
|
|
16
|
-
|
|
22
|
+
/** @type {RouteDefinition} */ routes;
|
|
23
|
+
/** @type {RouteErrorHandler | null} */ errorHandler;
|
|
17
24
|
|
|
18
|
-
/** @param {
|
|
19
|
-
constructor(
|
|
20
|
-
this.routes = routes;
|
|
25
|
+
/** @param {FetchRouterOptions} [options] */
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this.routes = options.routes ?? [];
|
|
28
|
+
this.errorHandler = options.errorHandler ?? null;
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
/**
|
|
24
32
|
* Finds routes that match the request method and URLPattern
|
|
25
33
|
* and get's the matched parameters and parsed URL
|
|
26
34
|
* @param {Request} request
|
|
27
|
-
* @returns {MatchedRoute
|
|
35
|
+
* @returns {Iterator<MatchedRoute>}
|
|
28
36
|
*/
|
|
29
|
-
findMatchingRoutes(request) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
*findMatchingRoutes(request) {
|
|
38
|
+
const url = new URL(request.url);
|
|
39
|
+
|
|
40
|
+
for (const route of this.routes) {
|
|
41
|
+
if (request.method !== route.method) continue;
|
|
42
|
+
|
|
43
|
+
const result = route.pattern.exec(url);
|
|
44
|
+
if (!result) continue;
|
|
45
|
+
|
|
46
|
+
yield { result, route, url };
|
|
47
|
+
}
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
/**
|
|
@@ -57,13 +67,16 @@ export class FetchRouter {
|
|
|
57
67
|
throw HTTPError.notFound();
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
/**
|
|
71
|
+
* @param {Request} request
|
|
72
|
+
* @param {unknown} error
|
|
73
|
+
*/
|
|
74
|
+
handleError(request, error) {
|
|
61
75
|
// Get or create a HTTP error based on the one thrown
|
|
62
76
|
const httpError =
|
|
63
77
|
error instanceof HTTPError ? error : HTTPError.internalServerError();
|
|
64
78
|
|
|
65
|
-
|
|
66
|
-
// if (httpError.status >= 500) console.error("FetchRouter error:", error);
|
|
79
|
+
if (httpError.status >= 500) this.errorHandler?.(error, request);
|
|
67
80
|
|
|
68
81
|
return httpError.toResponse();
|
|
69
82
|
}
|
|
@@ -76,7 +89,7 @@ export class FetchRouter {
|
|
|
76
89
|
this.findMatchingRoutes(request),
|
|
77
90
|
);
|
|
78
91
|
} catch (error) {
|
|
79
|
-
return this.handleError(error);
|
|
92
|
+
return this.handleError(request, error);
|
|
80
93
|
}
|
|
81
94
|
}
|
|
82
95
|
}
|
|
@@ -4,17 +4,19 @@ import { assertEquals, assertInstanceOf } from "./test-deps.js";
|
|
|
4
4
|
|
|
5
5
|
Deno.test("FetchRouter", async (t) => {
|
|
6
6
|
await t.step("constructor", () => {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
const routes = [
|
|
8
|
+
defineRoute({
|
|
9
|
+
method: "GET",
|
|
10
|
+
pathname: "/",
|
|
11
|
+
handler: () => new Response("OK"),
|
|
12
|
+
}),
|
|
13
|
+
];
|
|
14
|
+
const result = new FetchRouter({ routes });
|
|
15
|
+
assertEquals(result.routes, routes, "should store the routes");
|
|
14
16
|
});
|
|
15
17
|
|
|
16
18
|
await t.step("findMatchingRoutes", async (t) => {
|
|
17
|
-
const
|
|
19
|
+
const routes = [
|
|
18
20
|
defineRoute({
|
|
19
21
|
method: "GET",
|
|
20
22
|
pathname: "/",
|
|
@@ -25,27 +27,30 @@ Deno.test("FetchRouter", async (t) => {
|
|
|
25
27
|
pathname: "/hello/:name",
|
|
26
28
|
handler: () => new Response("OK"),
|
|
27
29
|
}),
|
|
28
|
-
]
|
|
30
|
+
];
|
|
31
|
+
const router = new FetchRouter({ routes });
|
|
29
32
|
|
|
30
33
|
await t.step("returns the match", () => {
|
|
31
|
-
const result =
|
|
32
|
-
new Request("http://localhost/"),
|
|
33
|
-
|
|
34
|
+
const result = [
|
|
35
|
+
...router.findMatchingRoutes(new Request("http://localhost/")),
|
|
36
|
+
];
|
|
34
37
|
|
|
35
38
|
assertEquals(result.length, 1, "should match 1 route");
|
|
36
39
|
});
|
|
37
40
|
await t.step("parses the URL", () => {
|
|
38
|
-
const result =
|
|
39
|
-
new Request("http://localhost/"),
|
|
40
|
-
|
|
41
|
+
const result = [
|
|
42
|
+
...router.findMatchingRoutes(new Request("http://localhost/")),
|
|
43
|
+
];
|
|
41
44
|
|
|
42
45
|
assertEquals(result.length, 1, "should match 1 route");
|
|
43
46
|
assertInstanceOf(result[0].url, URL);
|
|
44
47
|
});
|
|
45
48
|
await t.step("parse params", () => {
|
|
46
|
-
const result =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
const result = [
|
|
50
|
+
...router.findMatchingRoutes(
|
|
51
|
+
new Request("http://localhost/hello/Geoff", { method: "POST" }),
|
|
52
|
+
),
|
|
53
|
+
];
|
|
49
54
|
|
|
50
55
|
assertEquals(result.length, 1, "should match 1 route");
|
|
51
56
|
assertEquals(
|
|
@@ -97,26 +102,49 @@ Deno.test("FetchRouter", async (t) => {
|
|
|
97
102
|
const router = new FetchRouter();
|
|
98
103
|
|
|
99
104
|
await t.step("converts to HTTPError", () => {
|
|
100
|
-
const result = router.handleError(
|
|
105
|
+
const result = router.handleError(
|
|
106
|
+
new Request("http://localhost"),
|
|
107
|
+
new Error(),
|
|
108
|
+
);
|
|
101
109
|
assertInstanceOf(result, Response);
|
|
102
110
|
assertEquals(result.status, 500);
|
|
103
111
|
});
|
|
104
112
|
|
|
105
113
|
await t.step("uses the HTTPError", () => {
|
|
106
|
-
const result = router.handleError(
|
|
114
|
+
const result = router.handleError(
|
|
115
|
+
new Request("http://localhost"),
|
|
116
|
+
new HTTPError(400, "Bad Request"),
|
|
117
|
+
);
|
|
107
118
|
assertInstanceOf(result, Response);
|
|
108
119
|
assertEquals(result.status, 400);
|
|
109
120
|
});
|
|
121
|
+
|
|
122
|
+
await t.step("calls the callback", () => {
|
|
123
|
+
let args = [];
|
|
124
|
+
const router = new FetchRouter({
|
|
125
|
+
errorHandler(...result) {
|
|
126
|
+
args = result;
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
router.handleError(
|
|
130
|
+
new Request("http://localhost"),
|
|
131
|
+
new HTTPError(500, "Internal Server Error"),
|
|
132
|
+
);
|
|
133
|
+
assertInstanceOf(args[0], HTTPError);
|
|
134
|
+
assertEquals(args[0].status, 500);
|
|
135
|
+
assertInstanceOf(args[1], Request);
|
|
136
|
+
});
|
|
110
137
|
});
|
|
111
138
|
|
|
112
139
|
await t.step("getResponse", async () => {
|
|
113
|
-
const
|
|
140
|
+
const routes = [
|
|
114
141
|
defineRoute({
|
|
115
142
|
method: "PATCH",
|
|
116
143
|
pathname: "/hello/:name",
|
|
117
144
|
handler: ({ params }) => new Response(`Hello ${params.name}!`),
|
|
118
145
|
}),
|
|
119
|
-
]
|
|
146
|
+
];
|
|
147
|
+
const router = new FetchRouter({ routes });
|
|
120
148
|
|
|
121
149
|
const result = await router.getResponse(
|
|
122
150
|
new Request("http://localhost/hello/Geoff", { method: "PATCH" }),
|
package/core/http.js
CHANGED
|
@@ -1,41 +1,40 @@
|
|
|
1
|
+
/** @typedef {import("./types.ts").HTTPMethod} HTTPMethod */
|
|
2
|
+
/** @typedef {import("./types.ts").RouteResult} RouteResult */
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
|
-
* @
|
|
3
|
-
*/
|
|
5
|
+
* @template {string} T
|
|
6
|
+
* @typedef {import("./types.ts").ExtractRouteParams<T>} ExtractRouteParams */
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
|
-
* @
|
|
7
|
-
* @
|
|
8
|
-
* @property {URL} url
|
|
9
|
-
* @property {Record<string, string>} params
|
|
9
|
+
* @template T
|
|
10
|
+
* @typedef {import("./types.ts").RouteContext<T>} RouteContext
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
* @
|
|
14
|
+
* @template T
|
|
15
|
+
* @typedef {import("./types.ts").RouteHandler<T>} RouteHandler
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
|
-
* @
|
|
18
|
-
* @
|
|
19
|
-
* @property {pathname} pathname
|
|
20
|
-
* @property {RouteHandler} handler
|
|
19
|
+
* @template {string} T
|
|
20
|
+
* @typedef {import("./types.ts").RouteOptions<T>} RouteOptions
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* @
|
|
25
|
-
* @
|
|
26
|
-
* @property {URLPattern} pattern
|
|
27
|
-
* @property {RouteHandler} handler
|
|
24
|
+
* @template [T]
|
|
25
|
+
* @typedef {import("./types.ts").RouteDefinition<T>} RouteDefinition
|
|
28
26
|
*/
|
|
29
27
|
|
|
30
28
|
/**
|
|
31
|
-
* @
|
|
32
|
-
* @
|
|
29
|
+
* @template {string} T
|
|
30
|
+
* @param {RouteOptions<T>} init
|
|
31
|
+
* @returns {RouteDefinition<T>}
|
|
33
32
|
*/
|
|
34
|
-
export function defineRoute(
|
|
33
|
+
export function defineRoute(init) {
|
|
35
34
|
return {
|
|
36
|
-
method:
|
|
37
|
-
pattern: new URLPattern({ pathname:
|
|
38
|
-
handler:
|
|
35
|
+
method: init.method,
|
|
36
|
+
pattern: new URLPattern({ pathname: init.pathname }),
|
|
37
|
+
handler: init.handler,
|
|
39
38
|
};
|
|
40
39
|
}
|
|
41
40
|
|
package/core/migrator.test.js
CHANGED
|
@@ -4,8 +4,7 @@ import { assertEquals } from "./test-deps.js";
|
|
|
4
4
|
const bareOptions = {
|
|
5
5
|
getDefinitions: () => [],
|
|
6
6
|
getRecords: () => [],
|
|
7
|
-
|
|
8
|
-
executeDown() {},
|
|
7
|
+
execute() {},
|
|
9
8
|
};
|
|
10
9
|
|
|
11
10
|
Deno.test("defineMigration", async ({ step }) => {
|
|
@@ -84,7 +83,7 @@ Deno.test("Migrator", async ({ step }) => {
|
|
|
84
83
|
{ name: "b", up: () => result.push(2) },
|
|
85
84
|
{ name: "c", up: () => result.push(3) },
|
|
86
85
|
],
|
|
87
|
-
|
|
86
|
+
execute: (def, dir) => def[dir](),
|
|
88
87
|
});
|
|
89
88
|
|
|
90
89
|
await migrator.up();
|
|
@@ -105,7 +104,7 @@ Deno.test("Migrator", async ({ step }) => {
|
|
|
105
104
|
{ name: "c", down: () => result.push(3) },
|
|
106
105
|
],
|
|
107
106
|
getRecords: () => [{ name: "a" }, { name: "b" }, { name: "c" }],
|
|
108
|
-
|
|
107
|
+
execute: (def, dir) => def[dir](),
|
|
109
108
|
});
|
|
110
109
|
|
|
111
110
|
await migrator.down();
|
package/core/types.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/// <reference types="urlpattern-polyfill" />
|
|
2
|
+
|
|
3
|
+
export type HTTPMethod =
|
|
4
|
+
| "GET"
|
|
5
|
+
| "HEAD"
|
|
6
|
+
| "POST"
|
|
7
|
+
| "PUT"
|
|
8
|
+
| "PATCH"
|
|
9
|
+
| "DELETE"
|
|
10
|
+
| "CONNECT";
|
|
11
|
+
|
|
12
|
+
export type RouteResult = Promise<Response | undefined> | Response | undefined;
|
|
13
|
+
|
|
14
|
+
export type ExtractRouteParams<T extends string> =
|
|
15
|
+
T extends `${string}:${infer Param}/${infer Rest}`
|
|
16
|
+
? Param | ExtractRouteParams<Rest>
|
|
17
|
+
: T extends `${string}:${infer Param}`
|
|
18
|
+
? Param
|
|
19
|
+
: never;
|
|
20
|
+
|
|
21
|
+
export interface RouteContext<T> {
|
|
22
|
+
request: Request;
|
|
23
|
+
params: T;
|
|
24
|
+
url: URL;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RouteHandler<T> {
|
|
28
|
+
(context: RouteContext<T>): RouteResult;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RouteOptions<T extends string> {
|
|
32
|
+
method: HTTPMethod;
|
|
33
|
+
pathname: T;
|
|
34
|
+
handler: RouteHandler<Record<ExtractRouteParams<T>, string>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RouteDefinition<T> {
|
|
38
|
+
method: HTTPMethod;
|
|
39
|
+
pattern: URLPattern;
|
|
40
|
+
handler: RouteHandler<T>;
|
|
41
|
+
}
|
package/core/utilities.js
CHANGED
|
@@ -16,7 +16,9 @@ export function formatMarkdownTable(fields, columns, fallback) {
|
|
|
16
16
|
|
|
17
17
|
const lines = [
|
|
18
18
|
// Header
|
|
19
|
-
"| " +
|
|
19
|
+
"| " +
|
|
20
|
+
columns.map((n, i) => n.toString().padEnd(widths[i]), " ").join(" | ") +
|
|
21
|
+
" |",
|
|
20
22
|
|
|
21
23
|
// Seperator
|
|
22
24
|
"| " + columns.map((_, i) => "=".padEnd(widths[i], "=")).join(" | ") + " |",
|
|
@@ -26,7 +28,9 @@ export function formatMarkdownTable(fields, columns, fallback) {
|
|
|
26
28
|
(field) =>
|
|
27
29
|
"| " +
|
|
28
30
|
columns
|
|
29
|
-
.map((n, i) =>
|
|
31
|
+
.map((n, i) =>
|
|
32
|
+
(field[n] ?? fallback).toString().padEnd(widths[i], " "),
|
|
33
|
+
)
|
|
30
34
|
.join(" | ") +
|
|
31
35
|
" |",
|
|
32
36
|
),
|
|
@@ -35,10 +39,12 @@ export function formatMarkdownTable(fields, columns, fallback) {
|
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
|
-
* @template
|
|
42
|
+
* @template {() => any} T
|
|
43
|
+
* @param {T} handler
|
|
44
|
+
* @returns {T}
|
|
39
45
|
*/
|
|
40
46
|
export function loader(handler) {
|
|
41
|
-
/** @type {T | null} */
|
|
47
|
+
/** @type {ReturnType<T> | null} */
|
|
42
48
|
let result = null;
|
|
43
49
|
return () => {
|
|
44
50
|
if (result === null) result = handler();
|