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.
Files changed (75) hide show
  1. package/README.md +18 -0
  2. package/dist/adapter/express/types.d.ts +3 -46
  3. package/dist/adapter/fastify/coercion.d.ts +12 -0
  4. package/dist/adapter/fastify/coercion.js +289 -0
  5. package/dist/adapter/fastify/controllers.d.ts +7 -0
  6. package/dist/adapter/fastify/controllers.js +201 -0
  7. package/dist/adapter/fastify/index.d.ts +14 -0
  8. package/dist/adapter/fastify/index.js +67 -0
  9. package/dist/adapter/fastify/multipart.d.ts +26 -0
  10. package/dist/adapter/fastify/multipart.js +75 -0
  11. package/dist/adapter/fastify/openapi.d.ts +10 -0
  12. package/dist/adapter/fastify/openapi.js +76 -0
  13. package/dist/adapter/fastify/response-serializer.d.ts +2 -0
  14. package/dist/adapter/fastify/response-serializer.js +162 -0
  15. package/dist/adapter/fastify/types.d.ts +100 -0
  16. package/dist/adapter/fastify/types.js +2 -0
  17. package/dist/adapter/metal-orm/index.d.ts +1 -1
  18. package/dist/adapter/metal-orm/types.d.ts +23 -0
  19. package/dist/adapter/native/coercion.d.ts +12 -0
  20. package/dist/adapter/native/coercion.js +289 -0
  21. package/dist/adapter/native/controllers.d.ts +17 -0
  22. package/dist/adapter/native/controllers.js +215 -0
  23. package/dist/adapter/native/index.d.ts +14 -0
  24. package/dist/adapter/native/index.js +127 -0
  25. package/dist/adapter/native/openapi.d.ts +7 -0
  26. package/dist/adapter/native/openapi.js +82 -0
  27. package/dist/adapter/native/response-serializer.d.ts +5 -0
  28. package/dist/adapter/native/response-serializer.js +160 -0
  29. package/dist/adapter/native/router.d.ts +25 -0
  30. package/dist/adapter/native/router.js +68 -0
  31. package/dist/adapter/native/types.d.ts +77 -0
  32. package/dist/adapter/native/types.js +2 -0
  33. package/dist/core/auth.d.ts +11 -12
  34. package/dist/core/auth.js +2 -2
  35. package/dist/core/logger.d.ts +3 -4
  36. package/dist/core/logger.js +2 -2
  37. package/dist/core/streaming.d.ts +10 -10
  38. package/dist/core/streaming.js +31 -19
  39. package/dist/core/types.d.ts +102 -0
  40. package/dist/index.d.ts +6 -1
  41. package/dist/index.js +16 -1
  42. package/examples/fastify/app.ts +16 -0
  43. package/examples/fastify/index.ts +21 -0
  44. package/package.json +24 -18
  45. package/src/adapter/express/controllers.ts +249 -249
  46. package/src/adapter/express/types.ts +121 -160
  47. package/src/adapter/fastify/coercion.ts +369 -0
  48. package/src/adapter/fastify/controllers.ts +255 -0
  49. package/src/adapter/fastify/index.ts +53 -0
  50. package/src/adapter/fastify/multipart.ts +94 -0
  51. package/src/adapter/fastify/openapi.ts +93 -0
  52. package/src/adapter/fastify/response-serializer.ts +179 -0
  53. package/src/adapter/fastify/types.ts +119 -0
  54. package/src/adapter/metal-orm/index.ts +3 -0
  55. package/src/adapter/metal-orm/types.ts +25 -0
  56. package/src/adapter/native/coercion.ts +369 -0
  57. package/src/adapter/native/controllers.ts +271 -0
  58. package/src/adapter/native/index.ts +116 -0
  59. package/src/adapter/native/openapi.ts +109 -0
  60. package/src/adapter/native/response-serializer.ts +177 -0
  61. package/src/adapter/native/router.ts +90 -0
  62. package/src/adapter/native/types.ts +96 -0
  63. package/src/core/auth.ts +314 -315
  64. package/src/core/health.ts +234 -235
  65. package/src/core/logger.ts +245 -247
  66. package/src/core/streaming.ts +342 -330
  67. package/src/core/types.ts +115 -0
  68. package/src/index.ts +46 -16
  69. package/tests/e2e/fastify.e2e.test.ts +174 -0
  70. package/tests/native.test.ts +191 -0
  71. package/tests/typecheck/query-params.typecheck.ts +42 -0
  72. package/tests/unit/openapi-parameters.test.ts +97 -97
  73. package/tsconfig.json +14 -13
  74. package/tsconfig.typecheck.json +8 -0
  75. 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 * from "./adapter/express/index";
16
- export * from "./adapter/metal-orm/index";
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;