bun-router 0.5.8 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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();