@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.
Files changed (66) hide show
  1. package/.prettierignore +5 -0
  2. package/.prettierrc +7 -0
  3. package/bin/schemaapi +302 -0
  4. package/build.md +246 -0
  5. package/dist/core/contract.d.ts +4 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/schemaapi.cjs.js +13 -0
  8. package/dist/schemaapi.cjs.js.map +1 -0
  9. package/dist/schemaapi.esm.js +11 -0
  10. package/dist/schemaapi.esm.js.map +1 -0
  11. package/dist/schemaapi.umd.js +19 -0
  12. package/dist/schemaapi.umd.js.map +1 -0
  13. package/docs/adapters/deno.md +51 -0
  14. package/docs/adapters/express.md +67 -0
  15. package/docs/adapters/fastify.md +64 -0
  16. package/docs/adapters/hapi.md +67 -0
  17. package/docs/adapters/koa.md +61 -0
  18. package/docs/adapters/nest.md +66 -0
  19. package/docs/adapters/next.md +66 -0
  20. package/docs/adapters/remix.md +72 -0
  21. package/docs/cli.md +18 -0
  22. package/docs/consepts.md +18 -0
  23. package/docs/getting_started.md +149 -0
  24. package/docs/sdk.md +25 -0
  25. package/docs/validation.md +228 -0
  26. package/docs/versioning.md +28 -0
  27. package/eslint.config.mjs +34 -0
  28. package/estructure.md +55 -0
  29. package/libreria.md +319 -0
  30. package/package.json +61 -0
  31. package/readme.md +89 -0
  32. package/resumen.md +188 -0
  33. package/rollup.config.js +19 -0
  34. package/src/adapters/deno.ts +139 -0
  35. package/src/adapters/express.ts +134 -0
  36. package/src/adapters/fastify.ts +133 -0
  37. package/src/adapters/hapi.ts +140 -0
  38. package/src/adapters/index.ts +9 -0
  39. package/src/adapters/koa.ts +128 -0
  40. package/src/adapters/nest.ts +122 -0
  41. package/src/adapters/next.ts +175 -0
  42. package/src/adapters/remix.ts +145 -0
  43. package/src/adapters/ws.ts +132 -0
  44. package/src/core/client.ts +104 -0
  45. package/src/core/contract.ts +534 -0
  46. package/src/core/versioning.test.ts +174 -0
  47. package/src/docs.ts +535 -0
  48. package/src/index.ts +5 -0
  49. package/src/playground.test.ts +98 -0
  50. package/src/playground.ts +13 -0
  51. package/src/sdk.ts +17 -0
  52. package/tests/adapters.deno.test.ts +70 -0
  53. package/tests/adapters.express.test.ts +67 -0
  54. package/tests/adapters.fastify.test.ts +63 -0
  55. package/tests/adapters.hapi.test.ts +66 -0
  56. package/tests/adapters.koa.test.ts +58 -0
  57. package/tests/adapters.nest.test.ts +85 -0
  58. package/tests/adapters.next.test.ts +39 -0
  59. package/tests/adapters.remix.test.ts +52 -0
  60. package/tests/adapters.ws.test.ts +91 -0
  61. package/tests/cli.test.ts +156 -0
  62. package/tests/client.test.ts +110 -0
  63. package/tests/contract.handle.test.ts +267 -0
  64. package/tests/docs.test.ts +96 -0
  65. package/tests/sdk.test.ts +34 -0
  66. package/tsconfig.json +15 -0
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@codexsploitx/schemaapi",
3
+ "version": "1.0.0",
4
+ "description": "Type-safe API contracts (HTTP/WebSocket) with adapters, client and docs generator.",
5
+ "main": "dist/schemaapi.cjs.js",
6
+ "module": "dist/schemaapi.esm.js",
7
+ "browser": "dist/schemaapi.umd.js",
8
+ "types": "dist/index.d.ts",
9
+ "bin": {
10
+ "schemaapi": "bin/schemaapi"
11
+ },
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "scripts": {
16
+ "build": "rollup -c",
17
+ "dev": "echo 'Agrega Vite más adelante para dev rápido'",
18
+ "lint": "eslint .",
19
+ "lint:fix": "eslint . --fix",
20
+ "format": "prettier --write .",
21
+ "test": "vitest"
22
+ },
23
+ "keywords": [
24
+ "api",
25
+ "schema",
26
+ "zod",
27
+ "http",
28
+ "websocket",
29
+ "typescript"
30
+ ],
31
+ "author": "codexsploitx",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/CodexSploitx/SchemaApi.git"
35
+ },
36
+ "license": "ISC",
37
+ "type": "commonjs",
38
+ "dependencies": {
39
+ "@types/ws": "^8.18.1",
40
+ "tslib": "^2.8.1",
41
+ "ws": "^8.19.0",
42
+ "zod": "^4.3.5"
43
+ },
44
+ "devDependencies": {
45
+ "@eslint/js": "^9.39.2",
46
+ "@rollup/plugin-commonjs": "^29.0.0",
47
+ "@rollup/plugin-node-resolve": "^16.0.3",
48
+ "@rollup/plugin-typescript": "^12.3.0",
49
+ "@types/jest": "^30.0.0",
50
+ "eslint": "^9.39.2",
51
+ "eslint-config-prettier": "^10.1.8",
52
+ "globals": "^17.0.0",
53
+ "prettier": "^3.8.0",
54
+ "rollup": "^4.55.1",
55
+ "supertest": "^7.2.2",
56
+ "ts-node": "^10.9.2",
57
+ "typescript": "^5.9.3",
58
+ "typescript-eslint": "^8.53.0",
59
+ "vitest": "^4.0.17"
60
+ }
61
+ }
package/readme.md ADDED
@@ -0,0 +1,89 @@
1
+ # SchemaApi
2
+
3
+ > El Zod de las APIs
4
+ > No reemplaza tu stack. Lo hace confiable.
5
+
6
+ SchemaApi es una librería de **contratos para APIs**.
7
+ No sustituye Express, Fastify, Next.js, Axios ni Fetch.
8
+ Se coloca **encima** de ellos como una capa de validación, coherencia y arquitectura.
9
+
10
+ ---
11
+
12
+ ## 🧠 Filosofía
13
+
14
+ > “Tu API no es tu código.
15
+ > Tu API es el contrato que prometes al mundo.”
16
+
17
+ SchemaApi convierte ese contrato en:
18
+ - Código tipado
19
+ - Validación runtime
20
+ - Seguridad (roles y errores tipados)
21
+ - Documentación (vía CLI y contrato)
22
+ - SDK de cliente básico
23
+ - Tests (unitarios y de contrato)
24
+
25
+ ---
26
+
27
+ ## ✅ Lo que hace SchemaApi hoy
28
+
29
+ - Valida rutas, métodos HTTP y parámetros
30
+ - Valida query, body y headers
31
+ - Controla roles y autenticación
32
+ - Valida responses y errores (incluye status codes definidos en el contrato)
33
+ - Expone un contrato único fuente de verdad para docs/SDK/tests
34
+ - Ofrece un cliente JS/TS básico vía `createClient`
35
+ - Proporciona una CLI mínima (`schemaapi`) para orquestar generación de docs/SDK/tests
36
+ - Versionado de API con detección de breaking changes
37
+
38
+ ---
39
+
40
+ ## ❌ Lo que NO hace
41
+
42
+ - No transporta datos (usa Fetch / Axios)
43
+ - No reemplaza tu framework (Express / Next / Fastify)
44
+ - No maneja DB ni ORM
45
+ - No es un servidor
46
+
47
+ ---
48
+
49
+ ## 🌐 Instalación
50
+
51
+ ```bash
52
+ npm install schemaapi
53
+ # o
54
+ yarn add schemaapi
55
+
56
+
57
+ 🧩 Ejemplo básico
58
+
59
+ import { createContract } from "schemaapi";
60
+ import { z } from "zod";
61
+
62
+ export const contract = createContract({
63
+ "/users/:id": {
64
+ GET: {
65
+ params: z.object({ id: z.string().uuid() }),
66
+ headers: z.object({ authorization: z.string() }),
67
+ roles: ["user", "admin"],
68
+ response: z.object({
69
+ id: z.string(),
70
+ username: z.string(),
71
+ email: z.string().email()
72
+ }),
73
+ errors: { 401: "UNAUTHORIZED", 404: "USER_NOT_FOUND" }
74
+ }
75
+ }
76
+ });
77
+
78
+
79
+
80
+ 🛠 Integración con Express
81
+
82
+ app.get(
83
+ "/users/:id",
84
+ contract.handle("GET /users/:id", async (ctx) => {
85
+ const user = await db.findUser(ctx.params.id);
86
+ return user; // Validación automática
87
+ })
88
+ );
89
+ # SchemaApi
package/resumen.md ADDED
@@ -0,0 +1,188 @@
1
+ # SchemaApi - Checklist de Validaciones de Contract
2
+
3
+ > Guía completa y oficial de todas las validaciones que SchemaApi realiza sobre tus contratos de API.
4
+ > Ideal como checklist de desarrollo, QA o documentación interna.
5
+
6
+ ---
7
+
8
+ ## 1️⃣ Validaciones de método y ruta
9
+
10
+ | Validación | Descripción | Error típico |
11
+ |------------|------------|-------------|
12
+ | Método HTTP permitido | GET, POST, PUT, DELETE, PATCH, OPTIONS… | 405 Method Not Allowed |
13
+ | Existencia de la ruta | La ruta debe estar definida en el contrato | 404 Not Found |
14
+ | Parámetros dinámicos | `:id`, `:slug`, `*` deben declararse en `params` | 400 Bad Request |
15
+ | Métodos duplicados | Evita definir GET/POST duplicados en la misma ruta | Error en build/dev |
16
+ | Jerarquía de rutas | Rutas anidadas deben respetar la jerarquía | Error en build/dev |
17
+ | Versionado de rutas | Detecta breaking changes entre versiones | Warning/Error |
18
+
19
+ ---
20
+
21
+ ## 2️⃣ Validaciones de Path Params
22
+
23
+ | Validación | Descripción | Error típico |
24
+ |------------|------------|-------------|
25
+ | Tipo correcto | string, number, UUID, regex, enums | 400 Bad Request |
26
+ | Obligatorio/Optional | Parámetros marcados obligatorios deben llegar | 400 Bad Request |
27
+ | Formato | Regex, longitud mínima/máxima | 400 Bad Request |
28
+
29
+ **Ejemplo:**
30
+
31
+ ```ts
32
+ params: z.object({ id: z.string().uuid() })
33
+ 3️⃣ Validaciones de Query Params
34
+ Validación Descripción Error típico
35
+ Tipos correctos number, string, boolean, arrays 400 Bad Request
36
+ Obligatorio / Opcional Define si es requerido 400 Bad Request
37
+ Valores por defecto Permite fallback si no llega Correcto
38
+ Combinaciones dependientes Ej: sort + order 400 Bad Request
39
+ Arrays y objetos anidados Valida estructura compleja 400 Bad Request
40
+
41
+ 4️⃣ Validaciones de Body
42
+ Validación Descripción Error típico
43
+ Body obligatorio Según método HTTP 400 Bad Request
44
+ Campos obligatorios / opcionales Deben cumplir schema 400 Bad Request
45
+ Tipos de campos string, number, boolean, array, objeto 400 Bad Request
46
+ Valores por defecto Permite fallback Correcto
47
+ Estructuras anidadas Arrays y objetos complejos 400 Bad Request
48
+ Validaciones avanzadas Regex, min/max, custom validator 400 Bad Request
49
+
50
+ 5️⃣ Validaciones de Headers
51
+ Validación Descripción Error típico
52
+ Headers obligatorios Ej: Authorization, Content-Type 401/403
53
+ Tipos y formato string, number, regex 400/401
54
+ Valores predeterminados Si no llega header opcional Correcto
55
+ Combinaciones de headers Ej: X-Role + Authorization 400/403
56
+
57
+ 6️⃣ Roles y Autenticación
58
+ Validación Descripción Error típico
59
+ Rol requerido Endpoint solo accesible a roles definidos 403 Forbidden
60
+ Múltiples roles Permite más de un rol Correcto
61
+ Rol predeterminado Optional fallback Correcto
62
+ Integración JWT / sesiones Opcional, validación token 401/403
63
+ Scopes / permisos adicionales Validación más granular 403 Forbidden
64
+
65
+ 7️⃣ Responses y Status Codes
66
+ Validación Descripción Error típico
67
+ Schema de respuesta Debe cumplir schema exacto Error dev/test
68
+ Campos faltantes / extra Validación estricta Error dev/test
69
+ Status codes válidos Solo los definidos en errors Error dev/test
70
+ Arrays / objetos anidados Validación completa Error dev/test
71
+ Generación SDK / docs Automática a partir del schema N/A
72
+ Response opcional 204 Para métodos sin body Correcto
73
+
74
+ 8️⃣ Errores y excepciones
75
+ Validación Descripción Error típico
76
+ Todos los errores declarados Cada status code debe estar definido Error dev/test
77
+ Status code no definido Handler retorna status no declarado Error dev/test
78
+ Mensajes consistentes String o códigos Warning/Error
79
+ Custom error handlers Opcional, consistente Correcto
80
+ Errores globales Soporte de fallback Correcto
81
+
82
+ 9️⃣ Validaciones avanzadas / opcionales
83
+ Validación Descripción Error típico
84
+ Consistencia general Sin duplicados, campos obligatorios definidos Warning/Error
85
+ Comparación de versiones Detecta breaking changes Warning/Error
86
+ Validación condicional requiredIf / dependencias 400 Bad Request
87
+ Validación cross-endpoint Relaciones entre rutas Warning/Error
88
+ Observabilidad Logs automáticos de fallos N/A
89
+ Rate limiting lógico Throttling opcional 429 Too Many Requests
90
+ Payload / performance Tamaño máximo, response time 413 Payload Too Large
91
+
92
+ 🔟 Meta-validaciones y herramientas
93
+ Validación Descripción
94
+ Generación documentación Swagger / OpenAPI / Markdown
95
+ Generación SDK JS / TS tipado automático
96
+ Generación tests Unitarios por endpoint y errores
97
+ Compatibilidad versiones Detecta breaking changes entre versiones
98
+ Seguridad básica Validación headers sensibles / inyección
99
+
100
+ ✅ Resumen conceptual
101
+ SchemaApi puede validar todo lo que pueda romper la consistencia o confiabilidad de tu API:
102
+
103
+ Desde lo más básico (método, ruta, params, body)
104
+
105
+ Hasta avanzado (roles, versionado, dependencias cross-endpoint)
106
+
107
+ Esto convierte tu librería en una capa de contrato potente, que asegura que la API siempre se use de forma correcta antes de tocar la red o base de datos.
108
+
109
+
110
+ # SchemaApi - Flujo de Validación de Contract
111
+
112
+ > Diagrama visual del flujo completo de validaciones en SchemaApi.
113
+ > Muestra qué se valida en cada capa antes de ejecutar un endpoint.
114
+
115
+ ---
116
+
117
+ ## Flujo de Validación
118
+
119
+ ```mermaid
120
+ flowchart TD
121
+ A[Inicio: Llamada API] --> B{Ruta existe?}
122
+ B -- No --> C[404 Not Found]
123
+ B -- Sí --> D{Método HTTP permitido?}
124
+ D -- No --> E[405 Method Not Allowed]
125
+ D -- Sí --> F{Path Params válidos?}
126
+ F -- No --> G[400 Bad Request - Invalid Path Param]
127
+ F -- Sí --> H{Query Params válidos?}
128
+ H -- No --> I[400 Bad Request - Invalid Query Param]
129
+ H -- Sí --> J{Body válido?}
130
+ J -- No --> K[400 Bad Request - Invalid Body]
131
+ J -- Sí --> L{Headers válidos?}
132
+ L -- No --> M[401/403 - Invalid Headers]
133
+ L -- Sí --> N{Rol autorizado?}
134
+ N -- No --> O[403 Forbidden]
135
+ N -- Sí --> P[Ejecutar Handler]
136
+ P --> Q{Response cumple schema?}
137
+ Q -- No --> R[Error Dev/Test - Invalid Response]
138
+ Q -- Sí --> S{Status Code permitido?}
139
+ S -- No --> T[Error Dev/Test - Status Code No Definido]
140
+ S -- Sí --> U[Respuesta exitosa al cliente]
141
+ 🔹 Descripción del flujo
142
+ Ruta existe: Verifica que la ruta solicitada esté definida en el contrato.
143
+
144
+ Método HTTP permitido: Solo se aceptan los métodos definidos (GET, POST, etc.).
145
+
146
+ Path Params: Validación de tipos y obligatoriedad.
147
+
148
+ Query Params: Validación de tipos, opcionales/obligatorios y combinaciones.
149
+
150
+ Body: Validación de estructura, tipos, campos obligatorios y avanzados (regex, min/max, arrays/objetos).
151
+
152
+ Headers: Obligatorios y formato correcto.
153
+
154
+ Roles / permisos: Usuario debe tener rol autorizado.
155
+
156
+ Ejecutar Handler: Si todo es válido, se ejecuta la función del endpoint.
157
+
158
+ Response: Debe cumplir el schema exacto definido.
159
+
160
+ Status Code: Debe estar declarado en errors si no es 2xx.
161
+
162
+ Salida: La respuesta final al cliente es 100% validada.
163
+
164
+ 🔹 Tipos de errores detectados
165
+ Error Capa Código típico
166
+ Ruta no existe Ruta 404
167
+ Método no permitido Método 405
168
+ Path Param inválido Path Params 400
169
+ Query Param inválido Query Params 400
170
+ Body inválido Body 400
171
+ Headers inválidos Headers 401 / 403
172
+ Rol no autorizado Roles 403
173
+ Response inválida Response Dev/Test Error
174
+ Status code no declarado Status Dev/Test Error
175
+
176
+ 🔹 Beneficios de este flujo
177
+ Detecta errores antes de que lleguen a producción
178
+
179
+ Garantiza consistencia de contratos
180
+
181
+ Permite SDK tipado y generación de documentación automática
182
+
183
+ Facilita QA y testing: cada error es predecible y controlado
184
+
185
+ Posibilita versionado y detección de breaking changes
186
+
187
+ 💡 Tip visual:
188
+ Puedes integrar este diagrama con herramientas como Mermaid.js en docs, para que sea interactivo y siempre actualizado según tu contrato.
@@ -0,0 +1,19 @@
1
+ const typescript = require('@rollup/plugin-typescript');
2
+ const { nodeResolve } = require('@rollup/plugin-node-resolve');
3
+ const commonjs = require('@rollup/plugin-commonjs');
4
+ const pkg = require('./package.json');
5
+
6
+ module.exports = {
7
+ input: 'src/index.ts',
8
+ output: [
9
+ { file: pkg.main, format: 'cjs', sourcemap: true },
10
+ { file: pkg.module, format: 'esm', sourcemap: true },
11
+ { file: pkg.browser, format: 'umd', name: 'SchemaApi', sourcemap: true }
12
+ ],
13
+ external: ['zod'],
14
+ plugins: [
15
+ nodeResolve(),
16
+ commonjs(),
17
+ typescript({ tsconfig: './tsconfig.json', declaration: true, rootDir: 'src', outDir: 'dist' }),
18
+ ]
19
+ };
@@ -0,0 +1,139 @@
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
+ }
@@ -0,0 +1,134 @@
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
+ }