adorn-api 1.1.11 → 1.1.13
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 +18 -0
- package/dist/adapter/express/types.d.ts +3 -46
- package/dist/adapter/fastify/coercion.d.ts +12 -0
- package/dist/adapter/fastify/coercion.js +289 -0
- package/dist/adapter/fastify/controllers.d.ts +7 -0
- package/dist/adapter/fastify/controllers.js +201 -0
- package/dist/adapter/fastify/index.d.ts +14 -0
- package/dist/adapter/fastify/index.js +67 -0
- package/dist/adapter/fastify/multipart.d.ts +26 -0
- package/dist/adapter/fastify/multipart.js +75 -0
- package/dist/adapter/fastify/openapi.d.ts +10 -0
- package/dist/adapter/fastify/openapi.js +76 -0
- package/dist/adapter/fastify/response-serializer.d.ts +2 -0
- package/dist/adapter/fastify/response-serializer.js +162 -0
- package/dist/adapter/fastify/types.d.ts +100 -0
- package/dist/adapter/fastify/types.js +2 -0
- package/dist/adapter/metal-orm/index.d.ts +1 -1
- package/dist/adapter/metal-orm/types.d.ts +23 -0
- package/dist/adapter/native/coercion.d.ts +12 -0
- package/dist/adapter/native/coercion.js +289 -0
- package/dist/adapter/native/controllers.d.ts +17 -0
- package/dist/adapter/native/controllers.js +215 -0
- package/dist/adapter/native/index.d.ts +14 -0
- package/dist/adapter/native/index.js +127 -0
- package/dist/adapter/native/openapi.d.ts +7 -0
- package/dist/adapter/native/openapi.js +82 -0
- package/dist/adapter/native/response-serializer.d.ts +5 -0
- package/dist/adapter/native/response-serializer.js +160 -0
- package/dist/adapter/native/router.d.ts +25 -0
- package/dist/adapter/native/router.js +68 -0
- package/dist/adapter/native/types.d.ts +77 -0
- package/dist/adapter/native/types.js +2 -0
- package/dist/core/auth.d.ts +11 -12
- package/dist/core/auth.js +2 -2
- package/dist/core/logger.d.ts +3 -4
- package/dist/core/logger.js +2 -2
- package/dist/core/streaming.d.ts +10 -10
- package/dist/core/streaming.js +31 -19
- package/dist/core/types.d.ts +102 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +16 -1
- package/examples/fastify/app.ts +16 -0
- package/examples/fastify/index.ts +21 -0
- package/package.json +24 -18
- package/src/adapter/express/controllers.ts +249 -249
- package/src/adapter/express/types.ts +121 -160
- package/src/adapter/fastify/coercion.ts +369 -0
- package/src/adapter/fastify/controllers.ts +255 -0
- package/src/adapter/fastify/index.ts +53 -0
- package/src/adapter/fastify/multipart.ts +94 -0
- package/src/adapter/fastify/openapi.ts +93 -0
- package/src/adapter/fastify/response-serializer.ts +179 -0
- package/src/adapter/fastify/types.ts +119 -0
- package/src/adapter/metal-orm/index.ts +3 -0
- package/src/adapter/metal-orm/types.ts +25 -0
- package/src/adapter/native/coercion.ts +369 -0
- package/src/adapter/native/controllers.ts +271 -0
- package/src/adapter/native/index.ts +116 -0
- package/src/adapter/native/openapi.ts +109 -0
- package/src/adapter/native/response-serializer.ts +177 -0
- package/src/adapter/native/router.ts +90 -0
- package/src/adapter/native/types.ts +96 -0
- package/src/core/auth.ts +314 -315
- package/src/core/health.ts +234 -235
- package/src/core/logger.ts +245 -247
- package/src/core/streaming.ts +342 -330
- package/src/core/types.ts +115 -0
- package/src/index.ts +46 -16
- package/tests/e2e/fastify.e2e.test.ts +174 -0
- package/tests/native.test.ts +191 -0
- package/tests/typecheck/query-params.typecheck.ts +42 -0
- package/tests/unit/openapi-parameters.test.ts +97 -97
- package/tsconfig.json +14 -13
- package/tsconfig.typecheck.json +8 -0
- package/vitest.config.ts +47 -7
package/src/core/types.ts
CHANGED
|
@@ -12,3 +12,118 @@ export type DtoConstructor<T = any> = new (...args: any[]) => T;
|
|
|
12
12
|
* HTTP method types.
|
|
13
13
|
*/
|
|
14
14
|
export type HttpMethod = "get" | "post" | "put" | "patch" | "delete";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Uploaded file information from multipart form data.
|
|
18
|
+
*/
|
|
19
|
+
export interface UploadedFileInfo {
|
|
20
|
+
/** Original filename as provided by the client */
|
|
21
|
+
originalName: string;
|
|
22
|
+
/** MIME type of the file */
|
|
23
|
+
mimeType: string;
|
|
24
|
+
/** Size of the file in bytes */
|
|
25
|
+
size: number;
|
|
26
|
+
/** File buffer (when using memory storage) */
|
|
27
|
+
buffer?: Buffer;
|
|
28
|
+
/** Path to the file on disk (when using disk storage) */
|
|
29
|
+
path?: string;
|
|
30
|
+
/** Field name from the form */
|
|
31
|
+
fieldName: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generic request interface.
|
|
36
|
+
*/
|
|
37
|
+
export interface HttpRequest {
|
|
38
|
+
method: string;
|
|
39
|
+
url: string;
|
|
40
|
+
originalUrl?: string;
|
|
41
|
+
path?: string;
|
|
42
|
+
params: Record<string, any>;
|
|
43
|
+
query: Record<string, any>;
|
|
44
|
+
body: any;
|
|
45
|
+
headers: Record<string, any>;
|
|
46
|
+
ip?: string;
|
|
47
|
+
protocol?: string;
|
|
48
|
+
secure?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generic response interface.
|
|
53
|
+
*/
|
|
54
|
+
export interface HttpResponseHeaders {
|
|
55
|
+
[key: string]: string | string[] | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generic response interface.
|
|
60
|
+
*/
|
|
61
|
+
export interface HttpResponseWriter {
|
|
62
|
+
statusCode: number;
|
|
63
|
+
headersSent: boolean;
|
|
64
|
+
setHeader(name: string, value: string | string[]): void;
|
|
65
|
+
getHeader(name: string): string | string[] | undefined;
|
|
66
|
+
removeHeader(name: string): void;
|
|
67
|
+
status(code: number): this;
|
|
68
|
+
send(body?: any): this;
|
|
69
|
+
end(): void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Server-Sent Event emitter interface.
|
|
74
|
+
*/
|
|
75
|
+
export interface SseEmitterInterface {
|
|
76
|
+
send(data: any): void;
|
|
77
|
+
emit(event: string, data: any): void;
|
|
78
|
+
comment(text: string): void;
|
|
79
|
+
close(): void;
|
|
80
|
+
isClosed(): boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Stream writer interface.
|
|
85
|
+
*/
|
|
86
|
+
export interface StreamWriterInterface {
|
|
87
|
+
write(data: string | Buffer): boolean;
|
|
88
|
+
writeLine(data: string): boolean;
|
|
89
|
+
writeJson(data: any): boolean;
|
|
90
|
+
writeJsonLine(data: any): boolean;
|
|
91
|
+
close(): void;
|
|
92
|
+
isClosed(): boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Request context provided to route handlers.
|
|
97
|
+
*/
|
|
98
|
+
export interface RequestContext<
|
|
99
|
+
TBody = any,
|
|
100
|
+
TQuery extends object | undefined = Record<string, any>,
|
|
101
|
+
TParams extends object | undefined = Record<string, any>,
|
|
102
|
+
THeaders extends object | undefined = Record<string, any>,
|
|
103
|
+
TFiles extends Record<string, UploadedFileInfo | UploadedFileInfo[]> | undefined = any
|
|
104
|
+
> {
|
|
105
|
+
/** Raw request object */
|
|
106
|
+
req: any;
|
|
107
|
+
/** Raw response object */
|
|
108
|
+
res: any;
|
|
109
|
+
/** Parsed request body */
|
|
110
|
+
body: TBody;
|
|
111
|
+
/** Parsed query parameters */
|
|
112
|
+
query: TQuery;
|
|
113
|
+
/** Parsed path parameters */
|
|
114
|
+
params: TParams;
|
|
115
|
+
/** Request headers */
|
|
116
|
+
headers: THeaders;
|
|
117
|
+
/** Uploaded files (when using multipart handling) */
|
|
118
|
+
files: TFiles;
|
|
119
|
+
/**
|
|
120
|
+
* Server-Sent Events emitter for streaming events to client.
|
|
121
|
+
* Only available on routes marked with @Sse decorator.
|
|
122
|
+
*/
|
|
123
|
+
sse?: SseEmitterInterface;
|
|
124
|
+
/**
|
|
125
|
+
* Stream writer for streaming responses.
|
|
126
|
+
* Available on routes marked with @Streaming or @Sse decorator.
|
|
127
|
+
*/
|
|
128
|
+
stream?: StreamWriterInterface;
|
|
129
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,17 +1,47 @@
|
|
|
1
|
-
export * from "./core/decorators";
|
|
2
|
-
export * from "./core/schema";
|
|
3
|
-
export * from "./core/openapi";
|
|
4
|
-
export * from "./core/errors";
|
|
5
|
-
export * from "./core/response";
|
|
6
|
-
export * from "./core/validation";
|
|
7
|
-
export * from "./core/validation-errors";
|
|
8
|
-
export * from "./core/coerce";
|
|
9
|
-
export * from "./core/health";
|
|
10
|
-
export * from "./core/logger";
|
|
11
|
-
export * from "./core/serialization";
|
|
12
|
-
export * from "./core/auth";
|
|
13
|
-
export * from "./core/lifecycle";
|
|
14
|
-
export * from "./core/streaming";
|
|
15
|
-
export
|
|
16
|
-
|
|
1
|
+
export * from "./core/decorators";
|
|
2
|
+
export * from "./core/schema";
|
|
3
|
+
export * from "./core/openapi";
|
|
4
|
+
export * from "./core/errors";
|
|
5
|
+
export * from "./core/response";
|
|
6
|
+
export * from "./core/validation";
|
|
7
|
+
export * from "./core/validation-errors";
|
|
8
|
+
export * from "./core/coerce";
|
|
9
|
+
export * from "./core/health";
|
|
10
|
+
export * from "./core/logger";
|
|
11
|
+
export * from "./core/serialization";
|
|
12
|
+
export * from "./core/auth";
|
|
13
|
+
export * from "./core/lifecycle";
|
|
14
|
+
export * from "./core/streaming";
|
|
15
|
+
export {
|
|
16
|
+
createExpressApp,
|
|
17
|
+
attachControllers as attachExpressControllers,
|
|
18
|
+
attachOpenApi as attachExpressOpenApi,
|
|
19
|
+
shutdownApp as shutdownExpressApp
|
|
20
|
+
} from "./adapter/express/index";
|
|
21
|
+
export type {
|
|
22
|
+
ExpressAdapterOptions,
|
|
23
|
+
RequestContext as ExpressRequestContext
|
|
24
|
+
} from "./adapter/express/index";
|
|
25
|
+
export {
|
|
26
|
+
createFastifyApp,
|
|
27
|
+
attachControllers as attachFastifyControllers,
|
|
28
|
+
attachOpenApi as attachFastifyOpenApi,
|
|
29
|
+
shutdownApp as shutdownFastifyApp
|
|
30
|
+
} from "./adapter/fastify/index";
|
|
31
|
+
export type {
|
|
32
|
+
FastifyAdapterOptions,
|
|
33
|
+
RequestContext as FastifyRequestContext
|
|
34
|
+
} from "./adapter/fastify/index";
|
|
35
|
+
export {
|
|
36
|
+
createNativeApp,
|
|
37
|
+
attachControllers as attachNativeControllers,
|
|
38
|
+
attachOpenApi as attachNativeOpenApi,
|
|
39
|
+
shutdownApp as shutdownNativeApp
|
|
40
|
+
} from "./adapter/native/index";
|
|
41
|
+
export type {
|
|
42
|
+
NativeAdapterOptions,
|
|
43
|
+
RequestContext as NativeRequestContext,
|
|
44
|
+
NativeApp
|
|
45
|
+
} from "./adapter/native/index";
|
|
46
|
+
export * from "./adapter/metal-orm/index";
|
|
17
47
|
export * from "./core/types";
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import { createFastifyApp } from "../../src";
|
|
3
|
+
import { Controller, Get, Post, Body, Params, Query, t, type RequestContext, Auth, Roles, Sse } from "../../src";
|
|
4
|
+
|
|
5
|
+
@Controller("/test")
|
|
6
|
+
class TestController {
|
|
7
|
+
@Get("/hello")
|
|
8
|
+
hello() {
|
|
9
|
+
return { message: "Hello from Fastify" };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@Post("/echo")
|
|
13
|
+
@Body(t.object({ name: t.string() }))
|
|
14
|
+
echo(ctx: RequestContext<{ name: string }>) {
|
|
15
|
+
return { name: ctx.body.name };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Get("/params/:id")
|
|
19
|
+
@Params(t.object({ id: t.string() }))
|
|
20
|
+
getParam(ctx: RequestContext<any, any, { id: string }>) {
|
|
21
|
+
return { id: ctx.params.id };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Get("/query")
|
|
25
|
+
@Query(t.object({ q: t.string() }))
|
|
26
|
+
getQuery(ctx: RequestContext<any, { q: string }>) {
|
|
27
|
+
return { q: ctx.query.q };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Get("/protected")
|
|
31
|
+
@Auth()
|
|
32
|
+
protected() {
|
|
33
|
+
return { message: "Authenticated" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Get("/roles")
|
|
37
|
+
@Roles("admin")
|
|
38
|
+
roles() {
|
|
39
|
+
return { message: "Admin only" };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Get("/sse")
|
|
43
|
+
@Sse()
|
|
44
|
+
sse(ctx: RequestContext) {
|
|
45
|
+
ctx.sse?.send({ message: "Hello SSE" });
|
|
46
|
+
ctx.sse?.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("Fastify Adapter E2E", () => {
|
|
51
|
+
let app: any;
|
|
52
|
+
|
|
53
|
+
beforeAll(async () => {
|
|
54
|
+
app = await createFastifyApp({
|
|
55
|
+
controllers: [TestController]
|
|
56
|
+
});
|
|
57
|
+
await app.ready();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should handle GET /hello", async () => {
|
|
61
|
+
const response = await app.inject({
|
|
62
|
+
method: "GET",
|
|
63
|
+
url: "/test/hello"
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(response.statusCode).toBe(200);
|
|
67
|
+
expect(response.json()).toEqual({ message: "Hello from Fastify" });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should handle POST /echo with body", async () => {
|
|
71
|
+
const response = await app.inject({
|
|
72
|
+
method: "POST",
|
|
73
|
+
url: "/test/echo",
|
|
74
|
+
payload: { name: "Adorn" }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(response.statusCode).toBe(200);
|
|
78
|
+
expect(response.json()).toEqual({ name: "Adorn" });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should handle GET /params/:id", async () => {
|
|
82
|
+
const response = await app.inject({
|
|
83
|
+
method: "GET",
|
|
84
|
+
url: "/test/params/123"
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(response.statusCode).toBe(200);
|
|
88
|
+
expect(response.json()).toEqual({ id: "123" });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should handle GET /query", async () => {
|
|
92
|
+
const response = await app.inject({
|
|
93
|
+
method: "GET",
|
|
94
|
+
url: "/test/query?q=search"
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(response.statusCode).toBe(200);
|
|
98
|
+
expect(response.json()).toEqual({ q: "search" });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should handle validation errors", async () => {
|
|
102
|
+
const response = await app.inject({
|
|
103
|
+
method: "POST",
|
|
104
|
+
url: "/test/echo",
|
|
105
|
+
payload: { name: 123 } // Invalid type
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(response.statusCode).toBe(400);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should block unauthorized access to protected route", async () => {
|
|
112
|
+
const app401 = await createFastifyApp({
|
|
113
|
+
controllers: [TestController]
|
|
114
|
+
});
|
|
115
|
+
const response = await app401.inject({
|
|
116
|
+
method: "GET",
|
|
117
|
+
url: "/test/protected"
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
console.log("401 response body:", response.body);
|
|
121
|
+
expect(response.statusCode).toBe(401);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should allow authorized access to protected route", async () => {
|
|
125
|
+
const appAuth = await createFastifyApp({
|
|
126
|
+
controllers: [TestController]
|
|
127
|
+
});
|
|
128
|
+
appAuth.addHook("preHandler", async (req: any, _reply: any) => {
|
|
129
|
+
req.user = { id: "1" };
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const response = await appAuth.inject({
|
|
133
|
+
method: "GET",
|
|
134
|
+
url: "/test/protected"
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(response.statusCode).toBe(200);
|
|
138
|
+
expect(response.json()).toEqual({ message: "Authenticated" });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should block access without required role", async () => {
|
|
142
|
+
const appNoRole = await createFastifyApp({
|
|
143
|
+
controllers: [TestController]
|
|
144
|
+
});
|
|
145
|
+
appNoRole.addHook("preHandler", async (req: any, _reply: any) => {
|
|
146
|
+
req.user = { id: "1", roles: ["user"] };
|
|
147
|
+
});
|
|
148
|
+
const response = await appNoRole.inject({
|
|
149
|
+
method: "GET",
|
|
150
|
+
url: "/test/roles"
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(response.statusCode).toBe(403);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should allow access with required role", async () => {
|
|
157
|
+
// Overriding user with admin role
|
|
158
|
+
const appWithAdmin = await createFastifyApp({
|
|
159
|
+
controllers: [TestController]
|
|
160
|
+
});
|
|
161
|
+
appWithAdmin.addHook("preHandler", async (req: any) => {
|
|
162
|
+
req.user = { id: "1", roles: ["admin"] };
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const response = await appWithAdmin.inject({
|
|
166
|
+
method: "GET",
|
|
167
|
+
url: "/test/roles"
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(response.statusCode).toBe(200);
|
|
171
|
+
expect(response.json()).toEqual({ message: "Admin only" });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
Controller,
|
|
4
|
+
Get,
|
|
5
|
+
Post,
|
|
6
|
+
Body,
|
|
7
|
+
Params,
|
|
8
|
+
Query,
|
|
9
|
+
Returns,
|
|
10
|
+
t,
|
|
11
|
+
createNativeApp,
|
|
12
|
+
shutdownNativeApp,
|
|
13
|
+
type NativeRequestContext
|
|
14
|
+
} from "../src";
|
|
15
|
+
|
|
16
|
+
@Controller("/test")
|
|
17
|
+
class TestController {
|
|
18
|
+
@Get("/hello")
|
|
19
|
+
async hello() {
|
|
20
|
+
return { message: "hello" };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Post("/echo")
|
|
24
|
+
@Body(t.object({ name: t.string() }))
|
|
25
|
+
async echo(ctx: NativeRequestContext<{ name: string }>) {
|
|
26
|
+
return { hello: ctx.body.name };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Get("/greet/:name")
|
|
30
|
+
@Params(t.object({ name: t.string() }))
|
|
31
|
+
async greet(ctx: NativeRequestContext<any, any, { name: string }>) {
|
|
32
|
+
return { message: `Hello, ${ctx.params.name}!` };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Get("/query")
|
|
36
|
+
@Query(t.object({ q: t.string() }))
|
|
37
|
+
async query(ctx: NativeRequestContext<any, { q: string }>) {
|
|
38
|
+
return { result: ctx.query.q };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("Native Adapter", () => {
|
|
43
|
+
let app: any;
|
|
44
|
+
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
app = await createNativeApp({
|
|
47
|
+
controllers: [TestController]
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
await shutdownNativeApp();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should handle GET request", async () => {
|
|
56
|
+
const req: any = {
|
|
57
|
+
method: "GET",
|
|
58
|
+
url: "/test/hello",
|
|
59
|
+
headers: {}
|
|
60
|
+
};
|
|
61
|
+
const res: any = {
|
|
62
|
+
statusCode: 0,
|
|
63
|
+
headers: {},
|
|
64
|
+
setHeader(name: string, value: string) {
|
|
65
|
+
this.headers[name] = value;
|
|
66
|
+
},
|
|
67
|
+
getHeader(name: string) {
|
|
68
|
+
return this.headers[name];
|
|
69
|
+
},
|
|
70
|
+
end: (data: string) => {
|
|
71
|
+
res.body = data;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
await app.handle(req, res);
|
|
76
|
+
|
|
77
|
+
expect(res.statusCode).toBe(200);
|
|
78
|
+
expect(JSON.parse(res.body)).toEqual({ message: "hello" });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should handle POST request with body", async () => {
|
|
82
|
+
const req: any = {
|
|
83
|
+
method: "POST",
|
|
84
|
+
url: "/test/echo",
|
|
85
|
+
headers: { "content-type": "application/json" },
|
|
86
|
+
on: (event: string, cb: any) => {
|
|
87
|
+
if (event === "data") {
|
|
88
|
+
cb(JSON.stringify({ name: "World" }));
|
|
89
|
+
}
|
|
90
|
+
if (event === "end") {
|
|
91
|
+
cb();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const res: any = {
|
|
96
|
+
statusCode: 0,
|
|
97
|
+
headers: {},
|
|
98
|
+
setHeader(name: string, value: string) {
|
|
99
|
+
this.headers[name] = value;
|
|
100
|
+
},
|
|
101
|
+
getHeader(name: string) {
|
|
102
|
+
return this.headers[name];
|
|
103
|
+
},
|
|
104
|
+
end: (data: string) => {
|
|
105
|
+
res.body = data;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
await app.handle(req, res);
|
|
110
|
+
|
|
111
|
+
expect(res.statusCode).toBe(200);
|
|
112
|
+
expect(JSON.parse(res.body)).toEqual({ hello: "World" });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should handle path parameters", async () => {
|
|
116
|
+
const req: any = {
|
|
117
|
+
method: "GET",
|
|
118
|
+
url: "/test/greet/John",
|
|
119
|
+
headers: {}
|
|
120
|
+
};
|
|
121
|
+
const res: any = {
|
|
122
|
+
statusCode: 0,
|
|
123
|
+
headers: {},
|
|
124
|
+
setHeader(name: string, value: string) {
|
|
125
|
+
this.headers[name] = value;
|
|
126
|
+
},
|
|
127
|
+
getHeader(name: string) {
|
|
128
|
+
return this.headers[name];
|
|
129
|
+
},
|
|
130
|
+
end: (data: string) => {
|
|
131
|
+
res.body = data;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
await app.handle(req, res);
|
|
136
|
+
|
|
137
|
+
expect(res.statusCode).toBe(200);
|
|
138
|
+
expect(JSON.parse(res.body)).toEqual({ message: "Hello, John!" });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should handle query parameters", async () => {
|
|
142
|
+
const req: any = {
|
|
143
|
+
method: "GET",
|
|
144
|
+
url: "/test/query?q=search",
|
|
145
|
+
headers: {}
|
|
146
|
+
};
|
|
147
|
+
const res: any = {
|
|
148
|
+
statusCode: 0,
|
|
149
|
+
headers: {},
|
|
150
|
+
setHeader(name: string, value: string) {
|
|
151
|
+
this.headers[name] = value;
|
|
152
|
+
},
|
|
153
|
+
getHeader(name: string) {
|
|
154
|
+
return this.headers[name];
|
|
155
|
+
},
|
|
156
|
+
end: (data: string) => {
|
|
157
|
+
res.body = data;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
await app.handle(req, res);
|
|
162
|
+
|
|
163
|
+
expect(res.statusCode).toBe(200);
|
|
164
|
+
expect(JSON.parse(res.body)).toEqual({ result: "search" });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should return 404 for unknown route", async () => {
|
|
168
|
+
const req: any = {
|
|
169
|
+
method: "GET",
|
|
170
|
+
url: "/unknown",
|
|
171
|
+
headers: {}
|
|
172
|
+
};
|
|
173
|
+
const res: any = {
|
|
174
|
+
statusCode: 0,
|
|
175
|
+
headers: {},
|
|
176
|
+
setHeader(name: string, value: string) {
|
|
177
|
+
this.headers[name] = value;
|
|
178
|
+
},
|
|
179
|
+
getHeader(name: string) {
|
|
180
|
+
return this.headers[name];
|
|
181
|
+
},
|
|
182
|
+
end: (data: string) => {
|
|
183
|
+
res.body = data;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
await app.handle(req, res);
|
|
188
|
+
|
|
189
|
+
expect(res.statusCode).toBe(404);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PagedQueryParams,
|
|
3
|
+
PaginationQueryParams,
|
|
4
|
+
SortDirection,
|
|
5
|
+
SortingQueryParams
|
|
6
|
+
} from "../../src/index";
|
|
7
|
+
|
|
8
|
+
type Assert<T extends true> = T;
|
|
9
|
+
type IsEqual<A, B> =
|
|
10
|
+
(<T>() => T extends A ? 1 : 2) extends
|
|
11
|
+
(<T>() => T extends B ? 1 : 2)
|
|
12
|
+
? true
|
|
13
|
+
: false;
|
|
14
|
+
|
|
15
|
+
type _SortDirectionMatchesPublicType = Assert<
|
|
16
|
+
IsEqual<SortingQueryParams["sortDirection"], SortDirection | undefined>
|
|
17
|
+
>;
|
|
18
|
+
|
|
19
|
+
const paginationOnly: PaginationQueryParams = {
|
|
20
|
+
page: 1,
|
|
21
|
+
pageSize: 25
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const sortingOnly: SortingQueryParams = {
|
|
25
|
+
sortBy: "createdAt",
|
|
26
|
+
sortDirection: "desc"
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const pagedWithSorting: PagedQueryParams = {
|
|
30
|
+
page: paginationOnly.page,
|
|
31
|
+
pageSize: paginationOnly.pageSize,
|
|
32
|
+
sortBy: sortingOnly.sortBy,
|
|
33
|
+
sortDirection: "asc"
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// @ts-expect-error sortDirection accepts only "asc" | "desc"
|
|
37
|
+
const invalidSorting: SortingQueryParams = { sortDirection: "ASC" };
|
|
38
|
+
|
|
39
|
+
void paginationOnly;
|
|
40
|
+
void sortingOnly;
|
|
41
|
+
void pagedWithSorting;
|
|
42
|
+
void invalidSorting;
|