@codexsploitx/schemaapi 1.0.0 → 1.0.2
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/LICENSE +21 -0
- package/package.json +1 -1
- package/readme.md +125 -48
- package/docs/adapters/deno.md +0 -51
- package/docs/adapters/express.md +0 -67
- package/docs/adapters/fastify.md +0 -64
- package/docs/adapters/hapi.md +0 -67
- package/docs/adapters/koa.md +0 -61
- package/docs/adapters/nest.md +0 -66
- package/docs/adapters/next.md +0 -66
- package/docs/adapters/remix.md +0 -72
- package/docs/cli.md +0 -18
- package/docs/consepts.md +0 -18
- package/docs/getting_started.md +0 -149
- package/docs/sdk.md +0 -25
- package/docs/validation.md +0 -228
- package/docs/versioning.md +0 -28
- package/eslint.config.mjs +0 -34
- package/rollup.config.js +0 -19
- package/src/adapters/deno.ts +0 -139
- package/src/adapters/express.ts +0 -134
- package/src/adapters/fastify.ts +0 -133
- package/src/adapters/hapi.ts +0 -140
- package/src/adapters/index.ts +0 -9
- package/src/adapters/koa.ts +0 -128
- package/src/adapters/nest.ts +0 -122
- package/src/adapters/next.ts +0 -175
- package/src/adapters/remix.ts +0 -145
- package/src/adapters/ws.ts +0 -132
- package/src/core/client.ts +0 -104
- package/src/core/contract.ts +0 -534
- package/src/core/versioning.test.ts +0 -174
- package/src/docs.ts +0 -535
- package/src/index.ts +0 -5
- package/src/playground.test.ts +0 -98
- package/src/playground.ts +0 -13
- package/src/sdk.ts +0 -17
- package/tests/adapters.deno.test.ts +0 -70
- package/tests/adapters.express.test.ts +0 -67
- package/tests/adapters.fastify.test.ts +0 -63
- package/tests/adapters.hapi.test.ts +0 -66
- package/tests/adapters.koa.test.ts +0 -58
- package/tests/adapters.nest.test.ts +0 -85
- package/tests/adapters.next.test.ts +0 -39
- package/tests/adapters.remix.test.ts +0 -52
- package/tests/adapters.ws.test.ts +0 -91
- package/tests/cli.test.ts +0 -156
- package/tests/client.test.ts +0 -110
- package/tests/contract.handle.test.ts +0 -267
- package/tests/docs.test.ts +0 -96
- package/tests/sdk.test.ts +0 -34
- package/tsconfig.json +0 -15
package/src/adapters/deno.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
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
|
-
export function handleContract(
|
|
23
|
-
contract: AnyContract,
|
|
24
|
-
handlers: Record<
|
|
25
|
-
string,
|
|
26
|
-
(ctx: Record<string, unknown>) => unknown | Promise<unknown>
|
|
27
|
-
>
|
|
28
|
-
) {
|
|
29
|
-
// Retorna una función (req: Request) => Promise<Response> compatible con Deno.serve
|
|
30
|
-
return async (req: Request): Promise<Response> => {
|
|
31
|
-
const url = new URL(req.url);
|
|
32
|
-
const path = url.pathname;
|
|
33
|
-
const method = req.method;
|
|
34
|
-
|
|
35
|
-
const schema = contract.schema as Record<string, Record<string, unknown>>;
|
|
36
|
-
|
|
37
|
-
for (const routePattern of Object.keys(schema)) {
|
|
38
|
-
// Regex simple para matching: /users/:id -> /users/([^/]+)
|
|
39
|
-
// Agregamos ^ y $ para match exacto
|
|
40
|
-
const regexPattern = routePattern.replace(/:[a-zA-Z0-9_]+/g, "([^/]+)");
|
|
41
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
42
|
-
|
|
43
|
-
const match = path.match(regex);
|
|
44
|
-
if (match) {
|
|
45
|
-
const routeMethods = schema[routePattern] as Record<
|
|
46
|
-
string,
|
|
47
|
-
unknown
|
|
48
|
-
>;
|
|
49
|
-
if (routeMethods[method]) {
|
|
50
|
-
const endpoint = `${method} ${routePattern}`;
|
|
51
|
-
const implementation = handlers[endpoint];
|
|
52
|
-
const methodSchema =
|
|
53
|
-
(routeMethods as Record<string, unknown>)[method] as MethodSchemaLike | undefined;
|
|
54
|
-
|
|
55
|
-
if (!implementation) {
|
|
56
|
-
return new Response("Not Implemented", { status: 501 });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Extraer params
|
|
60
|
-
// Necesitamos saber los nombres de los parámetros para mapearlos
|
|
61
|
-
// routePattern: /users/:id/posts/:postId
|
|
62
|
-
// match: [..., "123", "456"]
|
|
63
|
-
const paramNames = (routePattern.match(/:[a-zA-Z0-9_]+/g) || []).map(
|
|
64
|
-
(p) => p.substring(1)
|
|
65
|
-
);
|
|
66
|
-
const params: Record<string, string> = {};
|
|
67
|
-
paramNames.forEach((name, index) => {
|
|
68
|
-
params[name] = match[index + 1];
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const wrapped = contract.handle(endpoint, implementation);
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
let body = undefined;
|
|
75
|
-
if (method !== "GET" && method !== "HEAD") {
|
|
76
|
-
try {
|
|
77
|
-
body = await req.json();
|
|
78
|
-
} catch {}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const context = {
|
|
82
|
-
params,
|
|
83
|
-
query: Object.fromEntries(url.searchParams.entries()),
|
|
84
|
-
body,
|
|
85
|
-
headers: Object.fromEntries(req.headers.entries()),
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const result = await wrapped(context);
|
|
89
|
-
const media = methodSchema?.media;
|
|
90
|
-
if (media && media.kind === "download") {
|
|
91
|
-
const download = result as DownloadResult;
|
|
92
|
-
const data =
|
|
93
|
-
download &&
|
|
94
|
-
typeof download === "object" &&
|
|
95
|
-
"data" in download
|
|
96
|
-
? (download as { data: unknown }).data
|
|
97
|
-
: download;
|
|
98
|
-
const contentType =
|
|
99
|
-
download &&
|
|
100
|
-
typeof download === "object" &&
|
|
101
|
-
"contentType" in download
|
|
102
|
-
? (download as { contentType: string }).contentType
|
|
103
|
-
: "application/octet-stream";
|
|
104
|
-
const filename =
|
|
105
|
-
download &&
|
|
106
|
-
typeof download === "object" &&
|
|
107
|
-
"filename" in download
|
|
108
|
-
? (download as { filename: string }).filename
|
|
109
|
-
: undefined;
|
|
110
|
-
|
|
111
|
-
const headers: Record<string, string> = {
|
|
112
|
-
"Content-Type": String(contentType),
|
|
113
|
-
};
|
|
114
|
-
if (filename) {
|
|
115
|
-
headers["Content-Disposition"] = `attachment; filename="${filename}"`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return new Response(data as BodyInit, {
|
|
119
|
-
headers,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return new Response(JSON.stringify(result), {
|
|
124
|
-
headers: { "Content-Type": "application/json" },
|
|
125
|
-
});
|
|
126
|
-
} catch (err: unknown) {
|
|
127
|
-
const payload = buildErrorPayload(err);
|
|
128
|
-
return new Response(JSON.stringify(payload), {
|
|
129
|
-
status: payload.status,
|
|
130
|
-
headers: { "Content-Type": "application/json" },
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return new Response("Not Found", { status: 404 });
|
|
138
|
-
};
|
|
139
|
-
}
|
package/src/adapters/express.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import type { createContract } from "../core/contract";
|
|
2
|
-
|
|
3
|
-
type AnyContract = ReturnType<typeof createContract>;
|
|
4
|
-
|
|
5
|
-
type MethodSchemaLike = {
|
|
6
|
-
media?: {
|
|
7
|
-
kind?: string;
|
|
8
|
-
contentTypes?: string[];
|
|
9
|
-
maxSize?: number;
|
|
10
|
-
};
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
type DownloadResult =
|
|
14
|
-
| {
|
|
15
|
-
data: unknown;
|
|
16
|
-
contentType?: string;
|
|
17
|
-
filename?: string;
|
|
18
|
-
}
|
|
19
|
-
| unknown;
|
|
20
|
-
|
|
21
|
-
export interface ExpressLikeRequest {
|
|
22
|
-
params: Record<string, string>;
|
|
23
|
-
query: Record<string, string>;
|
|
24
|
-
body: unknown;
|
|
25
|
-
headers: Record<string, string>;
|
|
26
|
-
user?: unknown;
|
|
27
|
-
[key: string]: unknown;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface ExpressLikeResponse {
|
|
31
|
-
json: (body: unknown) => void;
|
|
32
|
-
send?: (body: unknown) => void;
|
|
33
|
-
setHeader?: (name: string, value: string) => void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type ExpressLikeHandler = (
|
|
37
|
-
req: ExpressLikeRequest,
|
|
38
|
-
res: ExpressLikeResponse,
|
|
39
|
-
next: (err?: unknown) => void
|
|
40
|
-
) => void | Promise<void>;
|
|
41
|
-
|
|
42
|
-
export type ExpressLikeApp = {
|
|
43
|
-
[method: string]: (path: string, handler: ExpressLikeHandler) => void;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export function handleContract(
|
|
47
|
-
app: ExpressLikeApp,
|
|
48
|
-
contract: AnyContract,
|
|
49
|
-
handlers: Record<
|
|
50
|
-
string,
|
|
51
|
-
(ctx: Record<string, unknown>) => unknown | Promise<unknown>
|
|
52
|
-
>
|
|
53
|
-
) {
|
|
54
|
-
const schema = contract.schema as Record<string, Record<string, unknown>>;
|
|
55
|
-
|
|
56
|
-
Object.keys(schema).forEach((route) => {
|
|
57
|
-
const methods = schema[route] as Record<string, unknown>;
|
|
58
|
-
|
|
59
|
-
Object.keys(methods).forEach((method) => {
|
|
60
|
-
const endpoint = `${method} ${route}`;
|
|
61
|
-
const implementation = handlers[endpoint];
|
|
62
|
-
const methodSchema = (methods as Record<string, unknown>)[
|
|
63
|
-
method
|
|
64
|
-
] as MethodSchemaLike | undefined;
|
|
65
|
-
|
|
66
|
-
if (!implementation) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const wrapped = contract.handle(endpoint, implementation);
|
|
71
|
-
const httpMethod = method.toLowerCase();
|
|
72
|
-
const register = (app as Record<string, unknown>)[
|
|
73
|
-
httpMethod
|
|
74
|
-
] as ExpressLikeApp[keyof ExpressLikeApp] | undefined;
|
|
75
|
-
|
|
76
|
-
if (!register) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
register(route, async (req, res, next) => {
|
|
81
|
-
try {
|
|
82
|
-
const context: Record<string, unknown> = {
|
|
83
|
-
params: req.params || {},
|
|
84
|
-
query: req.query || {},
|
|
85
|
-
body: req.body,
|
|
86
|
-
headers: req.headers || {},
|
|
87
|
-
user: req.user,
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const result = await wrapped(context);
|
|
91
|
-
const media = methodSchema?.media;
|
|
92
|
-
if (media && media.kind === "download") {
|
|
93
|
-
const download = result as DownloadResult;
|
|
94
|
-
const data =
|
|
95
|
-
download &&
|
|
96
|
-
typeof download === "object" &&
|
|
97
|
-
"data" in download
|
|
98
|
-
? (download as { data: unknown }).data
|
|
99
|
-
: download;
|
|
100
|
-
const contentType =
|
|
101
|
-
download &&
|
|
102
|
-
typeof download === "object" &&
|
|
103
|
-
"contentType" in download
|
|
104
|
-
? (download as { contentType: string }).contentType
|
|
105
|
-
: "application/octet-stream";
|
|
106
|
-
const filename =
|
|
107
|
-
download &&
|
|
108
|
-
typeof download === "object" &&
|
|
109
|
-
"filename" in download
|
|
110
|
-
? (download as { filename: string }).filename
|
|
111
|
-
: undefined;
|
|
112
|
-
|
|
113
|
-
if (typeof res.setHeader === "function") {
|
|
114
|
-
res.setHeader("Content-Type", contentType);
|
|
115
|
-
if (filename) {
|
|
116
|
-
res.setHeader(
|
|
117
|
-
"Content-Disposition",
|
|
118
|
-
`attachment; filename="${filename}"`
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
if (typeof res.send === "function") {
|
|
123
|
-
res.send(data);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
res.json(result);
|
|
128
|
-
} catch (error) {
|
|
129
|
-
next(error);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
}
|
package/src/adapters/fastify.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
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
|
-
}
|
package/src/adapters/hapi.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
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
|
-
}
|
package/src/adapters/index.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
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";
|
package/src/adapters/koa.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
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
|
-
}
|