@di-framework/di-framework-http 0.0.0-prerelease.308 → 0.0.0-prerelease.310

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.
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from "bun:test";
1
+ import { describe, it, expect } from 'bun:test';
2
2
  import {
3
3
  TypedRouter,
4
4
  json,
@@ -7,119 +7,114 @@ import {
7
7
  type ResponseSpec,
8
8
  type PathParams,
9
9
  type QueryParams,
10
- } from "./typed-router.ts";
10
+ } from './typed-router.ts';
11
11
 
12
- describe("TypedRouter", () => {
13
- it("should handle GET requests", async () => {
12
+ describe('TypedRouter', () => {
13
+ it('should handle GET requests', async () => {
14
14
  const router = TypedRouter();
15
- router.get("/test", () => json({ ok: true }));
15
+ router.get('/test', () => json({ ok: true }));
16
16
 
17
- const req = new Request("http://localhost/test");
17
+ const req = new Request('http://localhost/test');
18
18
  const res = await router.fetch(req);
19
19
  expect(res.status).toBe(200);
20
20
  const body = await res.json();
21
21
  expect(body).toEqual({ ok: true });
22
22
  });
23
23
 
24
- it("should handle POST requests with JSON content-type", async () => {
24
+ it('should handle POST requests with JSON content-type', async () => {
25
25
  const router = TypedRouter();
26
- router.post("/test", (req) => json({ received: req.content }));
26
+ router.post('/test', (req) => json({ received: req.content }));
27
27
 
28
- const req = new Request("http://localhost/test", {
29
- method: "POST",
30
- headers: { "Content-Type": "application/json" },
31
- body: JSON.stringify({ hello: "world" }),
28
+ const req = new Request('http://localhost/test', {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/json' },
31
+ body: JSON.stringify({ hello: 'world' }),
32
32
  });
33
33
  const res = await router.fetch(req);
34
34
  expect(res.status).toBe(200);
35
35
  const body = await res.json();
36
- expect(body).toEqual({ received: { hello: "world" } });
36
+ expect(body).toEqual({ received: { hello: 'world' } });
37
37
  });
38
38
 
39
- it("should reject POST requests without application/json", async () => {
39
+ it('should reject POST requests without application/json', async () => {
40
40
  const router = TypedRouter();
41
- router.post("/test", () => json({ ok: true }));
41
+ router.post('/test', () => json({ ok: true }));
42
42
 
43
- const req = new Request("http://localhost/test", {
44
- method: "POST",
45
- headers: { "Content-Type": "text/plain" },
46
- body: "hello",
43
+ const req = new Request('http://localhost/test', {
44
+ method: 'POST',
45
+ headers: { 'Content-Type': 'text/plain' },
46
+ body: 'hello',
47
47
  });
48
48
  const res = await router.fetch(req);
49
49
  expect(res.status).toBe(415);
50
50
  const body = (await res.json()) as any;
51
- expect(body.error).toBe("Content-Type must be application/json");
51
+ expect(body.error).toBe('Content-Type must be application/json');
52
52
  });
53
53
 
54
- it("should attach path and method to the handler", () => {
54
+ it('should attach path and method to the handler', () => {
55
55
  const router = TypedRouter();
56
- const handler = router.get("/metadata", () => json({}));
56
+ const handler = router.get('/metadata', () => json({}));
57
57
 
58
- expect(handler.path).toBe("/metadata");
59
- expect(handler.method).toBe("get");
58
+ expect(handler.path).toBe('/metadata');
59
+ expect(handler.method).toBe('get');
60
60
  });
61
61
 
62
- it("should support extra arguments in fetch", async () => {
62
+ it('should support extra arguments in fetch', async () => {
63
63
  type Env = { BINDING: string };
64
64
  const router = TypedRouter<[Env]>();
65
- router.get("/env", (req, env) => json({ binding: env.BINDING }));
65
+ router.get('/env', (req, env) => json({ binding: env.BINDING }));
66
66
 
67
- const req = new Request("http://localhost/env");
68
- const res = await router.fetch(req, { BINDING: "value" });
67
+ const req = new Request('http://localhost/env');
68
+ const res = await router.fetch(req, { BINDING: 'value' });
69
69
  const body = await res.json();
70
- expect(body).toEqual({ binding: "value" });
70
+ expect(body).toEqual({ binding: 'value' });
71
71
  });
72
72
 
73
- it("should support other HTTP methods", async () => {
73
+ it('should support other HTTP methods', async () => {
74
74
  const router = TypedRouter();
75
- router.put("/put", () => json({ method: "PUT" }));
76
- router.delete("/delete", () => json({ method: "DELETE" }));
77
- router.patch("/patch", () => json({ method: "PATCH" }));
75
+ router.put('/put', () => json({ method: 'PUT' }));
76
+ router.delete('/delete', () => json({ method: 'DELETE' }));
77
+ router.patch('/patch', () => json({ method: 'PATCH' }));
78
78
 
79
79
  expect(
80
80
  (
81
81
  (await (
82
82
  await router.fetch(
83
- new Request("http://localhost/put", {
84
- method: "PUT",
85
- headers: { "Content-Type": "application/json" },
86
- body: "{}",
83
+ new Request('http://localhost/put', {
84
+ method: 'PUT',
85
+ headers: { 'Content-Type': 'application/json' },
86
+ body: '{}',
87
87
  }),
88
88
  )
89
89
  ).json()) as any
90
90
  ).method,
91
- ).toBe("PUT");
91
+ ).toBe('PUT');
92
92
  expect(
93
93
  (
94
94
  (await (
95
- await router.fetch(
96
- new Request("http://localhost/delete", { method: "DELETE" }),
97
- )
95
+ await router.fetch(new Request('http://localhost/delete', { method: 'DELETE' }))
98
96
  ).json()) as any
99
97
  ).method,
100
- ).toBe("DELETE");
98
+ ).toBe('DELETE');
101
99
  expect(
102
100
  (
103
101
  (await (
104
102
  await router.fetch(
105
- new Request("http://localhost/patch", {
106
- method: "PATCH",
107
- headers: { "Content-Type": "application/json" },
108
- body: "{}",
103
+ new Request('http://localhost/patch', {
104
+ method: 'PATCH',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: '{}',
109
107
  }),
110
108
  )
111
109
  ).json()) as any
112
110
  ).method,
113
- ).toBe("PATCH");
111
+ ).toBe('PATCH');
114
112
  });
115
113
 
116
- it("should handle multipart POST requests with { multipart: true }", async () => {
114
+ it('should handle multipart POST requests with { multipart: true }', async () => {
117
115
  const router = TypedRouter();
118
- router.post<
119
- RequestSpec<Multipart<{ file: File }>>,
120
- ResponseSpec<{ ok: boolean }>
121
- >(
122
- "/upload",
116
+ router.post<RequestSpec<Multipart<{ file: File }>>, ResponseSpec<{ ok: boolean }>>(
117
+ '/upload',
123
118
  (req) => {
124
119
  return json({ ok: req.content instanceof FormData });
125
120
  },
@@ -127,10 +122,10 @@ describe("TypedRouter", () => {
127
122
  );
128
123
 
129
124
  const formData = new FormData();
130
- formData.append("file", new Blob(["hello"]), "test.txt");
125
+ formData.append('file', new Blob(['hello']), 'test.txt');
131
126
 
132
- const req = new Request("http://localhost/upload", {
133
- method: "POST",
127
+ const req = new Request('http://localhost/upload', {
128
+ method: 'POST',
134
129
  body: formData,
135
130
  });
136
131
  const res = await router.fetch(req);
@@ -139,40 +134,40 @@ describe("TypedRouter", () => {
139
134
  expect(body.ok).toBe(true);
140
135
  });
141
136
 
142
- it("should parse FormData as req.content in multipart handlers", async () => {
137
+ it('should parse FormData as req.content in multipart handlers', async () => {
143
138
  const router = TypedRouter();
144
139
  router.post(
145
- "/upload",
140
+ '/upload',
146
141
  (req) => {
147
142
  const content = req.content as FormData;
148
- const name = content.get("name");
143
+ const name = content.get('name');
149
144
  return json({ name });
150
145
  },
151
146
  { multipart: true },
152
147
  );
153
148
 
154
149
  const formData = new FormData();
155
- formData.append("name", "test-file");
150
+ formData.append('name', 'test-file');
156
151
 
157
- const req = new Request("http://localhost/upload", {
158
- method: "POST",
152
+ const req = new Request('http://localhost/upload', {
153
+ method: 'POST',
159
154
  body: formData,
160
155
  });
161
156
  const res = await router.fetch(req);
162
157
  expect(res.status).toBe(200);
163
158
  const body = (await res.json()) as any;
164
- expect(body.name).toBe("test-file");
159
+ expect(body.name).toBe('test-file');
165
160
  });
166
161
 
167
- it("should not enforce JSON content-type on multipart routes", async () => {
162
+ it('should not enforce JSON content-type on multipart routes', async () => {
168
163
  const router = TypedRouter();
169
- router.post("/upload", () => json({ ok: true }), { multipart: true });
164
+ router.post('/upload', () => json({ ok: true }), { multipart: true });
170
165
 
171
166
  const formData = new FormData();
172
- formData.append("field", "value");
167
+ formData.append('field', 'value');
173
168
 
174
- const req = new Request("http://localhost/upload", {
175
- method: "POST",
169
+ const req = new Request('http://localhost/upload', {
170
+ method: 'POST',
176
171
  body: formData,
177
172
  });
178
173
  const res = await router.fetch(req);
@@ -180,37 +175,37 @@ describe("TypedRouter", () => {
180
175
  expect(res.status).toBe(200);
181
176
  });
182
177
 
183
- it("should still reject non-JSON on non-multipart POST routes (backward compat)", async () => {
178
+ it('should still reject non-JSON on non-multipart POST routes (backward compat)', async () => {
184
179
  const router = TypedRouter();
185
- router.post("/json-only", () => json({ ok: true }));
180
+ router.post('/json-only', () => json({ ok: true }));
186
181
 
187
182
  const formData = new FormData();
188
- formData.append("field", "value");
183
+ formData.append('field', 'value');
189
184
 
190
- const req = new Request("http://localhost/json-only", {
191
- method: "POST",
185
+ const req = new Request('http://localhost/json-only', {
186
+ method: 'POST',
192
187
  body: formData,
193
188
  });
194
189
  const res = await router.fetch(req);
195
190
  expect(res.status).toBe(415);
196
191
  });
197
192
 
198
- it("should type params and query from RequestSpec", async () => {
193
+ it('should type params and query from RequestSpec', async () => {
199
194
  const router = TypedRouter();
200
195
  router.get<
201
196
  RequestSpec<PathParams<{ id: string }> & QueryParams<{ search?: string }>>,
202
197
  ResponseSpec<{ id: string; search?: string }>
203
- >("/item/:id", (req) => {
198
+ >('/item/:id', (req) => {
204
199
  // Type testing:
205
200
  const id: string = req.params.id;
206
201
  const search: string | undefined = req.query.search;
207
202
  return json({ id, search });
208
203
  });
209
204
 
210
- const req = new Request("http://localhost/item/123?search=test");
205
+ const req = new Request('http://localhost/item/123?search=test');
211
206
  const res = await router.fetch(req);
212
207
  expect(res.status).toBe(200);
213
208
  const body = (await res.json()) as any;
214
- expect(body).toEqual({ id: "123", search: "test" });
209
+ expect(body).toEqual({ id: '123', search: 'test' });
215
210
  });
216
211
  });
@@ -1,14 +1,9 @@
1
- import {
2
- Router,
3
- withContent,
4
- json as ittyJson,
5
- type IRequest,
6
- } from "itty-router";
1
+ import { Router, withContent, json as ittyJson, type IRequest } from 'itty-router';
7
2
 
8
3
  /** Marker for body "shape + content-type" */
9
- export type Json<T> = { readonly __kind: "json"; readonly __type?: T };
4
+ export type Json<T> = { readonly __kind: 'json'; readonly __type?: T };
10
5
  export type Multipart<T> = {
11
- readonly __kind: "multipart";
6
+ readonly __kind: 'multipart';
12
7
  readonly __type?: T;
13
8
  };
14
9
  export type PathParams<T> = { readonly __pathParams?: T };
@@ -20,22 +15,15 @@ export type ResponseSpec<Body = unknown> = { readonly __res?: Body };
20
15
 
21
16
  /** Map a BodySpec to the actual req.content type */
22
17
  type ContentOf<BodySpec> =
23
- BodySpec extends Json<infer T>
24
- ? T
25
- : BodySpec extends Multipart<infer _T>
26
- ? FormData
27
- : unknown;
18
+ BodySpec extends Json<infer T> ? T : BodySpec extends Multipart<infer _T> ? FormData : unknown;
28
19
 
29
- type PathParamsOf<BodySpec> = BodySpec extends PathParams<infer T>
30
- ? T
31
- : Record<string, string>;
20
+ type PathParamsOf<BodySpec> = BodySpec extends PathParams<infer T> ? T : Record<string, string>;
32
21
 
33
- type QueryParamsOf<BodySpec> = BodySpec extends QueryParams<infer T>
34
- ? T
35
- : Record<string, string | string[] | undefined>;
22
+ type QueryParamsOf<BodySpec> =
23
+ BodySpec extends QueryParams<infer T> ? T : Record<string, string | string[] | undefined>;
36
24
 
37
25
  /** The actual request type your handlers receive */
38
- export type TypedRequest<ReqSpec> = Omit<IRequest, "params" | "query"> & {
26
+ export type TypedRequest<ReqSpec> = Omit<IRequest, 'params' | 'query'> & {
39
27
  content: ContentOf<ReqSpec extends RequestSpec<infer B> ? B : never>;
40
28
  params: PathParamsOf<ReqSpec extends RequestSpec<infer B> ? B : never>;
41
29
  query: QueryParamsOf<ReqSpec extends RequestSpec<infer B> ? B : never>;
@@ -53,10 +41,7 @@ export type HandlerController<ReqSpec, ResSpec, Args extends any[] = any[]> = (
53
41
  ) => TypedResponse<ResSpec> | Promise<TypedResponse<ResSpec>>;
54
42
 
55
43
  /** A typed json() that returns a Response annotated with Response<T> */
56
- export function json<T>(
57
- data: T,
58
- init?: ResponseInit,
59
- ): TypedResponse<ResponseSpec<T>> {
44
+ export function json<T>(data: T, init?: ResponseInit): TypedResponse<ResponseSpec<T>> {
60
45
  return ittyJson(data as any, init) as any;
61
46
  }
62
47
 
@@ -95,12 +80,9 @@ export function TypedRouter<Args extends any[] = any[]>(
95
80
  const r = Router(opts);
96
81
 
97
82
  function enforceJson(req: globalThis.Request) {
98
- const ct = (req.headers.get("content-type") ?? "").toLowerCase();
99
- if (!ct.includes("application/json") && !ct.includes("+json")) {
100
- return ittyJson(
101
- { error: "Content-Type must be application/json" },
102
- { status: 415 },
103
- );
83
+ const ct = (req.headers.get('content-type') ?? '').toLowerCase();
84
+ if (!ct.includes('application/json') && !ct.includes('+json')) {
85
+ return ittyJson({ error: 'Content-Type must be application/json' }, { status: 415 });
104
86
  }
105
87
  return null;
106
88
  }
@@ -113,19 +95,11 @@ export function TypedRouter<Args extends any[] = any[]>(
113
95
  }
114
96
  }
115
97
 
116
- const methodsToProxy = [
117
- "get",
118
- "post",
119
- "put",
120
- "delete",
121
- "patch",
122
- "head",
123
- "options",
124
- ];
98
+ const methodsToProxy = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'];
125
99
 
126
100
  const wrapper: any = new Proxy(r, {
127
101
  get(target, prop, receiver) {
128
- if (typeof prop === "string" && methodsToProxy.includes(prop)) {
102
+ if (typeof prop === 'string' && methodsToProxy.includes(prop)) {
129
103
  return (
130
104
  path: string,
131
105
  controller: HandlerController<any, any, Args>,
@@ -134,7 +108,7 @@ export function TypedRouter<Args extends any[] = any[]>(
134
108
  const handler = (...args: any[]) => {
135
109
  const req = args[0] as IRequest & { content?: unknown };
136
110
  const extraArgs = args.slice(1) as Args;
137
- if (prop === "post" || prop === "put" || prop === "patch") {
111
+ if (prop === 'post' || prop === 'put' || prop === 'patch') {
138
112
  if (!options?.multipart) {
139
113
  const ctErr = enforceJson(req);
140
114
  if (ctErr) return ctErr;
@@ -160,7 +134,7 @@ export function TypedRouter<Args extends any[] = any[]>(
160
134
  };
161
135
  }
162
136
  const value = Reflect.get(target, prop, receiver);
163
- if (typeof value === "function") {
137
+ if (typeof value === 'function') {
164
138
  return (...args: any[]) => {
165
139
  const result = value.apply(target, args);
166
140
  return result === target ? wrapper : result;