@codexsploitx/schemaapi 1.0.0 → 1.0.1

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 (50) hide show
  1. package/package.json +1 -1
  2. package/docs/adapters/deno.md +0 -51
  3. package/docs/adapters/express.md +0 -67
  4. package/docs/adapters/fastify.md +0 -64
  5. package/docs/adapters/hapi.md +0 -67
  6. package/docs/adapters/koa.md +0 -61
  7. package/docs/adapters/nest.md +0 -66
  8. package/docs/adapters/next.md +0 -66
  9. package/docs/adapters/remix.md +0 -72
  10. package/docs/cli.md +0 -18
  11. package/docs/consepts.md +0 -18
  12. package/docs/getting_started.md +0 -149
  13. package/docs/sdk.md +0 -25
  14. package/docs/validation.md +0 -228
  15. package/docs/versioning.md +0 -28
  16. package/eslint.config.mjs +0 -34
  17. package/rollup.config.js +0 -19
  18. package/src/adapters/deno.ts +0 -139
  19. package/src/adapters/express.ts +0 -134
  20. package/src/adapters/fastify.ts +0 -133
  21. package/src/adapters/hapi.ts +0 -140
  22. package/src/adapters/index.ts +0 -9
  23. package/src/adapters/koa.ts +0 -128
  24. package/src/adapters/nest.ts +0 -122
  25. package/src/adapters/next.ts +0 -175
  26. package/src/adapters/remix.ts +0 -145
  27. package/src/adapters/ws.ts +0 -132
  28. package/src/core/client.ts +0 -104
  29. package/src/core/contract.ts +0 -534
  30. package/src/core/versioning.test.ts +0 -174
  31. package/src/docs.ts +0 -535
  32. package/src/index.ts +0 -5
  33. package/src/playground.test.ts +0 -98
  34. package/src/playground.ts +0 -13
  35. package/src/sdk.ts +0 -17
  36. package/tests/adapters.deno.test.ts +0 -70
  37. package/tests/adapters.express.test.ts +0 -67
  38. package/tests/adapters.fastify.test.ts +0 -63
  39. package/tests/adapters.hapi.test.ts +0 -66
  40. package/tests/adapters.koa.test.ts +0 -58
  41. package/tests/adapters.nest.test.ts +0 -85
  42. package/tests/adapters.next.test.ts +0 -39
  43. package/tests/adapters.remix.test.ts +0 -52
  44. package/tests/adapters.ws.test.ts +0 -91
  45. package/tests/cli.test.ts +0 -156
  46. package/tests/client.test.ts +0 -110
  47. package/tests/contract.handle.test.ts +0 -267
  48. package/tests/docs.test.ts +0 -96
  49. package/tests/sdk.test.ts +0 -34
  50. package/tsconfig.json +0 -15
@@ -1,175 +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
- // 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
- }
@@ -1,145 +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 RemixParams = Record<string, string | undefined>;
7
-
8
- type MethodSchemaLike = {
9
- media?: {
10
- kind?: string;
11
- contentTypes?: string[];
12
- maxSize?: number;
13
- };
14
- };
15
-
16
- type DownloadResult =
17
- | {
18
- data: unknown;
19
- contentType?: string;
20
- filename?: string;
21
- }
22
- | unknown;
23
-
24
- export function createRouteHandlers(
25
- contract: AnyContract,
26
- handlers: Record<
27
- string,
28
- (ctx: Record<string, unknown>) => unknown | Promise<unknown>
29
- >,
30
- routePattern: string
31
- ) {
32
- const schema = contract.schema as Record<string, Record<string, unknown>>;
33
- const methods = schema[routePattern] as
34
- | Record<string, MethodSchemaLike>
35
- | undefined;
36
-
37
- if (!methods) {
38
- const notFound = () => {
39
- throw new Response("Not Found in Contract", { status: 404 });
40
- };
41
- return { loader: notFound, action: notFound };
42
- }
43
-
44
- const handleRequest = async (request: Request, params: RemixParams) => {
45
- const method = request.method;
46
- const endpoint = `${method} ${routePattern}`;
47
- const implementation = handlers[endpoint];
48
- const methodSchema = methods && methods[method];
49
-
50
- if (!implementation) {
51
- throw new Response("Method Not Implemented", { status: 501 });
52
- }
53
-
54
- const wrapped = contract.handle(endpoint, implementation);
55
-
56
- try {
57
- let body = undefined;
58
- // Parse body only for non-GET/HEAD methods
59
- if (method !== "GET" && method !== "HEAD") {
60
- try {
61
- body = await request.json();
62
- } catch {
63
- // Si falla json, tal vez es FormData? Remix usa mucho FormData.
64
- // Por simplicidad, aquí asumimos JSON como el estándar de SchemaApi.
65
- // Si se requiere FormData, se debería procesar antes o aquí.
66
- }
67
- }
68
-
69
- const url = new URL(request.url);
70
- const context: Record<string, unknown> = {
71
- params: params || {},
72
- query: Object.fromEntries(url.searchParams.entries()),
73
- body,
74
- headers: Object.fromEntries(request.headers.entries()),
75
- };
76
-
77
- const result = await wrapped(context);
78
- const media = methodSchema?.media;
79
- if (media && media.kind === "download") {
80
- const download = result as DownloadResult;
81
- const hasData =
82
- download &&
83
- typeof download === "object" &&
84
- Object.prototype.hasOwnProperty.call(download, "data");
85
- const data = hasData
86
- ? (download as { data: unknown }).data
87
- : download;
88
- const contentType =
89
- download &&
90
- typeof download === "object" &&
91
- "contentType" in download &&
92
- typeof (download as { contentType?: unknown }).contentType ===
93
- "string"
94
- ? (download as { contentType?: string }).contentType
95
- : "application/octet-stream";
96
- const filename =
97
- download &&
98
- typeof download === "object" &&
99
- "filename" in download &&
100
- typeof (download as { filename?: unknown }).filename === "string"
101
- ? (download as { filename?: string }).filename
102
- : undefined;
103
-
104
- const headers: Record<string, string> = {
105
- "Content-Type": String(contentType),
106
- };
107
- if (filename) {
108
- headers["Content-Disposition"] = `attachment; filename="${filename}"`;
109
- }
110
-
111
- return new Response(data as BodyInit, {
112
- headers,
113
- });
114
- }
115
-
116
- return new Response(JSON.stringify(result), {
117
- headers: { "Content-Type": "application/json" },
118
- });
119
- } catch (err) {
120
- const payload = buildErrorPayload(err);
121
- return new Response(JSON.stringify(payload), {
122
- status: payload.status,
123
- headers: { "Content-Type": "application/json" },
124
- });
125
- }
126
- };
127
-
128
- const loader = async (args: { request: Request; params: RemixParams }) => {
129
- if (methods["GET"]) {
130
- return handleRequest(args.request, args.params);
131
- }
132
- // Si no hay GET definido pero se llama al loader
133
- throw new Response("Method Not Allowed", { status: 405 });
134
- };
135
-
136
- const action = async (args: { request: Request; params: RemixParams }) => {
137
- const method = args.request.method;
138
- if (methods[method]) {
139
- return handleRequest(args.request, args.params);
140
- }
141
- throw new Response("Method Not Allowed", { status: 405 });
142
- };
143
-
144
- return { loader, action };
145
- }
@@ -1,132 +0,0 @@
1
- import { WebSocketServer, WebSocket } from "ws";
2
- import type { IncomingMessage } from "http";
3
- import type { createContract } from "../core/contract";
4
-
5
- type AnyContract = ReturnType<typeof createContract>;
6
-
7
- type WsSchemaDefinition = {
8
- params?: unknown;
9
- query?: unknown;
10
- serverMessages?: {
11
- parse: (value: unknown) => unknown;
12
- };
13
- clientMessages?: {
14
- parse: (value: unknown) => unknown;
15
- };
16
- };
17
-
18
- type WsHandlerContext = {
19
- params: Record<string, string>;
20
- query: Record<string, string>;
21
- headers: IncomingMessage["headers"];
22
- ws: WebSocket;
23
- send: (data: unknown) => void;
24
- onMessage: (cb: (data: unknown) => void) => void;
25
- };
26
-
27
- export function handleContract(
28
- wss: WebSocketServer,
29
- contract: AnyContract,
30
- handlers: Record<string, (ctx: WsHandlerContext) => void | Promise<void>>
31
- ) {
32
- const schema = contract.schema as Record<string, Record<string, unknown>>;
33
-
34
- wss.on("connection", async (ws: WebSocket, req: IncomingMessage) => {
35
- const url = req.url ? new URL(req.url, "http://localhost") : null;
36
- if (!url) {
37
- ws.close(1002, "Invalid URL");
38
- return;
39
- }
40
-
41
- const path = url.pathname;
42
- let matched = false;
43
-
44
- for (const routePattern of Object.keys(schema)) {
45
- const methods = schema[routePattern] as Record<string, unknown>;
46
- const wsDef = methods["WS"] as WsSchemaDefinition | undefined;
47
- if (!wsDef) continue;
48
-
49
- // Match path
50
- const regexPattern = routePattern.replace(/:[a-zA-Z0-9_]+/g, "([^/]+)");
51
- const regex = new RegExp(`^${regexPattern}$`);
52
- const match = path.match(regex);
53
-
54
- if (match) {
55
- matched = true;
56
- const implementation = handlers[`WS ${routePattern}`];
57
- if (!implementation) {
58
- ws.close(1011, "Handler not found");
59
- return;
60
- }
61
-
62
- // Extract params
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 query = Object.fromEntries(url.searchParams.entries());
72
-
73
- const context: WsHandlerContext = {
74
- params,
75
- query,
76
- headers: req.headers,
77
- ws,
78
- send: (data: unknown) => {
79
- if (wsDef.serverMessages) {
80
- try {
81
- wsDef.serverMessages.parse(data);
82
- } catch (e) {
83
- console.error("Server message validation failed", e);
84
- }
85
- }
86
- ws.send(JSON.stringify(data));
87
- },
88
- onMessage: (cb: (data: unknown) => void) => {
89
- ws.on("message", (raw) => {
90
- try {
91
- const json = JSON.parse(raw.toString());
92
- if (wsDef.clientMessages) {
93
- try {
94
- const parsed = wsDef.clientMessages.parse(json);
95
- cb(parsed);
96
- } catch (e) {
97
- console.error("Client message validation failed", e);
98
- ws.send(
99
- JSON.stringify({
100
- error: "Invalid Message Schema",
101
- details: e,
102
- })
103
- );
104
- }
105
- } else {
106
- cb(json);
107
- }
108
- } catch (e) {
109
- console.error("Invalid JSON", e);
110
- }
111
- });
112
- },
113
- };
114
-
115
- try {
116
- await implementation(context);
117
- } catch (e) {
118
- console.error("Error in WS handler", e);
119
- ws.close(1011, "Internal Error");
120
- }
121
-
122
- break;
123
- }
124
- }
125
-
126
- if (!matched) {
127
- ws.close(4404, "Not Found");
128
- }
129
- });
130
-
131
- return wss;
132
- }
@@ -1,104 +0,0 @@
1
- // Definimos tipos básicos para simular la estructura del cliente
2
- // En una implementación real, estos tipos se inferirían recursivamente del esquema T
3
-
4
- type WebSocketLike = {
5
- close: (code?: number, reason?: string) => void;
6
- };
7
-
8
- type WebSocketConstructor = new (url: string) => WebSocketLike;
9
-
10
- type ClientConfig = {
11
- baseUrl: string;
12
- fetch?: typeof fetch;
13
- WebSocket?: WebSocketConstructor;
14
- };
15
-
16
- export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
17
-
18
- export type ExtractSchema<TContract> = TContract;
19
-
20
- export type InferResponse<TContract, Endpoint extends string> = unknown;
21
-
22
- type RequestParams = {
23
- id?: string;
24
- body?: unknown;
25
- headers?: Record<string, string>;
26
- [key: string]: unknown;
27
- };
28
-
29
- export function createClient<T>(contract: { schema: T }, config: ClientConfig) {
30
- const { baseUrl, fetch: customFetch } = config;
31
- const fetchFn = customFetch || globalThis.fetch;
32
-
33
- return new Proxy({} as Record<string, unknown>, {
34
- get(_target, resource: string) {
35
- return new Proxy({} as Record<string, unknown>, {
36
- get(_target, operation: string) {
37
- // operation podría ser 'get', 'post', 'put', 'delete', 'ws', etc.
38
- // resource sería 'users', 'posts', etc.
39
-
40
- return async (params: RequestParams = {}) => {
41
- const method = operation.toUpperCase();
42
- let path = `/${resource}`;
43
-
44
- // Si hay params.id, asumimos que va en la URL
45
- if (params.id) {
46
- path += `/${params.id}`;
47
- }
48
-
49
- // Manejo de WebSocket
50
- if (method === "WS") {
51
- const wsUrlStr = baseUrl.replace(/^http/, "ws");
52
- const url = new URL(`${wsUrlStr}${path}`);
53
-
54
- // Agregar query params
55
- Object.keys(params).forEach(key => {
56
- if (key !== 'id' && key !== 'headers' && key !== 'body') {
57
- url.searchParams.append(key, String(params[key]));
58
- }
59
- });
60
-
61
- type GlobalWithWebSocket = typeof globalThis & {
62
- WebSocket?: WebSocketConstructor;
63
- };
64
- const globalWithWebSocket = globalThis as GlobalWithWebSocket;
65
- const WebSocketCtor =
66
- config.WebSocket || globalWithWebSocket.WebSocket;
67
- if (!WebSocketCtor) {
68
- throw new Error("WebSocket implementation not found. Please provide it in config.");
69
- }
70
-
71
- return new WebSocketCtor(url.toString());
72
- }
73
-
74
- const url = new URL(`${baseUrl}${path}`);
75
-
76
- // Agregar query params si existen (excluyendo id y body)
77
- Object.keys(params).forEach(key => {
78
- if (key !== 'id' && key !== 'body' && key !== 'headers') {
79
- url.searchParams.append(key, String(params[key]));
80
- }
81
- });
82
-
83
- const headers = {
84
- "Content-Type": "application/json",
85
- ...(params.headers || {})
86
- };
87
-
88
- const response = await fetchFn(url.toString(), {
89
- method,
90
- headers,
91
- body: params.body ? JSON.stringify(params.body) : undefined,
92
- });
93
-
94
- if (!response.ok) {
95
- throw new Error(`Request failed: ${response.status} ${response.statusText}`);
96
- }
97
-
98
- return response.json();
99
- };
100
- }
101
- });
102
- }
103
- });
104
- }