@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
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.
|
package/rollup.config.js
ADDED
|
@@ -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
|
+
}
|