@codexsploitx/schemaapi 1.0.5 → 1.0.6

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.
@@ -1,19 +1,1728 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
- typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.SchemaApi = {}));
5
- })(this, (function (exports) { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('zod')) :
3
+ typeof define === 'function' && define.amd ? define(['exports', 'zod'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.SchemaApi = {}, global.zod));
5
+ })(this, (function (exports, zod) { 'use strict';
6
6
 
7
- function createContract(schema) {
7
+ class SchemaApiError extends Error {
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.code = code;
11
+ this.name = "SchemaApiError";
12
+ }
13
+ }
14
+ function buildErrorPayload(error) {
15
+ if (error instanceof SchemaApiError) {
16
+ return {
17
+ error: error.message,
18
+ message: error.message,
19
+ status: error.code,
20
+ };
21
+ }
22
+ const typedError = error;
23
+ const status = typeof typedError.status === "number"
24
+ ? typedError.status
25
+ : typeof typedError.code === "number"
26
+ ? typedError.code
27
+ : 500;
28
+ const message = typeof typedError.message === "string"
29
+ ? typedError.message
30
+ : "Internal Server Error";
8
31
  return {
32
+ error: message,
33
+ message,
34
+ status,
35
+ };
36
+ }
37
+ function extractRouteParams(route) {
38
+ return route
39
+ .split("/")
40
+ .filter((segment) => segment.startsWith(":"))
41
+ .map((segment) => segment.slice(1))
42
+ .filter((name) => name.length > 0);
43
+ }
44
+ function getBaseSchema(schema) {
45
+ let current = schema;
46
+ while (current._def &&
47
+ current._def?.innerType) {
48
+ const def = current._def;
49
+ current = def.innerType;
50
+ }
51
+ return current;
52
+ }
53
+ function getObjectShape(schema) {
54
+ if (!schema) {
55
+ return null;
56
+ }
57
+ const base = getBaseSchema(schema);
58
+ if (base instanceof zod.ZodObject) {
59
+ const objectSchema = base;
60
+ return objectSchema.shape;
61
+ }
62
+ return null;
63
+ }
64
+ function getTypeName(schema) {
65
+ const base = getBaseSchema(schema);
66
+ const def = base._def;
67
+ if (!def || typeof def !== "object") {
68
+ return undefined;
69
+ }
70
+ const record = def;
71
+ const value = record["typeName"] || record["type"];
72
+ if (typeof value === "string") {
73
+ return value;
74
+ }
75
+ return undefined;
76
+ }
77
+ function isOptional(schema) {
78
+ return schema.isOptional();
79
+ }
80
+ function createContract(schema) {
81
+ const api = {
9
82
  handle(endpoint, handler) {
10
- return handler;
83
+ const [method, route] = endpoint.split(" ");
84
+ const routeSchema = schema[route];
85
+ if (!routeSchema) {
86
+ throw new SchemaApiError(404, `Route ${route} not defined in contract`);
87
+ }
88
+ const methodSchema = routeSchema[method];
89
+ if (!methodSchema) {
90
+ throw new SchemaApiError(405, `Method ${method} not defined for route ${route}`);
91
+ }
92
+ const dynamicParams = extractRouteParams(route);
93
+ if (dynamicParams.length > 0) {
94
+ const paramsShape = getObjectShape(methodSchema.params);
95
+ if (!paramsShape) {
96
+ throw new Error(`Route ${route} has dynamic params but no params schema is defined`);
97
+ }
98
+ dynamicParams.forEach((name) => {
99
+ if (!(name in paramsShape)) {
100
+ throw new Error(`Dynamic path param :${name} in ${route} is not defined in params`);
101
+ }
102
+ });
103
+ }
104
+ return async (ctx) => {
105
+ const context = ctx;
106
+ const media = methodSchema.media;
107
+ if (media && media.kind === "upload") {
108
+ const rawHeaders = (context.headers ??
109
+ {});
110
+ const headers = {};
111
+ Object.keys(rawHeaders).forEach((key) => {
112
+ const value = rawHeaders[key];
113
+ if (typeof value === "string") {
114
+ headers[key.toLowerCase()] = value;
115
+ }
116
+ });
117
+ const contentTypeHeader = headers["content-type"];
118
+ if (media.contentTypes && media.contentTypes.length > 0) {
119
+ const matches = !!contentTypeHeader &&
120
+ media.contentTypes.some((expected) => {
121
+ if (expected.endsWith("/*")) {
122
+ const prefix = expected.slice(0, -1);
123
+ return contentTypeHeader.startsWith(prefix);
124
+ }
125
+ return (contentTypeHeader === expected ||
126
+ contentTypeHeader.startsWith(`${expected};`));
127
+ });
128
+ if (!matches) {
129
+ throw new SchemaApiError(415, "Unsupported Media Type");
130
+ }
131
+ }
132
+ if (media.maxSize && media.maxSize > 0) {
133
+ const lengthHeader = headers["content-length"];
134
+ if (lengthHeader !== undefined) {
135
+ const parsed = Number(lengthHeader);
136
+ if (!Number.isNaN(parsed) && parsed > media.maxSize) {
137
+ throw new SchemaApiError(413, "Payload Too Large");
138
+ }
139
+ }
140
+ }
141
+ }
142
+ if (methodSchema.roles && methodSchema.roles.length > 0) {
143
+ const userRole = context.user?.role;
144
+ if (!userRole || !methodSchema.roles.includes(userRole)) {
145
+ throw new SchemaApiError(403, "Forbidden");
146
+ }
147
+ }
148
+ try {
149
+ if (methodSchema.params && context.params !== undefined) {
150
+ context.params = methodSchema.params.parse(context.params);
151
+ }
152
+ if (methodSchema.query && context.query !== undefined) {
153
+ context.query = methodSchema.query.parse(context.query);
154
+ }
155
+ if (methodSchema.body && context.body !== undefined) {
156
+ context.body = methodSchema.body.parse(context.body);
157
+ }
158
+ if (methodSchema.headers && context.headers !== undefined) {
159
+ context.headers = methodSchema.headers.parse(context.headers);
160
+ }
161
+ }
162
+ catch (error) {
163
+ if (error instanceof zod.z.ZodError) {
164
+ throw new SchemaApiError(400, `Validation Error: ${JSON.stringify(error.flatten())}`);
165
+ }
166
+ throw error;
167
+ }
168
+ let result;
169
+ try {
170
+ result = await handler(context);
171
+ }
172
+ catch (error) {
173
+ if (error instanceof SchemaApiError) {
174
+ const code = error.code;
175
+ const errorsConfig = (methodSchema.errors ??
176
+ {});
177
+ const allowedStatuses = Object.keys(errorsConfig);
178
+ if (code >= 400 && code !== 400 && code !== 500) {
179
+ if (!allowedStatuses.includes(String(code))) {
180
+ throw new SchemaApiError(code, `${code} is not defined in contract.errors`);
181
+ }
182
+ }
183
+ throw error;
184
+ }
185
+ throw error;
186
+ }
187
+ if (methodSchema.response) {
188
+ try {
189
+ return methodSchema.response.parse(result);
190
+ }
191
+ catch (error) {
192
+ if (error instanceof zod.z.ZodError) {
193
+ throw new SchemaApiError(500, `Response Validation Error: ${JSON.stringify(error.flatten())}`);
194
+ }
195
+ throw error;
196
+ }
197
+ }
198
+ return result;
199
+ };
200
+ },
201
+ compareWith(other) {
202
+ const breaking = [];
203
+ const oldSchema = other.schema;
204
+ const newSchema = schema;
205
+ Object.keys(oldSchema).forEach((route) => {
206
+ if (!(route in newSchema)) {
207
+ breaking.push(`Route removed: ${route}`);
208
+ return;
209
+ }
210
+ const oldRoute = oldSchema[route];
211
+ const newRoute = newSchema[route];
212
+ Object.keys(oldRoute).forEach((method) => {
213
+ const oldMethod = oldRoute[method];
214
+ const newMethod = newRoute[method];
215
+ const pathId = `${method} ${route}`;
216
+ if (!newMethod) {
217
+ breaking.push(`Method removed: ${pathId}`);
218
+ return;
219
+ }
220
+ // Check Response (Output)
221
+ const oldResponseShape = getObjectShape(oldMethod.response);
222
+ const newResponseShape = getObjectShape(newMethod.response);
223
+ if (oldResponseShape && newResponseShape) {
224
+ Object.keys(oldResponseShape).forEach((field) => {
225
+ if (!(field in newResponseShape)) {
226
+ breaking.push(`Response field removed: ${field} in ${pathId}`);
227
+ return;
228
+ }
229
+ const oldField = oldResponseShape[field];
230
+ const newField = newResponseShape[field];
231
+ const oldType = getTypeName(oldField);
232
+ const newType = getTypeName(newField);
233
+ if (oldType && newType && oldType !== newType) {
234
+ breaking.push(`Response field type changed: ${field} in ${pathId}`);
235
+ }
236
+ });
237
+ }
238
+ // Check Request Body (Input)
239
+ const oldBodyShape = getObjectShape(oldMethod.body);
240
+ const newBodyShape = getObjectShape(newMethod.body);
241
+ if (newBodyShape) {
242
+ Object.keys(newBodyShape).forEach((field) => {
243
+ const newField = newBodyShape[field];
244
+ const oldField = oldBodyShape ? oldBodyShape[field] : undefined;
245
+ if (!oldField) {
246
+ // New field added. Check if required.
247
+ if (!newField.isOptional()) {
248
+ breaking.push(`New required request body field: ${field} in ${pathId}`);
249
+ }
250
+ }
251
+ else {
252
+ // Field exists in both. Check type change.
253
+ const oldType = getTypeName(oldField);
254
+ const newType = getTypeName(newField);
255
+ if (oldType && newType && oldType !== newType) {
256
+ breaking.push(`Request body field type changed: ${field} in ${pathId}`);
257
+ }
258
+ // Check if became required (was optional)
259
+ if (oldField.isOptional() && !newField.isOptional()) {
260
+ breaking.push(`Request body field became required: ${field} in ${pathId}`);
261
+ }
262
+ }
263
+ });
264
+ }
265
+ // Check Query Params (Input)
266
+ const oldQueryShape = getObjectShape(oldMethod.query);
267
+ const newQueryShape = getObjectShape(newMethod.query);
268
+ if (newQueryShape) {
269
+ Object.keys(newQueryShape).forEach((field) => {
270
+ const newField = newQueryShape[field];
271
+ const oldField = oldQueryShape ? oldQueryShape[field] : undefined;
272
+ if (!oldField) {
273
+ if (!newField.isOptional()) {
274
+ breaking.push(`New required query param: ${field} in ${pathId}`);
275
+ }
276
+ }
277
+ else {
278
+ const oldType = getTypeName(oldField);
279
+ const newType = getTypeName(newField);
280
+ if (oldType && newType && oldType !== newType) {
281
+ breaking.push(`Query param type changed: ${field} in ${pathId}`);
282
+ }
283
+ }
284
+ });
285
+ }
286
+ // Check Path Params (Input)
287
+ const oldParamsShape = getObjectShape(oldMethod.params);
288
+ const newParamsShape = getObjectShape(newMethod.params);
289
+ if (newParamsShape) {
290
+ Object.keys(newParamsShape).forEach((field) => {
291
+ const newField = newParamsShape[field];
292
+ const oldField = oldParamsShape ? oldParamsShape[field] : undefined;
293
+ if (oldField) {
294
+ const oldType = getTypeName(oldField);
295
+ const newType = getTypeName(newField);
296
+ if (oldType && newType && oldType !== newType) {
297
+ breaking.push(`Path param type changed: ${field} in ${pathId}`);
298
+ }
299
+ }
300
+ });
301
+ }
302
+ const oldErrors = (oldMethod.errors ?? {});
303
+ const newErrors = (newMethod.errors ?? {});
304
+ Object.keys(oldErrors).forEach((status) => {
305
+ if (!(status in newErrors)) {
306
+ breaking.push(`Status code removed: ${status} in ${pathId}`);
307
+ }
308
+ else {
309
+ const oldError = oldErrors[status];
310
+ const newError = newErrors[status];
311
+ if (oldError !== newError) {
312
+ breaking.push(`Error code changed for status ${status} in ${pathId}`);
313
+ }
314
+ }
315
+ });
316
+ const oldRoles = (oldMethod.roles ?? []);
317
+ const newRoles = (newMethod.roles ?? []);
318
+ oldRoles.forEach((role) => {
319
+ if (!newRoles.includes(role)) {
320
+ breaking.push(`Role removed: ${role} in ${pathId}`);
321
+ }
322
+ });
323
+ });
324
+ });
325
+ return { breakingChanges: breaking };
326
+ },
327
+ docs() {
328
+ const routesDocs = [];
329
+ const current = schema;
330
+ Object.keys(current).forEach((route) => {
331
+ const routeDef = current[route];
332
+ Object.keys(routeDef).forEach((method) => {
333
+ const methodDef = routeDef[method];
334
+ const doc = {
335
+ method,
336
+ path: route,
337
+ roles: methodDef.roles ? [...methodDef.roles] : undefined,
338
+ media: methodDef.media,
339
+ errors: methodDef.errors
340
+ ? Object.keys(methodDef.errors).map((status) => ({
341
+ status,
342
+ code: String(methodDef.errors?.[status]),
343
+ }))
344
+ : undefined,
345
+ };
346
+ const fillFields = (source, assign) => {
347
+ if (!source) {
348
+ return;
349
+ }
350
+ const shape = getObjectShape(source);
351
+ if (!shape) {
352
+ return;
353
+ }
354
+ const fields = Object.keys(shape).map((name) => {
355
+ const fieldSchema = shape[name];
356
+ const typeName = getTypeName(fieldSchema);
357
+ const optional = isOptional(fieldSchema);
358
+ return {
359
+ name,
360
+ type: typeName,
361
+ optional,
362
+ };
363
+ });
364
+ assign(fields);
365
+ };
366
+ fillFields(methodDef.params, (fields) => {
367
+ doc.params = fields;
368
+ });
369
+ fillFields(methodDef.query, (fields) => {
370
+ doc.query = fields;
371
+ });
372
+ fillFields(methodDef.body, (fields) => {
373
+ doc.body = fields;
374
+ });
375
+ fillFields(methodDef.headers, (fields) => {
376
+ doc.headers = fields;
377
+ });
378
+ routesDocs.push(doc);
379
+ });
380
+ });
381
+ return { routes: routesDocs };
11
382
  },
12
383
  schema,
13
384
  };
385
+ return api;
386
+ }
387
+
388
+ // Definimos tipos básicos para simular la estructura del cliente
389
+ // En una implementación real, estos tipos se inferirían recursivamente del esquema T
390
+ function createClient(contract, config) {
391
+ const { baseUrl, fetch: customFetch } = config;
392
+ const fetchFn = customFetch || globalThis.fetch;
393
+ return new Proxy({}, {
394
+ get(_target, resource) {
395
+ return new Proxy({}, {
396
+ get(_target, operation) {
397
+ // operation podría ser 'get', 'post', 'put', 'delete', 'ws', etc.
398
+ // resource sería 'users', 'posts', etc.
399
+ return async (params = {}) => {
400
+ const method = operation.toUpperCase();
401
+ let path = `/${resource}`;
402
+ // Si hay params.id, asumimos que va en la URL
403
+ if (params.id) {
404
+ path += `/${params.id}`;
405
+ }
406
+ // Manejo de WebSocket
407
+ if (method === "WS") {
408
+ const wsUrlStr = baseUrl.replace(/^http/, "ws");
409
+ const url = new URL(`${wsUrlStr}${path}`);
410
+ // Agregar query params
411
+ Object.keys(params).forEach(key => {
412
+ if (key !== 'id' && key !== 'headers' && key !== 'body') {
413
+ url.searchParams.append(key, String(params[key]));
414
+ }
415
+ });
416
+ const globalWithWebSocket = globalThis;
417
+ const WebSocketCtor = config.WebSocket || globalWithWebSocket.WebSocket;
418
+ if (!WebSocketCtor) {
419
+ throw new Error("WebSocket implementation not found. Please provide it in config.");
420
+ }
421
+ return new WebSocketCtor(url.toString());
422
+ }
423
+ const url = new URL(`${baseUrl}${path}`);
424
+ // Agregar query params si existen (excluyendo id y body)
425
+ Object.keys(params).forEach(key => {
426
+ if (key !== 'id' && key !== 'body' && key !== 'headers') {
427
+ url.searchParams.append(key, String(params[key]));
428
+ }
429
+ });
430
+ const headers = {
431
+ "Content-Type": "application/json",
432
+ ...(params.headers || {})
433
+ };
434
+ const response = await fetchFn(url.toString(), {
435
+ method,
436
+ headers,
437
+ body: params.body ? JSON.stringify(params.body) : undefined,
438
+ });
439
+ if (!response.ok) {
440
+ throw new Error(`Request failed: ${response.status} ${response.statusText}`);
441
+ }
442
+ return response.json();
443
+ };
444
+ }
445
+ });
446
+ }
447
+ });
448
+ }
449
+
450
+ function generateSDK(contract, options) {
451
+ return createClient(contract, {
452
+ baseUrl: options.baseUrl,
453
+ fetch: options.fetch,
454
+ });
455
+ }
456
+
457
+ function handleContract$6(app, contract, handlers) {
458
+ const schema = contract.schema;
459
+ Object.keys(schema).forEach((route) => {
460
+ const methods = schema[route];
461
+ Object.keys(methods).forEach((method) => {
462
+ const endpoint = `${method} ${route}`;
463
+ const implementation = handlers[endpoint];
464
+ const methodSchema = methods[method];
465
+ if (!implementation) {
466
+ return;
467
+ }
468
+ const wrapped = contract.handle(endpoint, implementation);
469
+ const httpMethod = method.toLowerCase();
470
+ const register = app[httpMethod];
471
+ if (!register) {
472
+ return;
473
+ }
474
+ register(route, async (req, res, next) => {
475
+ try {
476
+ const context = {
477
+ params: req.params || {},
478
+ query: req.query || {},
479
+ body: req.body,
480
+ headers: req.headers || {},
481
+ user: req.user,
482
+ };
483
+ const result = await wrapped(context);
484
+ const media = methodSchema?.media;
485
+ if (media && media.kind === "download") {
486
+ const download = result;
487
+ const data = download &&
488
+ typeof download === "object" &&
489
+ "data" in download
490
+ ? download.data
491
+ : download;
492
+ const contentType = download &&
493
+ typeof download === "object" &&
494
+ "contentType" in download
495
+ ? download.contentType
496
+ : "application/octet-stream";
497
+ const filename = download &&
498
+ typeof download === "object" &&
499
+ "filename" in download
500
+ ? download.filename
501
+ : undefined;
502
+ if (typeof res.setHeader === "function") {
503
+ res.setHeader("Content-Type", contentType);
504
+ if (filename) {
505
+ res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
506
+ }
507
+ }
508
+ if (typeof res.send === "function") {
509
+ res.send(data);
510
+ return;
511
+ }
512
+ }
513
+ res.json(result);
514
+ }
515
+ catch (error) {
516
+ next(error);
517
+ }
518
+ });
519
+ });
520
+ });
521
+ }
522
+
523
+ var express = /*#__PURE__*/Object.freeze({
524
+ __proto__: null,
525
+ handleContract: handleContract$6
526
+ });
527
+
528
+ // Helper para obtener NextResponse (asumiendo que el usuario lo tiene disponible o lo importamos dinámicamente si fuera un paquete real)
529
+ // Aquí simularemos una respuesta JSON estándar
530
+ function jsonResponse(data, status = 200) {
531
+ return new Response(JSON.stringify(data), {
532
+ status,
533
+ headers: { "Content-Type": "application/json" },
534
+ });
535
+ }
536
+ function handleContract$5(contract, handlers) {
537
+ // Retorna un handler compatible con Route Handlers de Next.js (App Router)
538
+ // Uso: export const { GET, POST, PUT, DELETE } = adapters.next.handleContract(contract, handlers);
539
+ // en app/api/[...route]/route.ts
540
+ const dispatcher = async (req, { params }) => {
541
+ const url = new URL(req.url);
542
+ // En App Router [...route], params.route es un array. Pero aquí asumimos que el usuario mapea rutas específicas o catch-all.
543
+ // Si es catch-all, necesitamos reconstruir la ruta.
544
+ // Simplificación: Iteramos sobre el contrato para encontrar la ruta que coincida.
545
+ // Esto es ineficiente O(N) por request, pero funciona para validación.
546
+ // Una implementación más robusta usaría un router interno.
547
+ const method = req.method;
548
+ const path = url.pathname; // Ojo: esto incluye el prefijo /api si está ahí.
549
+ // Intentar hacer match con las rutas del contrato
550
+ const schema = contract.schema;
551
+ for (const routePattern of Object.keys(schema)) {
552
+ // Convertir /users/:id a regex simple
553
+ const regexPattern = routePattern.replace(/:[a-zA-Z0-9_]+/g, "([^/]+)");
554
+ const regex = new RegExp(`^.*${regexPattern}$`); // ^.* para permitir prefijos como /api
555
+ const match = path.match(regex);
556
+ if (match) {
557
+ // Verificar método
558
+ const routeMethods = schema[routePattern];
559
+ if (routeMethods[method]) {
560
+ const endpoint = `${method} ${routePattern}`;
561
+ const implementation = handlers[endpoint];
562
+ const methodSchema = routeMethods[method];
563
+ if (!implementation)
564
+ return jsonResponse({ error: "Not Implemented" }, 501);
565
+ const contextParams = params || {};
566
+ const wrapped = contract.handle(endpoint, implementation);
567
+ try {
568
+ // Parse body safely
569
+ let body = undefined;
570
+ if (method !== 'GET' && method !== 'HEAD') {
571
+ try {
572
+ body = await req.json();
573
+ }
574
+ catch { }
575
+ }
576
+ const context = {
577
+ params: contextParams,
578
+ query: Object.fromEntries(url.searchParams.entries()),
579
+ body,
580
+ headers: Object.fromEntries(req.headers.entries()),
581
+ };
582
+ const result = await wrapped(context);
583
+ const media = methodSchema?.media;
584
+ if (media && media.kind === "download") {
585
+ const download = result;
586
+ const hasData = download &&
587
+ typeof download === "object" &&
588
+ Object.prototype.hasOwnProperty.call(download, "data");
589
+ const data = hasData
590
+ ? download.data
591
+ : download;
592
+ const contentType = download &&
593
+ typeof download === "object" &&
594
+ "contentType" in download &&
595
+ typeof download.contentType ===
596
+ "string"
597
+ ? download.contentType
598
+ : "application/octet-stream";
599
+ const filename = download &&
600
+ typeof download === "object" &&
601
+ "filename" in download &&
602
+ typeof download.filename ===
603
+ "string"
604
+ ? download.filename
605
+ : undefined;
606
+ const headers = {
607
+ "Content-Type": String(contentType),
608
+ };
609
+ if (filename) {
610
+ headers["Content-Disposition"] =
611
+ `attachment; filename="${filename}"`;
612
+ }
613
+ return new Response(data, {
614
+ status: 200,
615
+ headers,
616
+ });
617
+ }
618
+ return jsonResponse(result);
619
+ }
620
+ catch (err) {
621
+ const payload = buildErrorPayload(err);
622
+ return jsonResponse(payload, payload.status);
623
+ }
624
+ }
625
+ }
626
+ }
627
+ return jsonResponse({ error: "Not Found" }, 404);
628
+ };
629
+ return {
630
+ GET: dispatcher,
631
+ POST: dispatcher,
632
+ PUT: dispatcher,
633
+ DELETE: dispatcher,
634
+ PATCH: dispatcher,
635
+ };
636
+ }
637
+
638
+ var next = /*#__PURE__*/Object.freeze({
639
+ __proto__: null,
640
+ handleContract: handleContract$5
641
+ });
642
+
643
+ function handleContract$4(fastify, contract, handlers) {
644
+ const schema = contract.schema;
645
+ Object.keys(schema).forEach((route) => {
646
+ const methods = schema[route];
647
+ Object.keys(methods).forEach((method) => {
648
+ const endpoint = `${method} ${route}`;
649
+ const implementation = handlers[endpoint];
650
+ const methodSchema = methods[method];
651
+ if (!implementation) {
652
+ return;
653
+ }
654
+ const wrapped = contract.handle(endpoint, implementation);
655
+ fastify.route({
656
+ method: method,
657
+ url: route, // Fastify soporta /:id sintaxis
658
+ handler: async (request, reply) => {
659
+ try {
660
+ const context = {
661
+ params: request.params || {},
662
+ query: request.query || {},
663
+ body: request.body,
664
+ headers: request.headers || {},
665
+ user: request.user,
666
+ };
667
+ const result = await wrapped(context);
668
+ const media = methodSchema?.media;
669
+ if (media && media.kind === "download") {
670
+ const download = result;
671
+ const data = download &&
672
+ typeof download === "object" &&
673
+ "data" in download
674
+ ? download.data
675
+ : download;
676
+ const contentType = download &&
677
+ typeof download === "object" &&
678
+ "contentType" in download
679
+ ? download.contentType
680
+ : "application/octet-stream";
681
+ const filename = download &&
682
+ typeof download === "object" &&
683
+ "filename" in download
684
+ ? download.filename
685
+ : undefined;
686
+ if (typeof reply.header === "function") {
687
+ reply.header("Content-Type", contentType);
688
+ if (filename) {
689
+ reply.header("Content-Disposition", `attachment; filename="${filename}"`);
690
+ }
691
+ }
692
+ reply.send(data);
693
+ return;
694
+ }
695
+ reply.send(result);
696
+ }
697
+ catch (error) {
698
+ const payload = buildErrorPayload(error);
699
+ reply.status(payload.status).send(payload);
700
+ }
701
+ },
702
+ });
703
+ });
704
+ });
705
+ }
706
+
707
+ var fastify = /*#__PURE__*/Object.freeze({
708
+ __proto__: null,
709
+ handleContract: handleContract$4
710
+ });
711
+
712
+ const SchemaApiModule = {
713
+ register(app, contract, handlers) {
714
+ const adapter = app.getHttpAdapter();
715
+ const schema = contract.schema;
716
+ Object.keys(schema).forEach((route) => {
717
+ const methods = schema[route];
718
+ Object.keys(methods).forEach((method) => {
719
+ const endpoint = `${method} ${route}`;
720
+ const implementation = handlers[endpoint];
721
+ if (!implementation) {
722
+ return;
723
+ }
724
+ const wrapped = contract.handle(endpoint, implementation);
725
+ const methodLower = method.toLowerCase();
726
+ if (typeof adapter[methodLower] === "function") {
727
+ adapter[methodLower](route, async (req, res, _next) => {
728
+ try {
729
+ // Intentamos normalizar request/response independientemente del driver (Express/Fastify)
730
+ // Nota: Esto asume Express por defecto para req.params/query/body.
731
+ // En Fastify, req.params/query/body también existen en el objeto Request estándar de Fastify.
732
+ const context = {
733
+ params: req.params || {},
734
+ query: req.query || {},
735
+ body: req.body,
736
+ headers: req.headers || {},
737
+ user: req.user,
738
+ };
739
+ const result = await wrapped(context);
740
+ // Responder
741
+ if (typeof res.send === "function") {
742
+ // Express / Fastify
743
+ res.send(result);
744
+ }
745
+ else if (typeof res.json === "function") {
746
+ // Express alternative
747
+ res.json(result);
748
+ }
749
+ else {
750
+ // Fallback simple
751
+ res.end(JSON.stringify(result));
752
+ }
753
+ }
754
+ catch (error) {
755
+ const typedError = error;
756
+ if (res.status)
757
+ res.status(typedError.status || 500);
758
+ else if (res.code)
759
+ res.code(typedError.status || 500); // Fastify
760
+ if (res.send)
761
+ res.send({ error: typedError.message });
762
+ else
763
+ res.end(JSON.stringify({ error: typedError.message }));
764
+ }
765
+ });
766
+ }
767
+ });
768
+ });
769
+ },
770
+ };
771
+
772
+ var nest = /*#__PURE__*/Object.freeze({
773
+ __proto__: null,
774
+ SchemaApiModule: SchemaApiModule
775
+ });
776
+
777
+ function handleContract$3(router, contract, handlers) {
778
+ const schema = contract.schema;
779
+ Object.keys(schema).forEach((route) => {
780
+ const methods = schema[route];
781
+ Object.keys(methods).forEach((method) => {
782
+ const endpoint = `${method} ${route}`;
783
+ const implementation = handlers[endpoint];
784
+ const methodSchema = methods[method];
785
+ if (!implementation) {
786
+ return;
787
+ }
788
+ const wrapped = contract.handle(endpoint, implementation);
789
+ // Koa Router register
790
+ router.register(route, [method], async (ctx, _next) => {
791
+ try {
792
+ const context = {
793
+ params: ctx.params || {},
794
+ query: ctx.query || {},
795
+ body: ctx.request.body, // requires koa-bodyparser or koa-body
796
+ headers: ctx.headers || {},
797
+ user: ctx.state?.user, // common pattern
798
+ };
799
+ const result = await wrapped(context);
800
+ const media = methodSchema?.media;
801
+ if (media && media.kind === "download") {
802
+ const download = result;
803
+ const data = download &&
804
+ typeof download === "object" &&
805
+ "data" in download
806
+ ? download.data
807
+ : download;
808
+ const contentType = download &&
809
+ typeof download === "object" &&
810
+ "contentType" in download
811
+ ? download.contentType
812
+ : "application/octet-stream";
813
+ const filename = download &&
814
+ typeof download === "object" &&
815
+ "filename" in download
816
+ ? download.filename
817
+ : undefined;
818
+ ctx.body = data;
819
+ ctx.type = contentType;
820
+ if (filename) {
821
+ ctx.set("Content-Disposition", `attachment; filename="${filename}"`);
822
+ }
823
+ }
824
+ else {
825
+ ctx.body = result;
826
+ }
827
+ }
828
+ catch (error) {
829
+ const payload = buildErrorPayload(error);
830
+ ctx.status = payload.status;
831
+ ctx.body = payload;
832
+ }
833
+ });
834
+ });
835
+ });
836
+ }
837
+
838
+ var koa = /*#__PURE__*/Object.freeze({
839
+ __proto__: null,
840
+ handleContract: handleContract$3
841
+ });
842
+
843
+ function handleContract$2(server, contract, handlers) {
844
+ const schema = contract.schema;
845
+ Object.keys(schema).forEach((route) => {
846
+ const methods = schema[route];
847
+ // Convertir ruta express-style :param a hapi-style {param}
848
+ const hapiRoute = route.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
849
+ Object.keys(methods).forEach((method) => {
850
+ const endpoint = `${method} ${route}`;
851
+ const implementation = handlers[endpoint];
852
+ const methodSchema = methods[method];
853
+ if (!implementation) {
854
+ return;
855
+ }
856
+ const wrapped = contract.handle(endpoint, implementation);
857
+ server.route({
858
+ method: method,
859
+ path: hapiRoute,
860
+ handler: async (request, h) => {
861
+ try {
862
+ const context = {
863
+ params: request.params || {},
864
+ query: request.query || {},
865
+ body: request.payload,
866
+ headers: request.headers || {},
867
+ user: request.auth?.credentials, // Common Hapi auth pattern
868
+ };
869
+ const result = await wrapped(context);
870
+ const media = methodSchema?.media;
871
+ if (media && media.kind === "download") {
872
+ const download = result;
873
+ const data = download &&
874
+ typeof download === "object" &&
875
+ "data" in download
876
+ ? download.data
877
+ : download;
878
+ const contentType = download &&
879
+ typeof download === "object" &&
880
+ "contentType" in download
881
+ ? download.contentType
882
+ : "application/octet-stream";
883
+ const filename = download &&
884
+ typeof download === "object" &&
885
+ "filename" in download
886
+ ? download.filename
887
+ : undefined;
888
+ const response = h.response(data);
889
+ response.type(contentType);
890
+ if (filename) {
891
+ response.header("Content-Disposition", `attachment; filename="${filename}"`);
892
+ }
893
+ return response;
894
+ }
895
+ return result;
896
+ }
897
+ catch (error) {
898
+ const payload = buildErrorPayload(error);
899
+ const response = h.response(payload);
900
+ response.code(payload.status);
901
+ return response;
902
+ }
903
+ },
904
+ });
905
+ });
906
+ });
907
+ }
908
+
909
+ var hapi = /*#__PURE__*/Object.freeze({
910
+ __proto__: null,
911
+ handleContract: handleContract$2
912
+ });
913
+
914
+ function createRouteHandlers(contract, handlers, routePattern) {
915
+ const schema = contract.schema;
916
+ const methods = schema[routePattern];
917
+ if (!methods) {
918
+ const notFound = () => {
919
+ throw new Response("Not Found in Contract", { status: 404 });
920
+ };
921
+ return { loader: notFound, action: notFound };
922
+ }
923
+ const handleRequest = async (request, params) => {
924
+ const method = request.method;
925
+ const endpoint = `${method} ${routePattern}`;
926
+ const implementation = handlers[endpoint];
927
+ const methodSchema = methods && methods[method];
928
+ if (!implementation) {
929
+ throw new Response("Method Not Implemented", { status: 501 });
930
+ }
931
+ const wrapped = contract.handle(endpoint, implementation);
932
+ try {
933
+ let body = undefined;
934
+ // Parse body only for non-GET/HEAD methods
935
+ if (method !== "GET" && method !== "HEAD") {
936
+ try {
937
+ body = await request.json();
938
+ }
939
+ catch {
940
+ // Si falla json, tal vez es FormData? Remix usa mucho FormData.
941
+ // Por simplicidad, aquí asumimos JSON como el estándar de SchemaApi.
942
+ // Si se requiere FormData, se debería procesar antes o aquí.
943
+ }
944
+ }
945
+ const url = new URL(request.url);
946
+ const context = {
947
+ params: params || {},
948
+ query: Object.fromEntries(url.searchParams.entries()),
949
+ body,
950
+ headers: Object.fromEntries(request.headers.entries()),
951
+ };
952
+ const result = await wrapped(context);
953
+ const media = methodSchema?.media;
954
+ if (media && media.kind === "download") {
955
+ const download = result;
956
+ const hasData = download &&
957
+ typeof download === "object" &&
958
+ Object.prototype.hasOwnProperty.call(download, "data");
959
+ const data = hasData
960
+ ? download.data
961
+ : download;
962
+ const contentType = download &&
963
+ typeof download === "object" &&
964
+ "contentType" in download &&
965
+ typeof download.contentType ===
966
+ "string"
967
+ ? download.contentType
968
+ : "application/octet-stream";
969
+ const filename = download &&
970
+ typeof download === "object" &&
971
+ "filename" in download &&
972
+ typeof download.filename === "string"
973
+ ? download.filename
974
+ : undefined;
975
+ const headers = {
976
+ "Content-Type": String(contentType),
977
+ };
978
+ if (filename) {
979
+ headers["Content-Disposition"] = `attachment; filename="${filename}"`;
980
+ }
981
+ return new Response(data, {
982
+ headers,
983
+ });
984
+ }
985
+ return new Response(JSON.stringify(result), {
986
+ headers: { "Content-Type": "application/json" },
987
+ });
988
+ }
989
+ catch (err) {
990
+ const payload = buildErrorPayload(err);
991
+ return new Response(JSON.stringify(payload), {
992
+ status: payload.status,
993
+ headers: { "Content-Type": "application/json" },
994
+ });
995
+ }
996
+ };
997
+ const loader = async (args) => {
998
+ if (methods["GET"]) {
999
+ return handleRequest(args.request, args.params);
1000
+ }
1001
+ // Si no hay GET definido pero se llama al loader
1002
+ throw new Response("Method Not Allowed", { status: 405 });
1003
+ };
1004
+ const action = async (args) => {
1005
+ const method = args.request.method;
1006
+ if (methods[method]) {
1007
+ return handleRequest(args.request, args.params);
1008
+ }
1009
+ throw new Response("Method Not Allowed", { status: 405 });
1010
+ };
1011
+ return { loader, action };
1012
+ }
1013
+
1014
+ var remix = /*#__PURE__*/Object.freeze({
1015
+ __proto__: null,
1016
+ createRouteHandlers: createRouteHandlers
1017
+ });
1018
+
1019
+ function handleContract$1(contract, handlers) {
1020
+ // Retorna una función (req: Request) => Promise<Response> compatible con Deno.serve
1021
+ return async (req) => {
1022
+ const url = new URL(req.url);
1023
+ const path = url.pathname;
1024
+ const method = req.method;
1025
+ const schema = contract.schema;
1026
+ for (const routePattern of Object.keys(schema)) {
1027
+ // Regex simple para matching: /users/:id -> /users/([^/]+)
1028
+ // Agregamos ^ y $ para match exacto
1029
+ const regexPattern = routePattern.replace(/:[a-zA-Z0-9_]+/g, "([^/]+)");
1030
+ const regex = new RegExp(`^${regexPattern}$`);
1031
+ const match = path.match(regex);
1032
+ if (match) {
1033
+ const routeMethods = schema[routePattern];
1034
+ if (routeMethods[method]) {
1035
+ const endpoint = `${method} ${routePattern}`;
1036
+ const implementation = handlers[endpoint];
1037
+ const methodSchema = routeMethods[method];
1038
+ if (!implementation) {
1039
+ return new Response("Not Implemented", { status: 501 });
1040
+ }
1041
+ // Extraer params
1042
+ // Necesitamos saber los nombres de los parámetros para mapearlos
1043
+ // routePattern: /users/:id/posts/:postId
1044
+ // match: [..., "123", "456"]
1045
+ const paramNames = (routePattern.match(/:[a-zA-Z0-9_]+/g) || []).map((p) => p.substring(1));
1046
+ const params = {};
1047
+ paramNames.forEach((name, index) => {
1048
+ params[name] = match[index + 1];
1049
+ });
1050
+ const wrapped = contract.handle(endpoint, implementation);
1051
+ try {
1052
+ let body = undefined;
1053
+ if (method !== "GET" && method !== "HEAD") {
1054
+ try {
1055
+ body = await req.json();
1056
+ }
1057
+ catch { }
1058
+ }
1059
+ const context = {
1060
+ params,
1061
+ query: Object.fromEntries(url.searchParams.entries()),
1062
+ body,
1063
+ headers: Object.fromEntries(req.headers.entries()),
1064
+ };
1065
+ const result = await wrapped(context);
1066
+ const media = methodSchema?.media;
1067
+ if (media && media.kind === "download") {
1068
+ const download = result;
1069
+ const data = download &&
1070
+ typeof download === "object" &&
1071
+ "data" in download
1072
+ ? download.data
1073
+ : download;
1074
+ const contentType = download &&
1075
+ typeof download === "object" &&
1076
+ "contentType" in download
1077
+ ? download.contentType
1078
+ : "application/octet-stream";
1079
+ const filename = download &&
1080
+ typeof download === "object" &&
1081
+ "filename" in download
1082
+ ? download.filename
1083
+ : undefined;
1084
+ const headers = {
1085
+ "Content-Type": String(contentType),
1086
+ };
1087
+ if (filename) {
1088
+ headers["Content-Disposition"] = `attachment; filename="${filename}"`;
1089
+ }
1090
+ return new Response(data, {
1091
+ headers,
1092
+ });
1093
+ }
1094
+ return new Response(JSON.stringify(result), {
1095
+ headers: { "Content-Type": "application/json" },
1096
+ });
1097
+ }
1098
+ catch (err) {
1099
+ const payload = buildErrorPayload(err);
1100
+ return new Response(JSON.stringify(payload), {
1101
+ status: payload.status,
1102
+ headers: { "Content-Type": "application/json" },
1103
+ });
1104
+ }
1105
+ }
1106
+ }
1107
+ }
1108
+ return new Response("Not Found", { status: 404 });
1109
+ };
1110
+ }
1111
+
1112
+ var deno = /*#__PURE__*/Object.freeze({
1113
+ __proto__: null,
1114
+ handleContract: handleContract$1
1115
+ });
1116
+
1117
+ function handleContract(wss, contract, handlers) {
1118
+ const schema = contract.schema;
1119
+ wss.on("connection", async (ws, req) => {
1120
+ const url = req.url ? new URL(req.url, "http://localhost") : null;
1121
+ if (!url) {
1122
+ ws.close(1002, "Invalid URL");
1123
+ return;
1124
+ }
1125
+ const path = url.pathname;
1126
+ let matched = false;
1127
+ for (const routePattern of Object.keys(schema)) {
1128
+ const methods = schema[routePattern];
1129
+ const wsDef = methods["WS"];
1130
+ if (!wsDef)
1131
+ continue;
1132
+ // Match path
1133
+ const regexPattern = routePattern.replace(/:[a-zA-Z0-9_]+/g, "([^/]+)");
1134
+ const regex = new RegExp(`^${regexPattern}$`);
1135
+ const match = path.match(regex);
1136
+ if (match) {
1137
+ matched = true;
1138
+ const implementation = handlers[`WS ${routePattern}`];
1139
+ if (!implementation) {
1140
+ ws.close(1011, "Handler not found");
1141
+ return;
1142
+ }
1143
+ // Extract params
1144
+ const paramNames = (routePattern.match(/:[a-zA-Z0-9_]+/g) || []).map((p) => p.substring(1));
1145
+ const params = {};
1146
+ paramNames.forEach((name, index) => {
1147
+ params[name] = match[index + 1];
1148
+ });
1149
+ const query = Object.fromEntries(url.searchParams.entries());
1150
+ const context = {
1151
+ params,
1152
+ query,
1153
+ headers: req.headers,
1154
+ ws,
1155
+ send: (data) => {
1156
+ if (wsDef.serverMessages) {
1157
+ try {
1158
+ wsDef.serverMessages.parse(data);
1159
+ }
1160
+ catch (e) {
1161
+ console.error("Server message validation failed", e);
1162
+ }
1163
+ }
1164
+ ws.send(JSON.stringify(data));
1165
+ },
1166
+ onMessage: (cb) => {
1167
+ ws.on("message", (raw) => {
1168
+ try {
1169
+ const json = JSON.parse(raw.toString());
1170
+ if (wsDef.clientMessages) {
1171
+ try {
1172
+ const parsed = wsDef.clientMessages.parse(json);
1173
+ cb(parsed);
1174
+ }
1175
+ catch (e) {
1176
+ console.error("Client message validation failed", e);
1177
+ ws.send(JSON.stringify({
1178
+ error: "Invalid Message Schema",
1179
+ details: e,
1180
+ }));
1181
+ }
1182
+ }
1183
+ else {
1184
+ cb(json);
1185
+ }
1186
+ }
1187
+ catch (e) {
1188
+ console.error("Invalid JSON", e);
1189
+ }
1190
+ });
1191
+ },
1192
+ };
1193
+ try {
1194
+ await implementation(context);
1195
+ }
1196
+ catch (e) {
1197
+ console.error("Error in WS handler", e);
1198
+ ws.close(1011, "Internal Error");
1199
+ }
1200
+ break;
1201
+ }
1202
+ }
1203
+ if (!matched) {
1204
+ ws.close(4404, "Not Found");
1205
+ }
1206
+ });
1207
+ return wss;
1208
+ }
1209
+
1210
+ var ws = /*#__PURE__*/Object.freeze({
1211
+ __proto__: null,
1212
+ handleContract: handleContract
1213
+ });
1214
+
1215
+ var index = /*#__PURE__*/Object.freeze({
1216
+ __proto__: null,
1217
+ deno: deno,
1218
+ express: express,
1219
+ fastify: fastify,
1220
+ hapi: hapi,
1221
+ koa: koa,
1222
+ nest: nest,
1223
+ next: next,
1224
+ remix: remix,
1225
+ ws: ws
1226
+ });
1227
+
1228
+ function renderDocsJSON(docs) {
1229
+ return JSON.stringify(docs, null, 2);
1230
+ }
1231
+ function renderDocsText(docs) {
1232
+ const lines = [];
1233
+ lines.push("SchemaApi Contract");
1234
+ lines.push("================================");
1235
+ docs.routes.forEach((route) => {
1236
+ lines.push("");
1237
+ lines.push(`${route.method} ${route.path}`);
1238
+ lines.push("-".repeat((route.method + route.path).length + 1));
1239
+ if (route.roles && route.roles.length > 0) {
1240
+ lines.push(`roles: ${route.roles.join(", ")}`);
1241
+ }
1242
+ if (route.media) {
1243
+ const mediaParts = [];
1244
+ if (route.media.kind) {
1245
+ mediaParts.push(`kind=${route.media.kind}`);
1246
+ }
1247
+ if (route.media.contentTypes && route.media.contentTypes.length > 0) {
1248
+ mediaParts.push(`types=[${route.media.contentTypes.join(", ")}]`);
1249
+ }
1250
+ if (route.media.maxSize) {
1251
+ mediaParts.push(`maxSize=${route.media.maxSize}`);
1252
+ }
1253
+ lines.push(`media: ${mediaParts.join(" ")}`);
1254
+ }
1255
+ const renderSection = (name, fields) => {
1256
+ if (!fields || fields.length === 0) {
1257
+ return;
1258
+ }
1259
+ lines.push(``);
1260
+ lines.push(`${name}:`);
1261
+ fields.forEach((f) => {
1262
+ const flag = f.optional ? "optional" : "required";
1263
+ const typeName = f.type ? f.type : "unknown";
1264
+ lines.push(` - ${f.name}: ${typeName} (${flag})`);
1265
+ });
1266
+ };
1267
+ renderSection("params", route.params);
1268
+ renderSection("query", route.query);
1269
+ renderSection("body", route.body);
1270
+ renderSection("headers", route.headers);
1271
+ if (route.errors && route.errors.length > 0) {
1272
+ lines.push("");
1273
+ lines.push("errors:");
1274
+ route.errors.forEach((e) => {
1275
+ lines.push(` - ${e.status}: ${e.code}`);
1276
+ });
1277
+ }
1278
+ });
1279
+ return lines.join("\n");
1280
+ }
1281
+ function getThemeBackground(theme) {
1282
+ if (theme === "dark") {
1283
+ return "#020617";
1284
+ }
1285
+ if (theme === "light") {
1286
+ return "#f9fafb";
1287
+ }
1288
+ return "#0b1120";
1289
+ }
1290
+ function getThemeForeground(theme) {
1291
+ if (theme === "dark") {
1292
+ return "#e5e7eb";
1293
+ }
1294
+ if (theme === "light") {
1295
+ return "#111827";
1296
+ }
1297
+ return "#e5e7eb";
1298
+ }
1299
+ function renderDocsHTML(docs, options) {
1300
+ const title = options?.title ?? "SchemaApi Docs";
1301
+ const bg = getThemeBackground(options?.theme);
1302
+ const fg = getThemeForeground(options?.theme);
1303
+ const escape = (value) => {
1304
+ if (value === undefined || value === null) {
1305
+ return "";
1306
+ }
1307
+ return String(value)
1308
+ .replace(/&/g, "&amp;")
1309
+ .replace(/</g, "&lt;")
1310
+ .replace(/>/g, "&gt;")
1311
+ .replace(/"/g, "&quot;");
1312
+ };
1313
+ const renderFieldsTable = (caption, fields) => {
1314
+ if (!fields || fields.length === 0) {
1315
+ return "";
1316
+ }
1317
+ const rows = fields
1318
+ .map((f) => {
1319
+ const typeName = f.type ? f.type : "unknown";
1320
+ const badge = f.optional ? "optional" : "required";
1321
+ return `<tr>
1322
+ <td class="field-name">${escape(f.name)}</td>
1323
+ <td class="field-type">${escape(typeName)}</td>
1324
+ <td class="field-badge ${badge}">${badge}</td>
1325
+ </tr>`;
1326
+ })
1327
+ .join("\n");
1328
+ return `<section class="block">
1329
+ <div class="block-title">${escape(caption)}</div>
1330
+ <table class="fields">
1331
+ <thead>
1332
+ <tr>
1333
+ <th>name</th>
1334
+ <th>type</th>
1335
+ <th>required</th>
1336
+ </tr>
1337
+ </thead>
1338
+ <tbody>
1339
+ ${rows}
1340
+ </tbody>
1341
+ </table>
1342
+ </section>`;
1343
+ };
1344
+ const renderRoute = (route) => {
1345
+ const method = escape(route.method);
1346
+ const path = escape(route.path);
1347
+ const roles = route.roles && route.roles.length > 0
1348
+ ? `<div class="pill-group">
1349
+ ${route.roles
1350
+ .map((r) => `<span class="pill pill-role">role: ${escape(r)}</span>`)
1351
+ .join("\n")}
1352
+ </div>`
1353
+ : "";
1354
+ const media = route.media
1355
+ ? (() => {
1356
+ const parts = [];
1357
+ if (route.media?.kind) {
1358
+ parts.push(`kind=${escape(route.media.kind)}`);
1359
+ }
1360
+ if (route.media?.contentTypes && route.media.contentTypes.length) {
1361
+ parts.push(`types=[${route.media.contentTypes
1362
+ .map((t) => escape(t))
1363
+ .join(", ")}]`);
1364
+ }
1365
+ if (route.media?.maxSize) {
1366
+ parts.push(`maxSize=${escape(route.media.maxSize)}`);
1367
+ }
1368
+ return `<div class="pill-group">
1369
+ <span class="pill pill-media">media ${parts.join(" · ")}</span>
1370
+ </div>`;
1371
+ })()
1372
+ : "";
1373
+ const errors = route.errors && route.errors.length > 0
1374
+ ? `<section class="block">
1375
+ <div class="block-title">errors</div>
1376
+ <table class="fields">
1377
+ <thead>
1378
+ <tr>
1379
+ <th>status</th>
1380
+ <th>code</th>
1381
+ </tr>
1382
+ </thead>
1383
+ <tbody>
1384
+ ${route.errors
1385
+ .map((e) => `<tr>
1386
+ <td class="field-status">${escape(e.status)}</td>
1387
+ <td class="field-error-code">${escape(e.code)}</td>
1388
+ </tr>`)
1389
+ .join("\n")}
1390
+ </tbody>
1391
+ </table>
1392
+ </section>`
1393
+ : "";
1394
+ const params = renderFieldsTable("params", route.params);
1395
+ const query = renderFieldsTable("query", route.query);
1396
+ const body = renderFieldsTable("body", route.body);
1397
+ const headers = renderFieldsTable("headers", route.headers);
1398
+ return `<article class="route-card route-${method.toLowerCase()}">
1399
+ <header class="route-header">
1400
+ <span class="method method-${method.toLowerCase()}">${method}</span>
1401
+ <span class="path">${path}</span>
1402
+ </header>
1403
+ <div class="route-meta">
1404
+ ${roles}
1405
+ ${media}
1406
+ </div>
1407
+ <div class="route-content">
1408
+ ${params}
1409
+ ${query}
1410
+ ${body}
1411
+ ${headers}
1412
+ ${errors}
1413
+ </div>
1414
+ </article>`;
1415
+ };
1416
+ const routesHtml = docs.routes.map((r) => renderRoute(r)).join("\n");
1417
+ return `<!doctype html>
1418
+ <html lang="en">
1419
+ <head>
1420
+ <meta charset="utf-8" />
1421
+ <title>${escape(title)}</title>
1422
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1423
+ <style>
1424
+ :root {
1425
+ --bg: ${bg};
1426
+ --fg: ${fg};
1427
+ --accent: #38bdf8;
1428
+ --accent-soft: rgba(56, 189, 248, 0.12);
1429
+ --card-bg: rgba(15, 23, 42, 0.9);
1430
+ --border-subtle: rgba(148, 163, 184, 0.35);
1431
+ --badge-get: #22c55e;
1432
+ --badge-post: #3b82f6;
1433
+ --badge-put: #eab308;
1434
+ --badge-delete: #ef4444;
1435
+ --badge-other: #a855f7;
1436
+ --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text",
1437
+ "Inter", sans-serif;
1438
+ --font-mono: ui-monospace, Menlo, Monaco, Consolas, "Liberation Mono",
1439
+ "Courier New", monospace;
1440
+ }
1441
+
1442
+ * {
1443
+ box-sizing: border-box;
1444
+ }
1445
+
1446
+ body {
1447
+ margin: 0;
1448
+ padding: 2.5rem 1.5rem 3rem;
1449
+ background: radial-gradient(circle at top left, #0f172a 0, var(--bg) 45%),
1450
+ radial-gradient(circle at bottom right, #020617 0, var(--bg) 55%);
1451
+ color: var(--fg);
1452
+ font-family: var(--font-sans);
1453
+ -webkit-font-smoothing: antialiased;
1454
+ }
1455
+
1456
+ .shell {
1457
+ max-width: 1120px;
1458
+ margin: 0 auto;
1459
+ }
1460
+
1461
+ .masthead {
1462
+ display: flex;
1463
+ flex-direction: column;
1464
+ gap: 0.75rem;
1465
+ margin-bottom: 2rem;
1466
+ }
1467
+
1468
+ .brand {
1469
+ display: inline-flex;
1470
+ align-items: center;
1471
+ gap: 0.5rem;
1472
+ font-family: var(--font-mono);
1473
+ font-size: 0.9rem;
1474
+ letter-spacing: 0.12em;
1475
+ text-transform: uppercase;
1476
+ color: #9ca3af;
1477
+ }
1478
+
1479
+ .brand-mark {
1480
+ width: 0.65rem;
1481
+ height: 0.65rem;
1482
+ border-radius: 999px;
1483
+ background: radial-gradient(circle at 30% 30%, #e5e7eb, #38bdf8);
1484
+ box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.2);
1485
+ }
1486
+
1487
+ .title {
1488
+ font-size: 1.9rem;
1489
+ font-weight: 600;
1490
+ letter-spacing: -0.03em;
1491
+ }
1492
+
1493
+ .subtitle {
1494
+ font-size: 0.95rem;
1495
+ color: #9ca3af;
1496
+ max-width: 520px;
1497
+ }
1498
+
1499
+ .routes-grid {
1500
+ display: grid;
1501
+ grid-template-columns: minmax(0, 1fr);
1502
+ gap: 1rem;
1503
+ }
1504
+
1505
+ @media (min-width: 900px) {
1506
+ .routes-grid {
1507
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1508
+ }
1509
+ }
1510
+
1511
+ .route-card {
1512
+ border-radius: 1rem;
1513
+ padding: 1rem 1.1rem 1.1rem;
1514
+ background: rgba(15, 23, 42, 0.92);
1515
+ border: 1px solid var(--border-subtle);
1516
+ box-shadow: 0 18px 55px rgba(15, 23, 42, 0.8);
1517
+ display: flex;
1518
+ flex-direction: column;
1519
+ gap: 0.75rem;
1520
+ }
1521
+
1522
+ .route-header {
1523
+ display: flex;
1524
+ align-items: center;
1525
+ gap: 0.75rem;
1526
+ font-family: var(--font-mono);
1527
+ font-size: 0.85rem;
1528
+ letter-spacing: 0.06em;
1529
+ text-transform: uppercase;
1530
+ }
1531
+
1532
+ .method {
1533
+ padding: 0.15rem 0.55rem;
1534
+ border-radius: 999px;
1535
+ border: 1px solid rgba(148, 163, 184, 0.5);
1536
+ background: rgba(15, 23, 42, 0.8);
1537
+ }
1538
+
1539
+ .method-get {
1540
+ border-color: rgba(34, 197, 94, 0.6);
1541
+ color: #bbf7d0;
1542
+ background: rgba(34, 197, 94, 0.08);
1543
+ }
1544
+
1545
+ .method-post {
1546
+ border-color: rgba(59, 130, 246, 0.6);
1547
+ color: #bfdbfe;
1548
+ background: rgba(59, 130, 246, 0.08);
1549
+ }
1550
+
1551
+ .method-put {
1552
+ border-color: rgba(234, 179, 8, 0.6);
1553
+ color: #fef9c3;
1554
+ background: rgba(234, 179, 8, 0.08);
1555
+ }
1556
+
1557
+ .method-delete {
1558
+ border-color: rgba(239, 68, 68, 0.6);
1559
+ color: #fee2e2;
1560
+ background: rgba(239, 68, 68, 0.08);
1561
+ }
1562
+
1563
+ .method-patch {
1564
+ border-color: rgba(168, 85, 247, 0.6);
1565
+ color: #ede9fe;
1566
+ background: rgba(168, 85, 247, 0.08);
1567
+ }
1568
+
1569
+ .path {
1570
+ padding: 0.1rem 0.65rem;
1571
+ border-radius: 999px;
1572
+ background: radial-gradient(circle at 0% 0%, #0f172a, #020617);
1573
+ border: 1px solid rgba(148, 163, 184, 0.5);
1574
+ color: #e5e7eb;
1575
+ }
1576
+
1577
+ .route-meta {
1578
+ display: flex;
1579
+ flex-direction: column;
1580
+ gap: 0.25rem;
1581
+ }
1582
+
1583
+ .pill-group {
1584
+ display: flex;
1585
+ flex-wrap: wrap;
1586
+ gap: 0.25rem;
1587
+ }
1588
+
1589
+ .pill {
1590
+ font-family: var(--font-mono);
1591
+ font-size: 0.7rem;
1592
+ padding: 0.1rem 0.45rem;
1593
+ border-radius: 999px;
1594
+ border: 1px solid rgba(148, 163, 184, 0.4);
1595
+ background: rgba(15, 23, 42, 0.9);
1596
+ color: #9ca3af;
1597
+ }
1598
+
1599
+ .pill-role {
1600
+ border-color: rgba(52, 211, 153, 0.5);
1601
+ color: #a7f3d0;
1602
+ }
1603
+
1604
+ .pill-media {
1605
+ border-color: rgba(56, 189, 248, 0.6);
1606
+ color: #bae6fd;
1607
+ background: rgba(15, 23, 42, 0.9);
1608
+ }
1609
+
1610
+ .route-content {
1611
+ display: flex;
1612
+ flex-direction: column;
1613
+ gap: 0.6rem;
1614
+ margin-top: 0.2rem;
1615
+ }
1616
+
1617
+ .block {
1618
+ border-radius: 0.75rem;
1619
+ border: 1px solid rgba(31, 41, 55, 0.9);
1620
+ background: radial-gradient(circle at top left, #020617, #020617);
1621
+ padding: 0.6rem 0.7rem 0.55rem;
1622
+ }
1623
+
1624
+ .block-title {
1625
+ font-family: var(--font-mono);
1626
+ font-size: 0.65rem;
1627
+ letter-spacing: 0.2em;
1628
+ text-transform: uppercase;
1629
+ color: #6b7280;
1630
+ margin-bottom: 0.35rem;
1631
+ }
1632
+
1633
+ table.fields {
1634
+ width: 100%;
1635
+ border-collapse: collapse;
1636
+ font-size: 0.8rem;
1637
+ font-family: var(--font-mono);
1638
+ }
1639
+
1640
+ table.fields th {
1641
+ text-align: left;
1642
+ font-weight: 500;
1643
+ color: #9ca3af;
1644
+ padding-bottom: 0.2rem;
1645
+ }
1646
+
1647
+ table.fields td {
1648
+ padding: 0.12rem 0;
1649
+ }
1650
+
1651
+ .field-name {
1652
+ color: #e5e7eb;
1653
+ }
1654
+
1655
+ .field-type {
1656
+ color: #a5b4fc;
1657
+ }
1658
+
1659
+ .field-badge {
1660
+ font-size: 0.7rem;
1661
+ padding: 0 0.4rem;
1662
+ border-radius: 999px;
1663
+ border: 1px solid rgba(148, 163, 184, 0.4);
1664
+ color: #9ca3af;
1665
+ }
1666
+
1667
+ .field-badge.required {
1668
+ border-color: rgba(248, 113, 113, 0.6);
1669
+ color: #fecaca;
1670
+ }
1671
+
1672
+ .field-badge.optional {
1673
+ border-color: rgba(59, 130, 246, 0.6);
1674
+ color: #bfdbfe;
1675
+ }
1676
+
1677
+ .field-status {
1678
+ color: #f97316;
1679
+ }
1680
+
1681
+ .field-error-code {
1682
+ color: #fca5a5;
1683
+ }
1684
+ </style>
1685
+ </head>
1686
+ <body>
1687
+ <div class="shell">
1688
+ <header class="masthead">
1689
+ <div class="brand">
1690
+ <span class="brand-mark"></span>
1691
+ <span>SCHEMAAPI · CONTRACT DOCS</span>
1692
+ </div>
1693
+ <h1 class="title">${escape(title)}</h1>
1694
+ <p class="subtitle">
1695
+ Contratos de API descritos directamente desde el runtime. Sin Swagger, sin
1696
+ YAML. Solo SchemaApi como única fuente de verdad.
1697
+ </p>
1698
+ </header>
1699
+ <main class="routes-grid">
1700
+ ${routesHtml}
1701
+ </main>
1702
+ </div>
1703
+ </body>
1704
+ </html>`;
1705
+ }
1706
+ function renderDocs(docs, options) {
1707
+ if (options.format === "json") {
1708
+ return renderDocsJSON(docs);
1709
+ }
1710
+ if (options.format === "html") {
1711
+ return renderDocsHTML(docs, options);
1712
+ }
1713
+ return renderDocsText(docs);
14
1714
  }
15
1715
 
1716
+ exports.SchemaApiError = SchemaApiError;
1717
+ exports.adapters = index;
1718
+ exports.buildErrorPayload = buildErrorPayload;
1719
+ exports.createClient = createClient;
16
1720
  exports.createContract = createContract;
1721
+ exports.generateSDK = generateSDK;
1722
+ exports.renderDocs = renderDocs;
1723
+ exports.renderDocsHTML = renderDocsHTML;
1724
+ exports.renderDocsJSON = renderDocsJSON;
1725
+ exports.renderDocsText = renderDocsText;
17
1726
 
18
1727
  }));
19
1728
  //# sourceMappingURL=schemaapi.umd.js.map