@codexsploitx/schemaapi 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +1 -1
- package/readme.md +125 -48
- package/docs/adapters/deno.md +0 -51
- package/docs/adapters/express.md +0 -67
- package/docs/adapters/fastify.md +0 -64
- package/docs/adapters/hapi.md +0 -67
- package/docs/adapters/koa.md +0 -61
- package/docs/adapters/nest.md +0 -66
- package/docs/adapters/next.md +0 -66
- package/docs/adapters/remix.md +0 -72
- package/docs/cli.md +0 -18
- package/docs/consepts.md +0 -18
- package/docs/getting_started.md +0 -149
- package/docs/sdk.md +0 -25
- package/docs/validation.md +0 -228
- package/docs/versioning.md +0 -28
- package/eslint.config.mjs +0 -34
- package/rollup.config.js +0 -19
- package/src/adapters/deno.ts +0 -139
- package/src/adapters/express.ts +0 -134
- package/src/adapters/fastify.ts +0 -133
- package/src/adapters/hapi.ts +0 -140
- package/src/adapters/index.ts +0 -9
- package/src/adapters/koa.ts +0 -128
- package/src/adapters/nest.ts +0 -122
- package/src/adapters/next.ts +0 -175
- package/src/adapters/remix.ts +0 -145
- package/src/adapters/ws.ts +0 -132
- package/src/core/client.ts +0 -104
- package/src/core/contract.ts +0 -534
- package/src/core/versioning.test.ts +0 -174
- package/src/docs.ts +0 -535
- package/src/index.ts +0 -5
- package/src/playground.test.ts +0 -98
- package/src/playground.ts +0 -13
- package/src/sdk.ts +0 -17
- package/tests/adapters.deno.test.ts +0 -70
- package/tests/adapters.express.test.ts +0 -67
- package/tests/adapters.fastify.test.ts +0 -63
- package/tests/adapters.hapi.test.ts +0 -66
- package/tests/adapters.koa.test.ts +0 -58
- package/tests/adapters.nest.test.ts +0 -85
- package/tests/adapters.next.test.ts +0 -39
- package/tests/adapters.remix.test.ts +0 -52
- package/tests/adapters.ws.test.ts +0 -91
- package/tests/cli.test.ts +0 -156
- package/tests/client.test.ts +0 -110
- package/tests/contract.handle.test.ts +0 -267
- package/tests/docs.test.ts +0 -96
- package/tests/sdk.test.ts +0 -34
- 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, "<")
|
|
108
|
-
.replace(/>/g, ">")
|
|
109
|
-
.replace(/"/g, """);
|
|
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
package/src/playground.test.ts
DELETED
|
@@ -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
|
-
|