bun-router 0.5.8 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,7 +56,7 @@ r.serve();
56
56
 
57
57
  **SQLite**
58
58
  ```ts
59
- import { router, json } from '..';
59
+ import { router, json } from 'bun-router';
60
60
  import { Database } from 'bun:sqlite';
61
61
 
62
62
  const r = router(3000, {db: './examples/dbs/test.db'});
package/examples/basic.ts CHANGED
@@ -2,17 +2,12 @@ import { router, http } from '..';
2
2
 
3
3
  const r = router();
4
4
 
5
- r.add('/', 'GET', () => http.json('ok'));
5
+ r.add('/', 'GET', () => http.json(200, 'ok'));
6
6
 
7
7
  r.add('/user/:name', 'GET', (ctx) => {
8
8
  const name = ctx.params.get('name');
9
- return http.json(name);
10
- });
11
-
12
- r.add('/user/:name/:id', 'GET', (ctx) => {
13
- const name = ctx.params.get('name');
14
- const id = ctx.params.get('id');
15
- return http.json({name: name, id: id});
9
+ if (!name) return http.json(500, 'no name');
10
+ return http.json(200, name);
16
11
  });
17
12
 
18
13
  r.serve();
@@ -0,0 +1,15 @@
1
+ import { router } from '..';
2
+
3
+ const r = router();
4
+
5
+ r.add('/set-cookie', 'GET', ctx => {
6
+ ctx.cookies.set('domain', 'localhost');
7
+ ctx.cookies.set('path', '/set-cookie');
8
+
9
+ ctx.logger.message(ctx.token ?? 'no token provided');
10
+
11
+ return ctx.json( 200, {message: 'cookie stored'});
12
+ });
13
+
14
+
15
+ r.serve();
@@ -1,29 +1,24 @@
1
1
  import { router, http } from '..';
2
2
  import { Context } from '../lib/router/router.d';
3
3
 
4
- const handler = (ctx: Context) => {
5
- const name = ctx.params.get('name');
6
- if (typeof name === 'undefined' || name === '') return http.html('<h6 style="color: red">User Undefined</h6>');
7
- return http.html(`<h4>Hello, ${name}!</h4>`);
8
- }
4
+ const home = (ctx: Context) => new Response('Welcome Home', { status: 200 });
9
5
 
10
- const space = (ctx: Context) => {
11
- const name = ctx.params.get('name');
12
- if (typeof name === 'undefined' || name === '') return http.html(`<h6 style="color: red">Space [${name}] Not Found</h6>`);
13
- return http.html(`<h4>Welcome to ${name}!`)
6
+ const subreddit = (ctx: Context) => {
7
+ const sub = ctx.params.get('subreddit');
8
+ if (!sub) return http.json(400, { error: 'no subreddit provided' });
9
+ return http.json(200, { subreddit: sub });
14
10
  }
15
11
 
16
- const handleSettings = (ctx: Context) => {
17
- const name = ctx.params.get('name');
18
- if (typeof name === 'undefined' || name === '') return http.html(`<h6 style="color: red">User Not Found</h6>`);
19
- return http.html(`<h4>Settings for ${name}</h4>`)
12
+ const user = (ctx: Context) => {
13
+ const user = ctx.params.get('user');
14
+ if (!user) return http.json(400, { error: 'no user provided' });
15
+ return http.json(200, { user: user });
20
16
  }
21
17
 
22
18
  const r = router();
23
19
 
24
- r.add('/u/:name', 'GET', handler);
25
- r.add('/s/:name', 'GET', space);
26
- r.add('/u/:name/settings', 'GET', handleSettings);
27
-
20
+ r.add('/', 'GET', home);
21
+ r.add('/r/:subreddit', 'GET', subreddit);
22
+ r.add('/u/:user', 'GET', user);
28
23
 
29
24
  r.serve();
@@ -8,9 +8,9 @@ r.add('/:foo', 'GET', (ctx) => {
8
8
  const foo = ctx.params.get('foo');
9
9
  if (!foo) {
10
10
  log.error(500, url.pathname, ctx.request.method, new Error('undefined'));
11
- return http.json({status: 500, text: 'Foo is undefined'});
11
+ return http.json(500,{text: 'Foo is undefined'});
12
12
  }
13
- return http.html(`<h4 style='font-family: sans-serif;'>Oh hello, ${foo}</h4>`)
13
+ return http.html(200, `<h4 style='font-family: sans-serif;'>Oh hello, ${foo}</h4>`)
14
14
  });
15
15
 
16
16
  r.serve();
package/examples/todo.ts CHANGED
@@ -26,7 +26,7 @@ r.add('/api/new', 'POST', ctx => {
26
26
  ctx.logger.message(`Adding ${key} with ${content}`);
27
27
  todo.add(key, content);
28
28
 
29
- return ctx.json({ message: 'ok' });
29
+ return ctx.json(200, { message: 'ok' });
30
30
  });
31
31
 
32
32
  r.add('/api/todo/:key', 'GET', ctx => {
@@ -36,11 +36,11 @@ r.add('/api/todo/:key', 'GET', ctx => {
36
36
  const content = todo.get(key);
37
37
  if (!content) return http.notFound();
38
38
 
39
- return ctx.json({key: key, content: content});
39
+ return ctx.json(200, {key: key, content: content});
40
40
  });
41
41
 
42
42
  r.add('/api/get/all', 'GET', ctx => {
43
- return ctx.json(todo.export());
43
+ return ctx.json(200, todo.export());
44
44
  });
45
45
 
46
46
  r.serve();
@@ -1,23 +1,22 @@
1
1
  import { httpStatusCodes } from "./status";
2
2
 
3
3
  const http = {
4
- json: async (data: any): Promise<Response> => {
4
+ json: async (statusCode: number, data: any): Promise<Response> => {
5
5
  const jsonString = JSON.stringify(data);
6
6
  return Promise.resolve(new Response(jsonString, {
7
- status: 200,
8
- statusText: httpStatusCodes[200],
7
+ status: statusCode,
8
+ statusText: httpStatusCodes[statusCode],
9
9
  headers: {'Content-Type': 'application/json'},
10
10
  }));
11
11
  },
12
- html: async (content: string): Promise<Response> => {
13
- content = Bun.escapeHTML(content);
14
- return Promise.resolve(new Response(Bun.escapeHTML(content), {
15
- status: 200,
16
- statusText: httpStatusCodes[200],
12
+ html: async (statusCode: number, content: string): Promise<Response> => {
13
+ return Promise.resolve(new Response(content, {
14
+ status: statusCode,
15
+ statusText: httpStatusCodes[statusCode],
17
16
  headers: {'Content-Type': 'text/html; charset=utf-8'}
18
17
  }));
19
18
  },
20
- file: async (fp: string): Promise<Response> => {
19
+ file: async (statusCode: number, fp: string): Promise<Response> => {
21
20
  const file = Bun.file(fp);
22
21
  const exists = await file.exists();
23
22
 
@@ -32,8 +31,8 @@ const http = {
32
31
  contentType = file.type + '; charset=utf-8';
33
32
 
34
33
  return Promise.resolve(new Response(content, {
35
- status: 200,
36
- statusText: httpStatusCodes[200],
34
+ status: statusCode,
35
+ statusText: httpStatusCodes[statusCode],
37
36
  headers: { 'Content-Type': contentType}
38
37
  }));
39
38
  },
@@ -50,6 +49,15 @@ const http = {
50
49
 
51
50
  return Promise.resolve(response);
52
51
  },
52
+ methodNotAllowed: async (msg?: string): Promise<Response> => {
53
+ const response = new Response(msg ?? 'method not allowed', {
54
+ status: 405,
55
+ statusText: httpStatusCodes[405],
56
+ headers: {'Content-Type': 'text/html'},
57
+ });
58
+
59
+ return Promise.resolve(response);
60
+ },
53
61
  message: async (status: number, msg?: string): Promise<Response> => {
54
62
  const response = new Response(msg ?? '?', {
55
63
  status: status,
@@ -57,8 +65,7 @@ const http = {
57
65
  headers: {'Content-Type': 'text/html; charset-utf-8'},
58
66
  });
59
67
  return Promise.resolve(response)
60
- }
61
-
68
+ },
62
69
  }
63
70
 
64
71
  export { http }
@@ -20,7 +20,7 @@ const Colors: Record<string,string> = {
20
20
  bgMagenta: "\x1b[45m",
21
21
  bgCyan: "\x1b[46m",
22
22
  bgWhite: "\x1b[47m",
23
- };
23
+ } as const;
24
24
 
25
25
 
26
26
  const color = (c: string, bkg: string, msg: string) => {
@@ -3,6 +3,14 @@ import {Logger} from './logger.d';
3
3
 
4
4
  const pad = (n: number) => String(n).padStart(2, '0');
5
5
 
6
+ const TITLE = `
7
+ _ _
8
+ | |_ _ _ ___ ___ ___ _ _| |_ ___ ___
9
+ | . | | | | | _| . | | | _| -_| _|
10
+ |___|___|_|_| |_| |___|___|_| |___|_|
11
+
12
+ `
13
+
6
14
  const timestamp = (date: Date) => {
7
15
  const month = pad(date.getMonth());
8
16
  const day = pad(date.getDate());
@@ -48,6 +56,7 @@ const logger = (): Logger => {
48
56
  const portColor = color('green', 'bgBlack', String(port));
49
57
  const msg = `${source}: Starting Server on :${portColor}\n`;
50
58
 
59
+ await Bun.write(Bun.stdout, TITLE);
51
60
  await Bun.write(Bun.stdout, msg);
52
61
  },
53
62
  info: async (statusCode: number, routePath: string, method: string, message?: string) => {
@@ -2,21 +2,28 @@ import { TLSOptions, TLSWebSocketServeOptions, WebSocketServeOptions, ServeOptio
2
2
  import { Logger } from '../logger/logger';
3
3
  import { Database } from 'bun:sqlite';
4
4
 
5
+ type HttpHandler = (ctx: Context) => Response | Promise<Response>
5
6
 
6
7
  type Context = {
7
- request: Request,
8
- route: Route,
9
- params: Map<string, string>,
10
- token?: string,
11
- db: Database,
12
- logger: Logger,
13
- json: (data: any) => Response | Promise<Response>,
14
- }
8
+ cookies: Map<string, string>;
9
+ db?: Database;
10
+ formData: FormData | Promise<FormData> | undefined;
11
+ json: (statusCode: number, data: any) => Response | Promise<Response>;
12
+ logger: Logger;
13
+ params: Map<string, string>;
14
+ query: URLSearchParams;
15
+ request: Request;
16
+ token?: string;
17
+ };
18
+
15
19
 
16
20
  type Route = {
17
- pattern: string,
18
- method: string,
19
- callback: (req: Context) => Response | Promise<Response>
21
+ children: Map<string, Route>;
22
+ path: string;
23
+ dynamicPath: string;
24
+ method: string;
25
+ handler: HttpHandler;
26
+ isLast: boolean;
20
27
  }
21
28
 
22
29
  type Options = {
@@ -31,12 +38,11 @@ type RouterOptions<Options> = ServeOptions
31
38
 
32
39
 
33
40
  type Router = (port?: number | string, options?: RouterOptions) => {
34
- add: (pattern: string, method: string, callback: (req: Context) => Response | Promise<Response>) => void,
35
- GET: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => void,
36
- POST: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => void,
37
- static: (pattern: string, root: string) => void,
38
- serve: () => void,
41
+ add: (pattern: string, method: string, callback: (req: Context) => Response | Promise<Response>) => void;
42
+ static: (pattern: string, root: string) => void;
43
+ serve: () => void;
39
44
  }
40
45
 
41
46
 
42
- export { Context , Route, Router, RouterOptions, Options }
47
+
48
+ export { Context , Route, Router, RouterOptions, Options, HttpHandler }
@@ -1,17 +1,15 @@
1
1
  import path from 'path';
2
2
  import { Database } from 'bun:sqlite';
3
- import { Route, Router, Context, RouterOptions, Options } from './router.d';
3
+ import { Route, Router, Context, RouterOptions, Options, HttpHandler } from './router.d';
4
4
  import { httpStatusCodes } from '../http/status';
5
5
  import { readDir } from '../fs/fsys';
6
6
  import { logger } from '../logger/logger';
7
- import { Logger } from '../logger/logger.d';
8
7
  import { http } from '../http/generic-methods';
8
+ import {Radix, createContext} from './tree';
9
9
 
10
- // extract dynamic URL parameters
11
- // if the route pattern is /:foo and the request URL is /bar: {foo: 'bar'}
12
- const extract = (route: Route, ctx: Context) => {
10
+ const extract = (path: string, ctx: Context) => {
13
11
  const url = new URL(ctx.request.url);
14
- const pathSegments = route.pattern.split('/');
12
+ const pathSegments = path.split('/');
15
13
  const urlSegments = url.pathname.split('/');
16
14
 
17
15
  if (pathSegments.length !== urlSegments.length) return
@@ -19,10 +17,10 @@ const extract = (route: Route, ctx: Context) => {
19
17
  return {
20
18
  params: () => {
21
19
  for (let i = 0; i < pathSegments.length; i++) {
22
- if ((pathSegments[i][0] === ':')) {
20
+ if((pathSegments[i][0] === ':')) {
23
21
  const k = pathSegments[i].replace(':', '');
24
22
  const v = urlSegments[i];
25
- ctx.params.set(k, v);
23
+ ctx.params.set(k,v);
26
24
  }
27
25
  }
28
26
  }
@@ -30,63 +28,16 @@ const extract = (route: Route, ctx: Context) => {
30
28
 
31
29
  }
32
30
 
33
- // ensure the route pattern matches the request URL
34
- const match = (route: Route, ctx: Context): boolean => {
35
- const url = new URL(ctx.request.url);
36
- const patternRegex = new RegExp('^' + route.pattern.replace(/:[^/]+/g, '([^/]+)') + '$');
37
- const matches = url.pathname.match(patternRegex);
38
-
39
- if (matches && route.method === ctx.request.method) {
40
- const extractor = extract(route, ctx);
41
- extractor?.params();
42
-
43
- return true;
44
- }
45
-
46
- return false;
47
- }
48
-
49
- // set the context for the reuest
50
- const setContext = (req: Request, lgr: Logger, opts: Options, route: Route): Context => {
51
- return {
52
- request: req,
53
- params: new Map(),
54
- db: new Database(opts.db ?? ':memory:'),
55
- logger: lgr,
56
- route: route,
57
- json: (data: any) => http.json(data),
58
- }
59
- }
60
-
61
31
  const router: Router = (port?: number | string, options?: RouterOptions<Options>) => {
62
- const routes: Array<Route> = new Array();
32
+ const {addRoute, findRoute} = Radix();
63
33
  const lgr = logger();
64
- let dbConn = '';
65
34
 
66
35
  return {
67
- // add a new route
68
- add: (pattern: string, method: string, callback: (ctx: Context) => Response | Promise<Response>) => {
69
- routes.push({
70
- pattern: pattern,
71
- method: method,
72
- callback: callback,
73
- })
36
+ // add a route to the router tree
37
+ add: (pattern: string, method: string, callback: HttpHandler) => {
38
+ addRoute(pattern, method, callback);
74
39
  },
75
- GET: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => {
76
- routes.push({
77
- pattern: pattern,
78
- method: 'GET',
79
- callback: callback,
80
- });
81
- },
82
- POST: (pattern: string, callback: (ctx: Context) => Response | Promise<Response>) => {
83
- routes.push({
84
- pattern: pattern,
85
- method: 'POST',
86
- callback: callback,
87
- });
88
- },
89
- // add a route for static files
40
+ // add a static route to the router tree
90
41
  static: async (pattern: string, root: string) => {
91
42
  await readDir(root, async (fp, _) => {
92
43
  const pure = path.join('.', fp);
@@ -103,12 +54,15 @@ const router: Router = (port?: number | string, options?: RouterOptions<Options>
103
54
  if (base === 'index') patternPath = pattern;
104
55
 
105
56
  const route: Route = {
106
- pattern: patternPath,
57
+ children: new Map(),
58
+ dynamicPath: '',
59
+ isLast: true,
60
+ path: patternPath,
107
61
  method: 'GET',
108
- callback: async () => await http.file(pure),
62
+ handler: async () => await http.file(200, pure),
109
63
  };
110
64
 
111
- routes.push(route);
65
+ addRoute(route.path, 'GET', route.handler);
112
66
  });
113
67
  },
114
68
  // start the server
@@ -116,41 +70,44 @@ const router: Router = (port?: number | string, options?: RouterOptions<Options>
116
70
  lgr.start(port ?? 3000);
117
71
  let opts: Options = { db: ':memory:' };
118
72
 
73
+ // TODO: add support for TLS and WebSockets
119
74
  Bun.serve({
120
75
  port: port ?? 3000,
121
76
  ...options,
122
77
  async fetch(req) {
123
78
  const url = new URL(req.url);
79
+ let path = url.pathname;
124
80
 
81
+ // set the database
125
82
  if (options) {
126
83
  let o = options as Options;
127
84
  opts.db = o.db;
128
85
  }
129
86
 
130
- let statusCode = 404;
131
-
132
- for (const route of routes) {
133
- const ctx = setContext(req, lgr, opts, route);
134
-
135
- if (match(route, ctx) || route.pattern === url.pathname) {
136
- if (route.method === ctx.request.method) {
137
- const res = await route.callback(ctx);
138
- statusCode = res.status;
139
- lgr.info(res.status, route.pattern, req.method, httpStatusCodes[res.status]);
140
- return Promise.resolve(res);
141
- } else {
142
- const res = new Response(httpStatusCodes[405], {
143
- status: 405,
144
- statusText: httpStatusCodes[305]
145
- });
146
- lgr.info(405, route.pattern, req.method, httpStatusCodes[405])
147
- return Promise.resolve(res);
148
- }
149
- }
150
- }
87
+ const route = findRoute(path);
88
+
89
+ // if the route exists, execute the handler
90
+ if (route) {
91
+ if (route.method !== req.method) {
92
+ lgr.info(405, url.pathname, req.method, httpStatusCodes[405]);
93
+ return Promise.resolve(http.methodNotAllowed());
94
+ }
95
+
96
+ const context = createContext(path, route, req);
97
+ context.db = new Database(opts.db);
98
+
99
+ const response = await route.handler(context);
100
+
101
+ lgr.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
102
+ return Promise.resolve(response);
103
+ }
104
+
105
+ // if no route is found, return 404
106
+ const response = await http.notFound();
107
+
108
+ lgr.info(response.status, url.pathname, req.method, httpStatusCodes[response.status]);
109
+ return Promise.resolve(http.notFound());
151
110
 
152
- lgr.info(statusCode, url.pathname, req.method, httpStatusCodes[statusCode]);
153
- return Promise.resolve(http.message(statusCode, httpStatusCodes[statusCode]));
154
111
  }
155
112
  });
156
113
  },
@@ -0,0 +1,90 @@
1
+ import { HttpHandler, Context, Route } from "./router.d";
2
+ import { http } from "../http/generic-methods";
3
+
4
+ const splitPath = (s: string): string[] => s.split('/').filter(x => x !== '');
5
+
6
+ const createRoute = (path: string, method: string, handler: HttpHandler): Route => {
7
+ const route: Route = {
8
+ children: new Map(),
9
+ path: path,
10
+ dynamicPath: '',
11
+ method: method,
12
+ handler: handler,
13
+ isLast: false
14
+ };
15
+
16
+ return route;
17
+ };
18
+
19
+ const extractParams = (path: string, route: Route, params: Map<string, string>) => {
20
+ const pathParts = splitPath(path);
21
+ const routeParts = splitPath(route.path);
22
+
23
+ for (let i = 0; i < routeParts.length; i++) {
24
+ const part = routeParts[i];
25
+ if (part.startsWith(':')) {
26
+ params.set(part.slice(1), pathParts[i]);
27
+ }
28
+ }
29
+ };
30
+
31
+ const createContext = (path: string, route: Route, req: Request): Context => {
32
+ const params: Map<string, string> = new Map();
33
+
34
+ if (route) extractParams(path, route, params);
35
+
36
+ return {
37
+ params: params,
38
+ request: req,
39
+ query: new URLSearchParams(path),
40
+ cookies: new Map(),
41
+ formData: undefined,
42
+ logger: undefined,
43
+ json: (statusCode: number, data: any) => http.json(statusCode, data),
44
+ }
45
+ };
46
+
47
+ const Radix = () => {
48
+ let root = createRoute('', 'GET', () => http.notFound());
49
+
50
+ const addRoute = (path: string, method: string, handler: HttpHandler) => {
51
+ const pathParts = splitPath(path);
52
+ let current = root;
53
+
54
+ for (let i = 0; i < pathParts.length; i++) {
55
+ const part = pathParts[i];
56
+ if (part.startsWith(':')) {
57
+ current.dynamicPath = part;
58
+ }
59
+ if (!current.children.has(part)) {
60
+ current.children.set(part, createRoute(part, method, handler));
61
+ }
62
+ current = current.children.get(part)!;
63
+ }
64
+
65
+ current.handler = handler;
66
+ current.isLast = true;
67
+ current.path = path;
68
+ };
69
+
70
+ const findRoute = (path: string): Route | undefined => {
71
+ const pathParts = splitPath(path);
72
+ let current = root;
73
+ for (let i = 0; i < pathParts.length; i++) {
74
+ const part = pathParts[i];
75
+ if (current.children.has(part)) {
76
+ current = current.children.get(part)!;
77
+ } else if (current.dynamicPath) {
78
+ current = current.children.get(current.dynamicPath)!;
79
+ } else {
80
+ return;
81
+ }
82
+ }
83
+ return current;
84
+ }
85
+
86
+ return { addRoute, findRoute }
87
+
88
+ };
89
+
90
+ export { Radix, createContext }
package/package.json CHANGED
@@ -8,5 +8,5 @@
8
8
  "peerDependencies": {
9
9
  "typescript": "^5.0.0"
10
10
  },
11
- "version": "0.5.8"
11
+ "version": "0.7.0"
12
12
  }
@@ -1,68 +1,4 @@
1
1
  import { describe, test, expect } from 'bun:test';
2
- import { extract } from '..';
3
- import { Context, Route } from '../lib/router/router.d';
4
-
5
- describe('URL Params', () => {
6
- test('/user/:name', () => {
7
- const route: Route = {
8
- pattern: '/user/:name',
9
- method: 'GET',
10
- callback: () => new Response('ok'),
11
- };
12
-
13
- const ctx: Context = {
14
- request: new Request('http://localhost:3000/user/foo'),
15
- params: new Map(),
16
- };
17
-
18
- const extractor = extract(route, ctx);
19
-
20
- extractor?.params();
21
-
22
- const name = ctx.params.get('name');
23
- expect(name).toBe('foo');
24
- });
25
-
26
- test('/user/:name/:id', () => {
27
- const route: Route = {
28
- pattern: '/user/:name/:id',
29
- method: 'GET',
30
- callback: () => new Response('ok'),
31
- };
32
-
33
- const ctx: Context = {
34
- request: new Request('http://localhost:3000/user/foo/123'),
35
- params: new Map(),
36
- };
37
-
38
- const extractor = extract(route, ctx);
39
-
40
- extractor?.params();
41
-
42
- const name = ctx.params.get('name');
43
- const id = ctx.params.get('id');
44
-
45
- expect(name).toBe('foo');
46
- expect(id).toBe('123');
47
- });
48
-
49
- test('/foo', () => {
50
- const route: Route = {
51
- pattern: '/foo',
52
- method: 'GET',
53
- callback: () => new Response('ok'),
54
- }
55
-
56
- const ctx: Context = {
57
- request: new Request('http://localhost:3000/foo'),
58
- params: new Map(),
59
- }
60
-
61
- const url = new URL(ctx.request.url);
62
-
63
- expect(url.pathname).toBe(route.pattern);
64
- });
65
- });
66
2
 
67
3
  describe('Router', () => {
68
4
  test('Serve', async () => {
@@ -1,22 +0,0 @@
1
- import { router, http } from '..';
2
-
3
- const r = router(3000, {db: './examples/dbs/test.db'});
4
-
5
- r.add('/u/new/:name', 'GET', (ctx) => {
6
- const name = ctx.params.get('name');
7
- const rando = Math.floor(Math.random()*1000);
8
-
9
- ctx.db.run(`INSERT INTO test VALUES(${rando}, "${name}")`);
10
-
11
- return http.json({message: 'ok'});
12
- });
13
-
14
- r.add('/u/:name', 'GET', (ctx) => {
15
- const name = ctx.params.get('name');
16
- const data = ctx.db.query(`SELECT * FROM test WHERE name = "${name}";`).get();
17
- const d = data as {id: number, name: string};
18
-
19
- return d ? http.json(d) : new Response('not found', {status: 404});
20
- });
21
-
22
- r.serve();