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.
Files changed (58) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +119 -1
  3. package/core/configuration.js +30 -25
  4. package/core/fetch-router.js +36 -23
  5. package/core/fetch-router.test.js +50 -22
  6. package/core/http.js +20 -21
  7. package/core/migrator.test.js +3 -4
  8. package/core/types.ts +41 -0
  9. package/core/utilities.js +10 -4
  10. package/package.json +12 -1
  11. package/source/node-router.js +8 -1
  12. package/source/package.json +9 -0
  13. package/source/postgres.js +18 -1
  14. package/tsconfig.json +17 -0
  15. package/types/core/configuration.d.ts +57 -0
  16. package/types/core/configuration.d.ts.map +1 -0
  17. package/types/core/configuration.test.d.ts +2 -0
  18. package/types/core/configuration.test.d.ts.map +1 -0
  19. package/types/core/fetch-router.d.ts +52 -0
  20. package/types/core/fetch-router.d.ts.map +1 -0
  21. package/types/core/fetch-router.test.d.ts +2 -0
  22. package/types/core/fetch-router.test.d.ts.map +1 -0
  23. package/types/core/http.d.ts +55 -0
  24. package/types/core/http.d.ts.map +1 -0
  25. package/types/core/http.test.d.ts +2 -0
  26. package/types/core/http.test.d.ts.map +1 -0
  27. package/types/core/migrator.d.ts +56 -0
  28. package/types/core/migrator.d.ts.map +1 -0
  29. package/types/core/migrator.test.d.ts +2 -0
  30. package/types/core/migrator.test.d.ts.map +1 -0
  31. package/types/core/mod.d.ts +7 -0
  32. package/types/core/mod.d.ts.map +1 -0
  33. package/types/core/postgres.d.ts +43 -0
  34. package/types/core/postgres.d.ts.map +1 -0
  35. package/types/core/test-deps.d.ts +2 -0
  36. package/types/core/test-deps.d.ts.map +1 -0
  37. package/types/core/types.d.ts +23 -0
  38. package/types/core/types.d.ts.map +1 -0
  39. package/types/core/utilities.d.ts +13 -0
  40. package/types/core/utilities.d.ts.map +1 -0
  41. package/types/core/utilities.test.d.ts +2 -0
  42. package/types/core/utilities.test.d.ts.map +1 -0
  43. package/types/source/configuration.d.ts +24 -0
  44. package/types/source/configuration.d.ts.map +1 -0
  45. package/types/source/core.d.ts +2 -0
  46. package/types/source/core.d.ts.map +1 -0
  47. package/types/source/express-router.d.ts +28 -0
  48. package/types/source/express-router.d.ts.map +1 -0
  49. package/types/source/koa-router.d.ts +33 -0
  50. package/types/source/koa-router.d.ts.map +1 -0
  51. package/types/source/mod.d.ts +7 -0
  52. package/types/source/mod.d.ts.map +1 -0
  53. package/types/source/node-router.d.ts +42 -0
  54. package/types/source/node-router.d.ts.map +1 -0
  55. package/types/source/polyfill.d.ts +2 -0
  56. package/types/source/polyfill.d.ts.map +1 -0
  57. package/types/source/postgres.d.ts +32 -0
  58. 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
@@ -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
- /** @template T @param {T} spec */
44
+ /**
45
+ * @template {ObjectSchema} T
46
+ * @param {T} spec
47
+ */
43
48
  object(spec) {
44
- return Object.assign(
45
- this.options.superstruct.defaulted(
46
- this.options.superstruct.object(spec),
47
- {},
48
- ),
49
- { [Configuration.spec]: { type: "object", value: spec } },
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
- return Object.assign(
61
- this.options.superstruct.defaulted(
62
- this.options.superstruct.string(),
63
- this._getValue(spec),
64
- ),
65
- { [Configuration.spec]: { type: "string", value: spec } },
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
- return Object.assign(
77
- this.options.superstruct.defaulted(
78
- this.options.superstruct.coerce(
79
- this.options.superstruct.instance(URL),
80
- this.options.superstruct.string(),
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
- { [Configuration.spec]: { type: "url", value: spec } },
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);
@@ -1,6 +1,8 @@
1
- /**
2
- * @typedef {import("./http.js").RouteDefinition} RouteDefinition
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
- import { HTTPError } from "./http.js";
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 RouteDefinions based on URLPattern */
20
+ /** A rudimentary HTTP router using fetch Request & Responses with RouteDefinitions based on URLPattern */
15
21
  export class FetchRouter {
16
- routes /** @type {RouteDefinition} */;
22
+ /** @type {RouteDefinition} */ routes;
23
+ /** @type {RouteErrorHandler | null} */ errorHandler;
17
24
 
18
- /** @param {RouteDefinition[]} routes */
19
- constructor(routes) {
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
- return this.routes
31
- .filter((route) => route.method === request.method)
32
- .map((route) => {
33
- const url = new URL(request.url);
34
- const result = route.pattern.exec(request.url);
35
- return { result, route, url };
36
- })
37
- .filter((item) => item.result);
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
- handleError(error) {
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
- // NOTE: could emit the error
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 route = defineRoute({
8
- method: "GET",
9
- pathname: "/",
10
- handler: () => new Response("OK"),
11
- });
12
- const result = new FetchRouter([route]);
13
- assertEquals(result.routes, [route], "should store the routes");
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 router = new FetchRouter([
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 = router.findMatchingRoutes(
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 = router.findMatchingRoutes(
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 = router.findMatchingRoutes(
47
- new Request("http://localhost/hello/Geoff", { method: "POST" }),
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(new Error());
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(new HTTPError(400, "Bad Request"));
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 router = new FetchRouter([
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
- * @typedef {"GET"|"HEAD"|"POST"|"PUT"|"PATCH"|"DELETE"|"CONNECT"} HTTPMethod
3
- */
5
+ * @template {string} T
6
+ * @typedef {import("./types.ts").ExtractRouteParams<T>} ExtractRouteParams */
4
7
 
5
8
  /**
6
- * @typedef {object} RouteContext
7
- * @property {Request} request
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
- * @typedef {(context: RouteContext) => Response | Promise<Response>} RouteHandler
14
+ * @template T
15
+ * @typedef {import("./types.ts").RouteHandler<T>} RouteHandler
14
16
  */
15
17
 
16
18
  /**
17
- * @typedef {object} RouteOptions
18
- * @property {HTTPMethod} method
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
- * @typedef {object} RouteDefinition
25
- * @property {HTTPMethod} method
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
- * @param {RouteOptions} options
32
- * @returns {RouteDefinition}
29
+ * @template {string} T
30
+ * @param {RouteOptions<T>} init
31
+ * @returns {RouteDefinition<T>}
33
32
  */
34
- export function defineRoute(options) {
33
+ export function defineRoute(init) {
35
34
  return {
36
- method: options.method,
37
- pattern: new URLPattern({ pathname: options.pathname }),
38
- handler: options.handler,
35
+ method: init.method,
36
+ pattern: new URLPattern({ pathname: init.pathname }),
37
+ handler: init.handler,
39
38
  };
40
39
  }
41
40
 
@@ -4,8 +4,7 @@ import { assertEquals } from "./test-deps.js";
4
4
  const bareOptions = {
5
5
  getDefinitions: () => [],
6
6
  getRecords: () => [],
7
- executeUp() {},
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
- executeUp: (_, fn) => fn(),
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
- executeDown: (_, fn) => fn(),
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
- "| " + columns.map((n, i) => n.padEnd(widths[i]), " ").join(" | ") + " |",
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) => (field[n] ?? fallback).padEnd(widths[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 T @param {() => T} handler
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();