@codexsploitx/schemaapi 1.0.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/.prettierignore +5 -0
- package/.prettierrc +7 -0
- package/bin/schemaapi +302 -0
- package/build.md +246 -0
- package/dist/core/contract.d.ts +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/schemaapi.cjs.js +13 -0
- package/dist/schemaapi.cjs.js.map +1 -0
- package/dist/schemaapi.esm.js +11 -0
- package/dist/schemaapi.esm.js.map +1 -0
- package/dist/schemaapi.umd.js +19 -0
- package/dist/schemaapi.umd.js.map +1 -0
- package/docs/adapters/deno.md +51 -0
- package/docs/adapters/express.md +67 -0
- package/docs/adapters/fastify.md +64 -0
- package/docs/adapters/hapi.md +67 -0
- package/docs/adapters/koa.md +61 -0
- package/docs/adapters/nest.md +66 -0
- package/docs/adapters/next.md +66 -0
- package/docs/adapters/remix.md +72 -0
- package/docs/cli.md +18 -0
- package/docs/consepts.md +18 -0
- package/docs/getting_started.md +149 -0
- package/docs/sdk.md +25 -0
- package/docs/validation.md +228 -0
- package/docs/versioning.md +28 -0
- package/eslint.config.mjs +34 -0
- package/estructure.md +55 -0
- package/libreria.md +319 -0
- package/package.json +61 -0
- package/readme.md +89 -0
- package/resumen.md +188 -0
- package/rollup.config.js +19 -0
- package/src/adapters/deno.ts +139 -0
- package/src/adapters/express.ts +134 -0
- package/src/adapters/fastify.ts +133 -0
- package/src/adapters/hapi.ts +140 -0
- package/src/adapters/index.ts +9 -0
- package/src/adapters/koa.ts +128 -0
- package/src/adapters/nest.ts +122 -0
- package/src/adapters/next.ts +175 -0
- package/src/adapters/remix.ts +145 -0
- package/src/adapters/ws.ts +132 -0
- package/src/core/client.ts +104 -0
- package/src/core/contract.ts +534 -0
- package/src/core/versioning.test.ts +174 -0
- package/src/docs.ts +535 -0
- package/src/index.ts +5 -0
- package/src/playground.test.ts +98 -0
- package/src/playground.ts +13 -0
- package/src/sdk.ts +17 -0
- package/tests/adapters.deno.test.ts +70 -0
- package/tests/adapters.express.test.ts +67 -0
- package/tests/adapters.fastify.test.ts +63 -0
- package/tests/adapters.hapi.test.ts +66 -0
- package/tests/adapters.koa.test.ts +58 -0
- package/tests/adapters.nest.test.ts +85 -0
- package/tests/adapters.next.test.ts +39 -0
- package/tests/adapters.remix.test.ts +52 -0
- package/tests/adapters.ws.test.ts +91 -0
- package/tests/cli.test.ts +156 -0
- package/tests/client.test.ts +110 -0
- package/tests/contract.handle.test.ts +267 -0
- package/tests/docs.test.ts +96 -0
- package/tests/sdk.test.ts +34 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { createContract } from "../core/contract";
|
|
2
|
+
import { buildErrorPayload } from "../core/contract";
|
|
3
|
+
|
|
4
|
+
type AnyContract = ReturnType<typeof createContract>;
|
|
5
|
+
|
|
6
|
+
type MethodSchemaLike = {
|
|
7
|
+
media?: {
|
|
8
|
+
kind?: string;
|
|
9
|
+
contentTypes?: string[];
|
|
10
|
+
maxSize?: number;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type DownloadResult =
|
|
15
|
+
| {
|
|
16
|
+
data: unknown;
|
|
17
|
+
contentType?: string;
|
|
18
|
+
filename?: string;
|
|
19
|
+
}
|
|
20
|
+
| unknown;
|
|
21
|
+
|
|
22
|
+
interface FastifyLikeRequest {
|
|
23
|
+
params: Record<string, string>;
|
|
24
|
+
query: Record<string, string>;
|
|
25
|
+
body: unknown;
|
|
26
|
+
headers: Record<string, string>;
|
|
27
|
+
user?: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface FastifyLikeReply {
|
|
31
|
+
status: (code: number) => FastifyLikeReply;
|
|
32
|
+
send: (payload: unknown) => void;
|
|
33
|
+
header: (key: string, value: string) => FastifyLikeReply;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface FastifyInstance {
|
|
37
|
+
route: (options: {
|
|
38
|
+
method: string | string[];
|
|
39
|
+
url: string;
|
|
40
|
+
handler: (
|
|
41
|
+
request: FastifyLikeRequest,
|
|
42
|
+
reply: FastifyLikeReply
|
|
43
|
+
) => Promise<void> | void;
|
|
44
|
+
}) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function handleContract(
|
|
48
|
+
fastify: FastifyInstance,
|
|
49
|
+
contract: AnyContract,
|
|
50
|
+
handlers: Record<
|
|
51
|
+
string,
|
|
52
|
+
(ctx: Record<string, unknown>) => unknown | Promise<unknown>
|
|
53
|
+
>
|
|
54
|
+
) {
|
|
55
|
+
const schema = contract.schema as Record<string, Record<string, unknown>>;
|
|
56
|
+
|
|
57
|
+
Object.keys(schema).forEach((route) => {
|
|
58
|
+
const methods = schema[route] as Record<string, unknown>;
|
|
59
|
+
|
|
60
|
+
Object.keys(methods).forEach((method) => {
|
|
61
|
+
const endpoint = `${method} ${route}`;
|
|
62
|
+
const implementation = handlers[endpoint];
|
|
63
|
+
const methodSchema = (methods as Record<string, unknown>)[
|
|
64
|
+
method
|
|
65
|
+
] as MethodSchemaLike | undefined;
|
|
66
|
+
|
|
67
|
+
if (!implementation) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const wrapped = contract.handle(endpoint, implementation);
|
|
72
|
+
|
|
73
|
+
fastify.route({
|
|
74
|
+
method: method,
|
|
75
|
+
url: route, // Fastify soporta /:id sintaxis
|
|
76
|
+
handler: async (
|
|
77
|
+
request: FastifyLikeRequest,
|
|
78
|
+
reply: FastifyLikeReply
|
|
79
|
+
) => {
|
|
80
|
+
try {
|
|
81
|
+
const context: Record<string, unknown> = {
|
|
82
|
+
params: request.params || {},
|
|
83
|
+
query: request.query || {},
|
|
84
|
+
body: request.body,
|
|
85
|
+
headers: request.headers || {},
|
|
86
|
+
user: request.user,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const result = await wrapped(context);
|
|
90
|
+
const media = methodSchema?.media;
|
|
91
|
+
if (media && media.kind === "download") {
|
|
92
|
+
const download = result as DownloadResult;
|
|
93
|
+
const data =
|
|
94
|
+
download &&
|
|
95
|
+
typeof download === "object" &&
|
|
96
|
+
"data" in download
|
|
97
|
+
? (download as { data: unknown }).data
|
|
98
|
+
: download;
|
|
99
|
+
const contentType =
|
|
100
|
+
download &&
|
|
101
|
+
typeof download === "object" &&
|
|
102
|
+
"contentType" in download
|
|
103
|
+
? (download as { contentType: string }).contentType
|
|
104
|
+
: "application/octet-stream";
|
|
105
|
+
const filename =
|
|
106
|
+
download &&
|
|
107
|
+
typeof download === "object" &&
|
|
108
|
+
"filename" in download
|
|
109
|
+
? (download as { filename: string }).filename
|
|
110
|
+
: undefined;
|
|
111
|
+
|
|
112
|
+
if (typeof reply.header === "function") {
|
|
113
|
+
reply.header("Content-Type", contentType);
|
|
114
|
+
if (filename) {
|
|
115
|
+
reply.header(
|
|
116
|
+
"Content-Disposition",
|
|
117
|
+
`attachment; filename="${filename}"`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
reply.send(data);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
reply.send(result);
|
|
125
|
+
} catch (error: unknown) {
|
|
126
|
+
const payload = buildErrorPayload(error);
|
|
127
|
+
reply.status(payload.status).send(payload);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { createContract } from "../core/contract";
|
|
2
|
+
import { buildErrorPayload } from "../core/contract";
|
|
3
|
+
|
|
4
|
+
type AnyContract = ReturnType<typeof createContract>;
|
|
5
|
+
|
|
6
|
+
type MethodSchemaLike = {
|
|
7
|
+
media?: {
|
|
8
|
+
kind?: string;
|
|
9
|
+
contentTypes?: string[];
|
|
10
|
+
maxSize?: number;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type DownloadResult =
|
|
15
|
+
| {
|
|
16
|
+
data: unknown;
|
|
17
|
+
contentType?: string;
|
|
18
|
+
filename?: string;
|
|
19
|
+
}
|
|
20
|
+
| unknown;
|
|
21
|
+
|
|
22
|
+
interface HapiLikeRequest {
|
|
23
|
+
params: Record<string, string>;
|
|
24
|
+
query: Record<string, string>;
|
|
25
|
+
payload: unknown;
|
|
26
|
+
headers: Record<string, string>;
|
|
27
|
+
auth?: { credentials?: unknown };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface HapiLikeResponseObject {
|
|
31
|
+
type: (contentType: string) => void;
|
|
32
|
+
header: (key: string, value: string) => void;
|
|
33
|
+
code: (statusCode: number) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface HapiLikeResponseToolkit {
|
|
37
|
+
response: (data: unknown) => HapiLikeResponseObject;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface HapiServer {
|
|
41
|
+
route: (options: {
|
|
42
|
+
method: string;
|
|
43
|
+
path: string;
|
|
44
|
+
handler: (
|
|
45
|
+
request: HapiLikeRequest,
|
|
46
|
+
h: HapiLikeResponseToolkit
|
|
47
|
+
) => Promise<unknown> | unknown;
|
|
48
|
+
}) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function handleContract(
|
|
52
|
+
server: HapiServer,
|
|
53
|
+
contract: AnyContract,
|
|
54
|
+
handlers: Record<
|
|
55
|
+
string,
|
|
56
|
+
(ctx: Record<string, unknown>) => unknown | Promise<unknown>
|
|
57
|
+
>
|
|
58
|
+
) {
|
|
59
|
+
const schema = contract.schema as Record<string, Record<string, unknown>>;
|
|
60
|
+
|
|
61
|
+
Object.keys(schema).forEach((route) => {
|
|
62
|
+
const methods = schema[route] as Record<string, unknown>;
|
|
63
|
+
|
|
64
|
+
// Convertir ruta express-style :param a hapi-style {param}
|
|
65
|
+
const hapiRoute = route.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
66
|
+
|
|
67
|
+
Object.keys(methods).forEach((method) => {
|
|
68
|
+
const endpoint = `${method} ${route}`;
|
|
69
|
+
const implementation = handlers[endpoint];
|
|
70
|
+
const methodSchema = (methods as Record<string, unknown>)[
|
|
71
|
+
method
|
|
72
|
+
] as MethodSchemaLike | undefined;
|
|
73
|
+
|
|
74
|
+
if (!implementation) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const wrapped = contract.handle(endpoint, implementation);
|
|
79
|
+
|
|
80
|
+
server.route({
|
|
81
|
+
method: method,
|
|
82
|
+
path: hapiRoute,
|
|
83
|
+
handler: async (
|
|
84
|
+
request: HapiLikeRequest,
|
|
85
|
+
h: HapiLikeResponseToolkit
|
|
86
|
+
) => {
|
|
87
|
+
try {
|
|
88
|
+
const context: Record<string, unknown> = {
|
|
89
|
+
params: request.params || {},
|
|
90
|
+
query: request.query || {},
|
|
91
|
+
body: request.payload,
|
|
92
|
+
headers: request.headers || {},
|
|
93
|
+
user: request.auth?.credentials, // Common Hapi auth pattern
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const result = await wrapped(context);
|
|
97
|
+
const media = methodSchema?.media;
|
|
98
|
+
if (media && media.kind === "download") {
|
|
99
|
+
const download = result as DownloadResult;
|
|
100
|
+
const data =
|
|
101
|
+
download &&
|
|
102
|
+
typeof download === "object" &&
|
|
103
|
+
"data" in download
|
|
104
|
+
? (download as { data: unknown }).data
|
|
105
|
+
: download;
|
|
106
|
+
const contentType =
|
|
107
|
+
download &&
|
|
108
|
+
typeof download === "object" &&
|
|
109
|
+
"contentType" in download
|
|
110
|
+
? (download as { contentType: string }).contentType
|
|
111
|
+
: "application/octet-stream";
|
|
112
|
+
const filename =
|
|
113
|
+
download &&
|
|
114
|
+
typeof download === "object" &&
|
|
115
|
+
"filename" in download
|
|
116
|
+
? (download as { filename: string }).filename
|
|
117
|
+
: undefined;
|
|
118
|
+
|
|
119
|
+
const response = h.response(data);
|
|
120
|
+
response.type(contentType);
|
|
121
|
+
if (filename) {
|
|
122
|
+
response.header(
|
|
123
|
+
"Content-Disposition",
|
|
124
|
+
`attachment; filename="${filename}"`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return response;
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
} catch (error: unknown) {
|
|
131
|
+
const payload = buildErrorPayload(error);
|
|
132
|
+
const response = h.response(payload);
|
|
133
|
+
response.code(payload.status);
|
|
134
|
+
return response;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * as express from "./express";
|
|
2
|
+
export * as next from "./next";
|
|
3
|
+
export * as fastify from "./fastify";
|
|
4
|
+
export * as nest from "./nest";
|
|
5
|
+
export * as koa from "./koa";
|
|
6
|
+
export * as hapi from "./hapi";
|
|
7
|
+
export * as remix from "./remix";
|
|
8
|
+
export * as deno from "./deno";
|
|
9
|
+
export * as ws from "./ws";
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { createContract } from "../core/contract";
|
|
2
|
+
import { buildErrorPayload } from "../core/contract";
|
|
3
|
+
|
|
4
|
+
type AnyContract = ReturnType<typeof createContract>;
|
|
5
|
+
|
|
6
|
+
type MethodSchemaLike = {
|
|
7
|
+
media?: {
|
|
8
|
+
kind?: string;
|
|
9
|
+
contentTypes?: string[];
|
|
10
|
+
maxSize?: number;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type DownloadResult =
|
|
15
|
+
| {
|
|
16
|
+
data: unknown;
|
|
17
|
+
contentType?: string;
|
|
18
|
+
filename?: string;
|
|
19
|
+
}
|
|
20
|
+
| unknown;
|
|
21
|
+
|
|
22
|
+
interface KoaLikeContext {
|
|
23
|
+
params: Record<string, string>;
|
|
24
|
+
query: Record<string, string>;
|
|
25
|
+
request: { body?: unknown };
|
|
26
|
+
headers: Record<string, string>;
|
|
27
|
+
state?: { user?: unknown };
|
|
28
|
+
body?: unknown;
|
|
29
|
+
type?: string;
|
|
30
|
+
set: (key: string, value: string) => void;
|
|
31
|
+
status?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface KoaRouter {
|
|
35
|
+
register: (
|
|
36
|
+
path: string,
|
|
37
|
+
methods: string[],
|
|
38
|
+
middleware: (
|
|
39
|
+
ctx: KoaLikeContext,
|
|
40
|
+
next: () => Promise<unknown>
|
|
41
|
+
) => Promise<unknown>
|
|
42
|
+
) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function handleContract(
|
|
46
|
+
router: KoaRouter,
|
|
47
|
+
contract: AnyContract,
|
|
48
|
+
handlers: Record<
|
|
49
|
+
string,
|
|
50
|
+
(ctx: Record<string, unknown>) => unknown | Promise<unknown>
|
|
51
|
+
>
|
|
52
|
+
) {
|
|
53
|
+
const schema = contract.schema as Record<string, Record<string, unknown>>;
|
|
54
|
+
|
|
55
|
+
Object.keys(schema).forEach((route) => {
|
|
56
|
+
const methods = schema[route] as Record<string, unknown>;
|
|
57
|
+
|
|
58
|
+
Object.keys(methods).forEach((method) => {
|
|
59
|
+
const endpoint = `${method} ${route}`;
|
|
60
|
+
const implementation = handlers[endpoint];
|
|
61
|
+
const methodSchema = (methods as Record<string, unknown>)[
|
|
62
|
+
method
|
|
63
|
+
] as MethodSchemaLike | undefined;
|
|
64
|
+
|
|
65
|
+
if (!implementation) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const wrapped = contract.handle(endpoint, implementation);
|
|
70
|
+
|
|
71
|
+
// Koa Router register
|
|
72
|
+
router.register(
|
|
73
|
+
route,
|
|
74
|
+
[method],
|
|
75
|
+
async (ctx: KoaLikeContext, _next: () => Promise<unknown>) => {
|
|
76
|
+
try {
|
|
77
|
+
const context: Record<string, unknown> = {
|
|
78
|
+
params: ctx.params || {},
|
|
79
|
+
query: ctx.query || {},
|
|
80
|
+
body: ctx.request.body, // requires koa-bodyparser or koa-body
|
|
81
|
+
headers: ctx.headers || {},
|
|
82
|
+
user: ctx.state?.user, // common pattern
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const result = await wrapped(context);
|
|
86
|
+
const media = methodSchema?.media;
|
|
87
|
+
if (media && media.kind === "download") {
|
|
88
|
+
const download = result as DownloadResult;
|
|
89
|
+
const data =
|
|
90
|
+
download &&
|
|
91
|
+
typeof download === "object" &&
|
|
92
|
+
"data" in download
|
|
93
|
+
? (download as { data: unknown }).data
|
|
94
|
+
: download;
|
|
95
|
+
const contentType =
|
|
96
|
+
download &&
|
|
97
|
+
typeof download === "object" &&
|
|
98
|
+
"contentType" in download
|
|
99
|
+
? (download as { contentType: string }).contentType
|
|
100
|
+
: "application/octet-stream";
|
|
101
|
+
const filename =
|
|
102
|
+
download &&
|
|
103
|
+
typeof download === "object" &&
|
|
104
|
+
"filename" in download
|
|
105
|
+
? (download as { filename: string }).filename
|
|
106
|
+
: undefined;
|
|
107
|
+
|
|
108
|
+
ctx.body = data;
|
|
109
|
+
ctx.type = contentType;
|
|
110
|
+
if (filename) {
|
|
111
|
+
ctx.set(
|
|
112
|
+
"Content-Disposition",
|
|
113
|
+
`attachment; filename="${filename}"`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
ctx.body = result;
|
|
118
|
+
}
|
|
119
|
+
} catch (error: unknown) {
|
|
120
|
+
const payload = buildErrorPayload(error);
|
|
121
|
+
ctx.status = payload.status;
|
|
122
|
+
ctx.body = payload;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { createContract } from "../core/contract";
|
|
2
|
+
|
|
3
|
+
type AnyContract = ReturnType<typeof createContract>;
|
|
4
|
+
|
|
5
|
+
interface NestLikeRequest {
|
|
6
|
+
params: Record<string, string>;
|
|
7
|
+
query: Record<string, string>;
|
|
8
|
+
body: unknown;
|
|
9
|
+
headers: Record<string, string>;
|
|
10
|
+
user?: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface NestLikeResponse {
|
|
14
|
+
send?: (payload: unknown) => void;
|
|
15
|
+
json?: (payload: unknown) => void;
|
|
16
|
+
end: (payload: string) => void;
|
|
17
|
+
status?: (code: number) => void;
|
|
18
|
+
code?: (code: number) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type HttpMethodHandler = (
|
|
22
|
+
path: string,
|
|
23
|
+
handler: (
|
|
24
|
+
req: NestLikeRequest,
|
|
25
|
+
res: NestLikeResponse,
|
|
26
|
+
next: unknown
|
|
27
|
+
) => Promise<void>
|
|
28
|
+
) => void;
|
|
29
|
+
|
|
30
|
+
interface NestApp {
|
|
31
|
+
getHttpAdapter: () => {
|
|
32
|
+
getInstance: () => unknown;
|
|
33
|
+
getRequestMethod: (request: unknown) => string;
|
|
34
|
+
getRequestUrl: (request: unknown) => string;
|
|
35
|
+
createMiddlewareFactory: (method: unknown) => unknown;
|
|
36
|
+
get: HttpMethodHandler;
|
|
37
|
+
post: HttpMethodHandler;
|
|
38
|
+
put: HttpMethodHandler;
|
|
39
|
+
delete: HttpMethodHandler;
|
|
40
|
+
patch: HttpMethodHandler;
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const SchemaApiModule = {
|
|
46
|
+
register(
|
|
47
|
+
app: NestApp,
|
|
48
|
+
contract: AnyContract,
|
|
49
|
+
handlers: Record<
|
|
50
|
+
string,
|
|
51
|
+
(ctx: Record<string, unknown>) => unknown | Promise<unknown>
|
|
52
|
+
>
|
|
53
|
+
) {
|
|
54
|
+
const adapter = app.getHttpAdapter();
|
|
55
|
+
const schema = contract.schema as Record<string, Record<string, unknown>>;
|
|
56
|
+
|
|
57
|
+
Object.keys(schema).forEach((route) => {
|
|
58
|
+
const methods = schema[route] as Record<string, unknown>;
|
|
59
|
+
|
|
60
|
+
Object.keys(methods).forEach((method) => {
|
|
61
|
+
const endpoint = `${method} ${route}`;
|
|
62
|
+
const implementation = handlers[endpoint];
|
|
63
|
+
|
|
64
|
+
if (!implementation) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const wrapped = contract.handle(endpoint, implementation);
|
|
69
|
+
const methodLower = method.toLowerCase() as
|
|
70
|
+
| "get"
|
|
71
|
+
| "post"
|
|
72
|
+
| "put"
|
|
73
|
+
| "delete"
|
|
74
|
+
| "patch";
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
typeof adapter[methodLower] === "function"
|
|
78
|
+
) {
|
|
79
|
+
(adapter[methodLower] as HttpMethodHandler)(
|
|
80
|
+
route,
|
|
81
|
+
async (req: NestLikeRequest, res: NestLikeResponse, _next: unknown) => {
|
|
82
|
+
try {
|
|
83
|
+
// Intentamos normalizar request/response independientemente del driver (Express/Fastify)
|
|
84
|
+
// Nota: Esto asume Express por defecto para req.params/query/body.
|
|
85
|
+
// En Fastify, req.params/query/body también existen en el objeto Request estándar de Fastify.
|
|
86
|
+
|
|
87
|
+
const context: Record<string, unknown> = {
|
|
88
|
+
params: req.params || {},
|
|
89
|
+
query: req.query || {},
|
|
90
|
+
body: req.body,
|
|
91
|
+
headers: req.headers || {},
|
|
92
|
+
user: req.user,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const result = await wrapped(context);
|
|
96
|
+
|
|
97
|
+
// Responder
|
|
98
|
+
if (typeof res.send === "function") {
|
|
99
|
+
// Express / Fastify
|
|
100
|
+
res.send(result);
|
|
101
|
+
} else if (typeof res.json === "function") {
|
|
102
|
+
// Express alternative
|
|
103
|
+
res.json(result);
|
|
104
|
+
} else {
|
|
105
|
+
// Fallback simple
|
|
106
|
+
res.end(JSON.stringify(result));
|
|
107
|
+
}
|
|
108
|
+
} catch (error: unknown) {
|
|
109
|
+
const typedError = error as { message?: string; status?: number };
|
|
110
|
+
if (res.status) res.status(typedError.status || 500);
|
|
111
|
+
else if (res.code) res.code(typedError.status || 500); // Fastify
|
|
112
|
+
|
|
113
|
+
if (res.send) res.send({ error: typedError.message });
|
|
114
|
+
else res.end(JSON.stringify({ error: typedError.message }));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { createContract } from "../core/contract";
|
|
2
|
+
import { buildErrorPayload } from "../core/contract";
|
|
3
|
+
|
|
4
|
+
type AnyContract = ReturnType<typeof createContract>;
|
|
5
|
+
|
|
6
|
+
type MethodSchemaLike = {
|
|
7
|
+
media?: {
|
|
8
|
+
kind?: string;
|
|
9
|
+
contentTypes?: string[];
|
|
10
|
+
maxSize?: number;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type DownloadResult =
|
|
15
|
+
| {
|
|
16
|
+
data: unknown;
|
|
17
|
+
contentType?: string;
|
|
18
|
+
filename?: string;
|
|
19
|
+
}
|
|
20
|
+
| unknown;
|
|
21
|
+
|
|
22
|
+
// Tipos mínimos compatibles con Next.js App Router
|
|
23
|
+
interface NextRequest {
|
|
24
|
+
nextUrl: {
|
|
25
|
+
searchParams: URLSearchParams;
|
|
26
|
+
pathname: string;
|
|
27
|
+
};
|
|
28
|
+
json: () => Promise<unknown>;
|
|
29
|
+
headers: Headers;
|
|
30
|
+
method: string;
|
|
31
|
+
url: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface NextResponse {
|
|
35
|
+
json: (body: unknown, init?: { status: number }) => unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Helper para obtener NextResponse (asumiendo que el usuario lo tiene disponible o lo importamos dinámicamente si fuera un paquete real)
|
|
39
|
+
// Aquí simularemos una respuesta JSON estándar
|
|
40
|
+
function jsonResponse(data: unknown, status: number = 200) {
|
|
41
|
+
return new Response(JSON.stringify(data), {
|
|
42
|
+
status,
|
|
43
|
+
headers: { "Content-Type": "application/json" },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function handleContract(
|
|
48
|
+
contract: AnyContract,
|
|
49
|
+
handlers: Record<
|
|
50
|
+
string,
|
|
51
|
+
(ctx: Record<string, unknown>) => unknown | Promise<unknown>
|
|
52
|
+
>
|
|
53
|
+
) {
|
|
54
|
+
// Retorna un handler compatible con Route Handlers de Next.js (App Router)
|
|
55
|
+
// Uso: export const { GET, POST, PUT, DELETE } = adapters.next.handleContract(contract, handlers);
|
|
56
|
+
// en app/api/[...route]/route.ts
|
|
57
|
+
|
|
58
|
+
const dispatcher = async (
|
|
59
|
+
req: Request | NextRequest,
|
|
60
|
+
{ params }: { params?: Record<string, string> }
|
|
61
|
+
) => {
|
|
62
|
+
const url = new URL(req.url);
|
|
63
|
+
// En App Router [...route], params.route es un array. Pero aquí asumimos que el usuario mapea rutas específicas o catch-all.
|
|
64
|
+
// Si es catch-all, necesitamos reconstruir la ruta.
|
|
65
|
+
|
|
66
|
+
// Simplificación: Iteramos sobre el contrato para encontrar la ruta que coincida.
|
|
67
|
+
// Esto es ineficiente O(N) por request, pero funciona para validación.
|
|
68
|
+
// Una implementación más robusta usaría un router interno.
|
|
69
|
+
|
|
70
|
+
const method = req.method;
|
|
71
|
+
const path = url.pathname; // Ojo: esto incluye el prefijo /api si está ahí.
|
|
72
|
+
|
|
73
|
+
// Intentar hacer match con las rutas del contrato
|
|
74
|
+
const schema = contract.schema as Record<string, Record<string, unknown>>;
|
|
75
|
+
|
|
76
|
+
for (const routePattern of Object.keys(schema)) {
|
|
77
|
+
// Convertir /users/:id a regex simple
|
|
78
|
+
const regexPattern = routePattern.replace(/:[a-zA-Z0-9_]+/g, "([^/]+)");
|
|
79
|
+
const regex = new RegExp(`^.*${regexPattern}$`); // ^.* para permitir prefijos como /api
|
|
80
|
+
|
|
81
|
+
const match = path.match(regex);
|
|
82
|
+
if (match) {
|
|
83
|
+
// Verificar método
|
|
84
|
+
const routeMethods = schema[routePattern] as Record<
|
|
85
|
+
string,
|
|
86
|
+
MethodSchemaLike
|
|
87
|
+
>;
|
|
88
|
+
if (routeMethods[method]) {
|
|
89
|
+
const endpoint = `${method} ${routePattern}`;
|
|
90
|
+
const implementation = handlers[endpoint];
|
|
91
|
+
const methodSchema = routeMethods[method];
|
|
92
|
+
|
|
93
|
+
if (!implementation) return jsonResponse({ error: "Not Implemented" }, 501);
|
|
94
|
+
|
|
95
|
+
const contextParams = params || {};
|
|
96
|
+
|
|
97
|
+
const wrapped = contract.handle(endpoint, implementation);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// Parse body safely
|
|
101
|
+
let body = undefined;
|
|
102
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
103
|
+
try {
|
|
104
|
+
body = await req.json();
|
|
105
|
+
} catch {}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const context = {
|
|
109
|
+
params: contextParams,
|
|
110
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
111
|
+
body,
|
|
112
|
+
headers: Object.fromEntries(req.headers.entries()),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const result = await wrapped(context);
|
|
116
|
+
const media = methodSchema?.media;
|
|
117
|
+
if (media && media.kind === "download") {
|
|
118
|
+
const download = result as DownloadResult;
|
|
119
|
+
const hasData =
|
|
120
|
+
download &&
|
|
121
|
+
typeof download === "object" &&
|
|
122
|
+
Object.prototype.hasOwnProperty.call(download, "data");
|
|
123
|
+
const data = hasData
|
|
124
|
+
? (download as { data: unknown }).data
|
|
125
|
+
: download;
|
|
126
|
+
const contentType =
|
|
127
|
+
download &&
|
|
128
|
+
typeof download === "object" &&
|
|
129
|
+
"contentType" in download &&
|
|
130
|
+
typeof (download as { contentType?: unknown }).contentType ===
|
|
131
|
+
"string"
|
|
132
|
+
? (download as { contentType?: string }).contentType
|
|
133
|
+
: "application/octet-stream";
|
|
134
|
+
const filename =
|
|
135
|
+
download &&
|
|
136
|
+
typeof download === "object" &&
|
|
137
|
+
"filename" in download &&
|
|
138
|
+
typeof (download as { filename?: unknown }).filename ===
|
|
139
|
+
"string"
|
|
140
|
+
? (download as { filename?: string }).filename
|
|
141
|
+
: undefined;
|
|
142
|
+
|
|
143
|
+
const headers: Record<string, string> = {
|
|
144
|
+
"Content-Type": String(contentType),
|
|
145
|
+
};
|
|
146
|
+
if (filename) {
|
|
147
|
+
headers["Content-Disposition"] =
|
|
148
|
+
`attachment; filename="${filename}"`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return new Response(data as BodyInit, {
|
|
152
|
+
status: 200,
|
|
153
|
+
headers,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return jsonResponse(result);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
const payload = buildErrorPayload(err);
|
|
159
|
+
return jsonResponse(payload, payload.status);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return jsonResponse({ error: "Not Found" }, 404);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
GET: dispatcher,
|
|
170
|
+
POST: dispatcher,
|
|
171
|
+
PUT: dispatcher,
|
|
172
|
+
DELETE: dispatcher,
|
|
173
|
+
PATCH: dispatcher,
|
|
174
|
+
};
|
|
175
|
+
}
|