@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
package/src/docs.ts DELETED
@@ -1,535 +0,0 @@
1
- import type { ContractDocs, FieldDoc, MethodDoc } from "./core/contract";
2
-
3
- export type DocsFormat = "json" | "html" | "text";
4
-
5
- export type RenderHtmlOptions = {
6
- title?: string;
7
- theme?: "light" | "dark" | "auto";
8
- };
9
-
10
- export function renderDocsJSON(docs: ContractDocs): string {
11
- return JSON.stringify(docs, null, 2);
12
- }
13
-
14
- export function renderDocsText(docs: ContractDocs): string {
15
- const lines: string[] = [];
16
-
17
- lines.push("SchemaApi Contract");
18
- lines.push("================================");
19
-
20
- docs.routes.forEach((route) => {
21
- lines.push("");
22
- lines.push(`${route.method} ${route.path}`);
23
- lines.push("-".repeat((route.method + route.path).length + 1));
24
-
25
- if (route.roles && route.roles.length > 0) {
26
- lines.push(`roles: ${route.roles.join(", ")}`);
27
- }
28
-
29
- if (route.media) {
30
- const mediaParts: string[] = [];
31
- if (route.media.kind) {
32
- mediaParts.push(`kind=${route.media.kind}`);
33
- }
34
- if (route.media.contentTypes && route.media.contentTypes.length > 0) {
35
- mediaParts.push(`types=[${route.media.contentTypes.join(", ")}]`);
36
- }
37
- if (route.media.maxSize) {
38
- mediaParts.push(`maxSize=${route.media.maxSize}`);
39
- }
40
- lines.push(`media: ${mediaParts.join(" ")}`);
41
- }
42
-
43
- const renderSection = (name: string, fields?: FieldDoc[]) => {
44
- if (!fields || fields.length === 0) {
45
- return;
46
- }
47
- lines.push(``);
48
- lines.push(`${name}:`);
49
- fields.forEach((f) => {
50
- const flag = f.optional ? "optional" : "required";
51
- const typeName = f.type ? f.type : "unknown";
52
- lines.push(` - ${f.name}: ${typeName} (${flag})`);
53
- });
54
- };
55
-
56
- renderSection("params", route.params);
57
- renderSection("query", route.query);
58
- renderSection("body", route.body);
59
- renderSection("headers", route.headers);
60
-
61
- if (route.errors && route.errors.length > 0) {
62
- lines.push("");
63
- lines.push("errors:");
64
- route.errors.forEach((e) => {
65
- lines.push(` - ${e.status}: ${e.code}`);
66
- });
67
- }
68
- });
69
-
70
- return lines.join("\n");
71
- }
72
-
73
- function getThemeBackground(theme: RenderHtmlOptions["theme"]): string {
74
- if (theme === "dark") {
75
- return "#020617";
76
- }
77
- if (theme === "light") {
78
- return "#f9fafb";
79
- }
80
- return "#0b1120";
81
- }
82
-
83
- function getThemeForeground(theme: RenderHtmlOptions["theme"]): string {
84
- if (theme === "dark") {
85
- return "#e5e7eb";
86
- }
87
- if (theme === "light") {
88
- return "#111827";
89
- }
90
- return "#e5e7eb";
91
- }
92
-
93
- export function renderDocsHTML(
94
- docs: ContractDocs,
95
- options?: RenderHtmlOptions
96
- ): string {
97
- const title = options?.title ?? "SchemaApi Docs";
98
- const bg = getThemeBackground(options?.theme);
99
- const fg = getThemeForeground(options?.theme);
100
-
101
- const escape = (value: unknown): string => {
102
- if (value === undefined || value === null) {
103
- return "";
104
- }
105
- return String(value)
106
- .replace(/&/g, "&")
107
- .replace(/</g, "&lt;")
108
- .replace(/>/g, "&gt;")
109
- .replace(/"/g, "&quot;");
110
- };
111
-
112
- const renderFieldsTable = (caption: string, fields?: FieldDoc[]): string => {
113
- if (!fields || fields.length === 0) {
114
- return "";
115
- }
116
- const rows = fields
117
- .map((f) => {
118
- const typeName = f.type ? f.type : "unknown";
119
- const badge = f.optional ? "optional" : "required";
120
- return `<tr>
121
- <td class="field-name">${escape(f.name)}</td>
122
- <td class="field-type">${escape(typeName)}</td>
123
- <td class="field-badge ${badge}">${badge}</td>
124
- </tr>`;
125
- })
126
- .join("\n");
127
-
128
- return `<section class="block">
129
- <div class="block-title">${escape(caption)}</div>
130
- <table class="fields">
131
- <thead>
132
- <tr>
133
- <th>name</th>
134
- <th>type</th>
135
- <th>required</th>
136
- </tr>
137
- </thead>
138
- <tbody>
139
- ${rows}
140
- </tbody>
141
- </table>
142
- </section>`;
143
- };
144
-
145
- const renderRoute = (route: MethodDoc): string => {
146
- const method = escape(route.method);
147
- const path = escape(route.path);
148
-
149
- const roles =
150
- route.roles && route.roles.length > 0
151
- ? `<div class="pill-group">
152
- ${route.roles
153
- .map(
154
- (r) => `<span class="pill pill-role">role: ${escape(r)}</span>`
155
- )
156
- .join("\n")}
157
- </div>`
158
- : "";
159
-
160
- const media = route.media
161
- ? (() => {
162
- const parts: string[] = [];
163
- if (route.media?.kind) {
164
- parts.push(`kind=${escape(route.media.kind)}`);
165
- }
166
- if (route.media?.contentTypes && route.media.contentTypes.length) {
167
- parts.push(
168
- `types=[${route.media.contentTypes
169
- .map((t) => escape(t))
170
- .join(", ")}]`
171
- );
172
- }
173
- if (route.media?.maxSize) {
174
- parts.push(`maxSize=${escape(route.media.maxSize)}`);
175
- }
176
- return `<div class="pill-group">
177
- <span class="pill pill-media">media ${parts.join(" · ")}</span>
178
- </div>`;
179
- })()
180
- : "";
181
-
182
- const errors =
183
- route.errors && route.errors.length > 0
184
- ? `<section class="block">
185
- <div class="block-title">errors</div>
186
- <table class="fields">
187
- <thead>
188
- <tr>
189
- <th>status</th>
190
- <th>code</th>
191
- </tr>
192
- </thead>
193
- <tbody>
194
- ${route.errors
195
- .map(
196
- (e) => `<tr>
197
- <td class="field-status">${escape(e.status)}</td>
198
- <td class="field-error-code">${escape(e.code)}</td>
199
- </tr>`
200
- )
201
- .join("\n")}
202
- </tbody>
203
- </table>
204
- </section>`
205
- : "";
206
-
207
- const params = renderFieldsTable("params", route.params);
208
- const query = renderFieldsTable("query", route.query);
209
- const body = renderFieldsTable("body", route.body);
210
- const headers = renderFieldsTable("headers", route.headers);
211
-
212
- return `<article class="route-card route-${method.toLowerCase()}">
213
- <header class="route-header">
214
- <span class="method method-${method.toLowerCase()}">${method}</span>
215
- <span class="path">${path}</span>
216
- </header>
217
- <div class="route-meta">
218
- ${roles}
219
- ${media}
220
- </div>
221
- <div class="route-content">
222
- ${params}
223
- ${query}
224
- ${body}
225
- ${headers}
226
- ${errors}
227
- </div>
228
- </article>`;
229
- };
230
-
231
- const routesHtml = docs.routes.map((r) => renderRoute(r)).join("\n");
232
-
233
- return `<!doctype html>
234
- <html lang="en">
235
- <head>
236
- <meta charset="utf-8" />
237
- <title>${escape(title)}</title>
238
- <meta name="viewport" content="width=device-width, initial-scale=1" />
239
- <style>
240
- :root {
241
- --bg: ${bg};
242
- --fg: ${fg};
243
- --accent: #38bdf8;
244
- --accent-soft: rgba(56, 189, 248, 0.12);
245
- --card-bg: rgba(15, 23, 42, 0.9);
246
- --border-subtle: rgba(148, 163, 184, 0.35);
247
- --badge-get: #22c55e;
248
- --badge-post: #3b82f6;
249
- --badge-put: #eab308;
250
- --badge-delete: #ef4444;
251
- --badge-other: #a855f7;
252
- --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text",
253
- "Inter", sans-serif;
254
- --font-mono: ui-monospace, Menlo, Monaco, Consolas, "Liberation Mono",
255
- "Courier New", monospace;
256
- }
257
-
258
- * {
259
- box-sizing: border-box;
260
- }
261
-
262
- body {
263
- margin: 0;
264
- padding: 2.5rem 1.5rem 3rem;
265
- background: radial-gradient(circle at top left, #0f172a 0, var(--bg) 45%),
266
- radial-gradient(circle at bottom right, #020617 0, var(--bg) 55%);
267
- color: var(--fg);
268
- font-family: var(--font-sans);
269
- -webkit-font-smoothing: antialiased;
270
- }
271
-
272
- .shell {
273
- max-width: 1120px;
274
- margin: 0 auto;
275
- }
276
-
277
- .masthead {
278
- display: flex;
279
- flex-direction: column;
280
- gap: 0.75rem;
281
- margin-bottom: 2rem;
282
- }
283
-
284
- .brand {
285
- display: inline-flex;
286
- align-items: center;
287
- gap: 0.5rem;
288
- font-family: var(--font-mono);
289
- font-size: 0.9rem;
290
- letter-spacing: 0.12em;
291
- text-transform: uppercase;
292
- color: #9ca3af;
293
- }
294
-
295
- .brand-mark {
296
- width: 0.65rem;
297
- height: 0.65rem;
298
- border-radius: 999px;
299
- background: radial-gradient(circle at 30% 30%, #e5e7eb, #38bdf8);
300
- box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.2);
301
- }
302
-
303
- .title {
304
- font-size: 1.9rem;
305
- font-weight: 600;
306
- letter-spacing: -0.03em;
307
- }
308
-
309
- .subtitle {
310
- font-size: 0.95rem;
311
- color: #9ca3af;
312
- max-width: 520px;
313
- }
314
-
315
- .routes-grid {
316
- display: grid;
317
- grid-template-columns: minmax(0, 1fr);
318
- gap: 1rem;
319
- }
320
-
321
- @media (min-width: 900px) {
322
- .routes-grid {
323
- grid-template-columns: repeat(2, minmax(0, 1fr));
324
- }
325
- }
326
-
327
- .route-card {
328
- border-radius: 1rem;
329
- padding: 1rem 1.1rem 1.1rem;
330
- background: rgba(15, 23, 42, 0.92);
331
- border: 1px solid var(--border-subtle);
332
- box-shadow: 0 18px 55px rgba(15, 23, 42, 0.8);
333
- display: flex;
334
- flex-direction: column;
335
- gap: 0.75rem;
336
- }
337
-
338
- .route-header {
339
- display: flex;
340
- align-items: center;
341
- gap: 0.75rem;
342
- font-family: var(--font-mono);
343
- font-size: 0.85rem;
344
- letter-spacing: 0.06em;
345
- text-transform: uppercase;
346
- }
347
-
348
- .method {
349
- padding: 0.15rem 0.55rem;
350
- border-radius: 999px;
351
- border: 1px solid rgba(148, 163, 184, 0.5);
352
- background: rgba(15, 23, 42, 0.8);
353
- }
354
-
355
- .method-get {
356
- border-color: rgba(34, 197, 94, 0.6);
357
- color: #bbf7d0;
358
- background: rgba(34, 197, 94, 0.08);
359
- }
360
-
361
- .method-post {
362
- border-color: rgba(59, 130, 246, 0.6);
363
- color: #bfdbfe;
364
- background: rgba(59, 130, 246, 0.08);
365
- }
366
-
367
- .method-put {
368
- border-color: rgba(234, 179, 8, 0.6);
369
- color: #fef9c3;
370
- background: rgba(234, 179, 8, 0.08);
371
- }
372
-
373
- .method-delete {
374
- border-color: rgba(239, 68, 68, 0.6);
375
- color: #fee2e2;
376
- background: rgba(239, 68, 68, 0.08);
377
- }
378
-
379
- .method-patch {
380
- border-color: rgba(168, 85, 247, 0.6);
381
- color: #ede9fe;
382
- background: rgba(168, 85, 247, 0.08);
383
- }
384
-
385
- .path {
386
- padding: 0.1rem 0.65rem;
387
- border-radius: 999px;
388
- background: radial-gradient(circle at 0% 0%, #0f172a, #020617);
389
- border: 1px solid rgba(148, 163, 184, 0.5);
390
- color: #e5e7eb;
391
- }
392
-
393
- .route-meta {
394
- display: flex;
395
- flex-direction: column;
396
- gap: 0.25rem;
397
- }
398
-
399
- .pill-group {
400
- display: flex;
401
- flex-wrap: wrap;
402
- gap: 0.25rem;
403
- }
404
-
405
- .pill {
406
- font-family: var(--font-mono);
407
- font-size: 0.7rem;
408
- padding: 0.1rem 0.45rem;
409
- border-radius: 999px;
410
- border: 1px solid rgba(148, 163, 184, 0.4);
411
- background: rgba(15, 23, 42, 0.9);
412
- color: #9ca3af;
413
- }
414
-
415
- .pill-role {
416
- border-color: rgba(52, 211, 153, 0.5);
417
- color: #a7f3d0;
418
- }
419
-
420
- .pill-media {
421
- border-color: rgba(56, 189, 248, 0.6);
422
- color: #bae6fd;
423
- background: rgba(15, 23, 42, 0.9);
424
- }
425
-
426
- .route-content {
427
- display: flex;
428
- flex-direction: column;
429
- gap: 0.6rem;
430
- margin-top: 0.2rem;
431
- }
432
-
433
- .block {
434
- border-radius: 0.75rem;
435
- border: 1px solid rgba(31, 41, 55, 0.9);
436
- background: radial-gradient(circle at top left, #020617, #020617);
437
- padding: 0.6rem 0.7rem 0.55rem;
438
- }
439
-
440
- .block-title {
441
- font-family: var(--font-mono);
442
- font-size: 0.65rem;
443
- letter-spacing: 0.2em;
444
- text-transform: uppercase;
445
- color: #6b7280;
446
- margin-bottom: 0.35rem;
447
- }
448
-
449
- table.fields {
450
- width: 100%;
451
- border-collapse: collapse;
452
- font-size: 0.8rem;
453
- font-family: var(--font-mono);
454
- }
455
-
456
- table.fields th {
457
- text-align: left;
458
- font-weight: 500;
459
- color: #9ca3af;
460
- padding-bottom: 0.2rem;
461
- }
462
-
463
- table.fields td {
464
- padding: 0.12rem 0;
465
- }
466
-
467
- .field-name {
468
- color: #e5e7eb;
469
- }
470
-
471
- .field-type {
472
- color: #a5b4fc;
473
- }
474
-
475
- .field-badge {
476
- font-size: 0.7rem;
477
- padding: 0 0.4rem;
478
- border-radius: 999px;
479
- border: 1px solid rgba(148, 163, 184, 0.4);
480
- color: #9ca3af;
481
- }
482
-
483
- .field-badge.required {
484
- border-color: rgba(248, 113, 113, 0.6);
485
- color: #fecaca;
486
- }
487
-
488
- .field-badge.optional {
489
- border-color: rgba(59, 130, 246, 0.6);
490
- color: #bfdbfe;
491
- }
492
-
493
- .field-status {
494
- color: #f97316;
495
- }
496
-
497
- .field-error-code {
498
- color: #fca5a5;
499
- }
500
- </style>
501
- </head>
502
- <body>
503
- <div class="shell">
504
- <header class="masthead">
505
- <div class="brand">
506
- <span class="brand-mark"></span>
507
- <span>SCHEMAAPI · CONTRACT DOCS</span>
508
- </div>
509
- <h1 class="title">${escape(title)}</h1>
510
- <p class="subtitle">
511
- Contratos de API descritos directamente desde el runtime. Sin Swagger, sin
512
- YAML. Solo SchemaApi como única fuente de verdad.
513
- </p>
514
- </header>
515
- <main class="routes-grid">
516
- ${routesHtml}
517
- </main>
518
- </div>
519
- </body>
520
- </html>`;
521
- }
522
-
523
- export function renderDocs(
524
- docs: ContractDocs,
525
- options: { format: DocsFormat } & Partial<RenderHtmlOptions>
526
- ): string {
527
- if (options.format === "json") {
528
- return renderDocsJSON(docs);
529
- }
530
- if (options.format === "html") {
531
- return renderDocsHTML(docs, options);
532
- }
533
- return renderDocsText(docs);
534
- }
535
-
package/src/index.ts DELETED
@@ -1,5 +0,0 @@
1
- export * from "./core/contract";
2
- export * from "./core/client";
3
- export { generateSDK } from "./sdk";
4
- export * as adapters from "./adapters";
5
- export * from "./docs";
@@ -1,98 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { z } from "zod";
3
- import { contract } from "./playground";
4
- import { createContract, SchemaApiError } from "./core/contract";
5
-
6
- describe("Playground Contract", () => {
7
- it("should be defined", () => {
8
- expect(contract).toBeDefined();
9
- expect(contract.schema).toBeDefined();
10
- });
11
-
12
- it("should have the correct structure", () => {
13
- type PlaygroundSchema = Record<
14
- string,
15
- {
16
- GET?: {
17
- roles?: string[];
18
- };
19
- }
20
- >;
21
- const schema = contract.schema as PlaygroundSchema;
22
- const route = schema["/users/:id"];
23
- expect(route).toBeDefined();
24
- const getRoute = route.GET;
25
- expect(getRoute).toBeDefined();
26
- expect(getRoute?.roles ?? []).toContain("admin");
27
- });
28
-
29
- it("should fail if dynamic params in route are not declared in params schema", () => {
30
- expect(() =>
31
- createContract({
32
- "/posts/:postId/comments/:id": {
33
- GET: {
34
- params: z.object({ postId: z.string() }),
35
- response: z.object({ ok: z.boolean() }),
36
- },
37
- },
38
- }).handle("GET /posts/:postId/comments/:id", async () => ({ ok: true }))
39
- ).toThrow(
40
- "Dynamic path param :id in /posts/:postId/comments/:id is not defined in params"
41
- );
42
- });
43
-
44
- it("should allow handler errors with status codes defined in contract.errors", async () => {
45
- const c = createContract({
46
- "/users/:id": {
47
- GET: {
48
- params: z.object({ id: z.string().uuid() }),
49
- response: z.object({ ok: z.boolean() }),
50
- errors: {
51
- 404: "USER_NOT_FOUND",
52
- },
53
- },
54
- },
55
- });
56
-
57
- const handler = c.handle("GET /users/:id", async () => {
58
- throw new SchemaApiError(404, "Not Found");
59
- });
60
-
61
- await expect(
62
- handler({ params: { id: "550e8400-e29b-41d4-a716-446655440000" } })
63
- ).rejects.toMatchObject({ code: 404 });
64
- });
65
-
66
- it("should reject handler errors with status codes not defined in contract.errors", async () => {
67
- const c = createContract({
68
- "/users/:id": {
69
- GET: {
70
- params: z.object({ id: z.string().uuid() }),
71
- response: z.object({ ok: z.boolean() }),
72
- errors: {
73
- 404: "USER_NOT_FOUND",
74
- },
75
- },
76
- },
77
- });
78
-
79
- const handler = c.handle("GET /users/:id", async () => {
80
- throw new SchemaApiError(418, "Teapot");
81
- });
82
-
83
- await expect(
84
- handler({ params: { id: "550e8400-e29b-41d4-a716-446655440000" } })
85
- ).rejects.toThrow("418 is not defined in contract.errors");
86
- });
87
-
88
- it("should expose docs derived from the contract schema", () => {
89
- const docs = contract.docs();
90
- expect(docs.routes.length).toBeGreaterThan(0);
91
- const userRoute = docs.routes.find(
92
- (r) => r.path === "/users/:id" && r.method === "GET"
93
- );
94
- expect(userRoute).toBeDefined();
95
- expect(userRoute?.roles).toContain("admin");
96
- expect(userRoute?.params?.some((f) => f.name === "id")).toBe(true);
97
- });
98
- });
package/src/playground.ts DELETED
@@ -1,13 +0,0 @@
1
- import { createContract } from "./index";
2
- import { z } from "zod";
3
-
4
- export const contract = createContract({
5
- "/users/:id": {
6
- GET: {
7
- params: z.object({ id: z.string().uuid() }),
8
- headers: z.object({ authorization: z.string() }),
9
- roles: ["user", "admin"],
10
- response: z.object({ id: z.string(), username: z.string() }),
11
- },
12
- },
13
- });
package/src/sdk.ts DELETED
@@ -1,17 +0,0 @@
1
- import { createClient } from "./core/client";
2
-
3
- type GenerateSDKOptions = {
4
- baseUrl: string;
5
- fetch?: typeof fetch;
6
- };
7
-
8
- export function generateSDK<TContract>(
9
- contract: { schema: TContract },
10
- options: GenerateSDKOptions
11
- ) {
12
- return createClient(contract, {
13
- baseUrl: options.baseUrl,
14
- fetch: options.fetch,
15
- });
16
- }
17
-