@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.
- package/package.json +1 -1
- 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/tests/cli.test.ts
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import childProcess from "child_process";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import fs from "fs";
|
|
5
|
-
import os from "os";
|
|
6
|
-
|
|
7
|
-
declare const __dirname: string;
|
|
8
|
-
|
|
9
|
-
type CliResult = {
|
|
10
|
-
stdout: string;
|
|
11
|
-
stderr: string;
|
|
12
|
-
code: number | null;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
function runCli(args: string[]): Promise<CliResult> {
|
|
16
|
-
return new Promise((resolve, reject) => {
|
|
17
|
-
const binPath = path.join(__dirname, "..", "bin", "schemaapi");
|
|
18
|
-
childProcess.execFile("node", [binPath, ...args], (error: unknown, stdout: unknown, stderr: unknown) => {
|
|
19
|
-
const stdoutText = String(stdout ?? "");
|
|
20
|
-
const stderrText = String(stderr ?? "");
|
|
21
|
-
|
|
22
|
-
if (error && (error as { code?: number }).code !== 0) {
|
|
23
|
-
const code = (error as { code?: number }).code ?? 1;
|
|
24
|
-
resolve({ stdout: stdoutText, stderr: stderrText, code });
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
resolve({ stdout: stdoutText, stderr: stderrText, code: 0 });
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function runCliInDir(args: string[], cwd: string): Promise<CliResult> {
|
|
33
|
-
return new Promise((resolve, reject) => {
|
|
34
|
-
const binPath = path.join(__dirname, "..", "bin", "schemaapi");
|
|
35
|
-
childProcess.execFile(
|
|
36
|
-
"node",
|
|
37
|
-
[binPath, ...args],
|
|
38
|
-
{ cwd },
|
|
39
|
-
(error: unknown, stdout: unknown, stderr: unknown) => {
|
|
40
|
-
const stdoutText = String(stdout ?? "");
|
|
41
|
-
const stderrText = String(stderr ?? "");
|
|
42
|
-
|
|
43
|
-
if (error && (error as { code?: number }).code !== 0) {
|
|
44
|
-
const code = (error as { code?: number }).code ?? 1;
|
|
45
|
-
resolve({ stdout: stdoutText, stderr: stderrText, code });
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
resolve({ stdout: stdoutText, stderr: stderrText, code: 0 });
|
|
49
|
-
}
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
describe("CLI schemaapi", () => {
|
|
55
|
-
it("muestra ayuda cuando no se pasan argumentos", async () => {
|
|
56
|
-
const result = await runCli([]);
|
|
57
|
-
expect(result.code).toBe(0);
|
|
58
|
-
expect(result.stdout).toContain("SchemaApi CLI");
|
|
59
|
-
expect(result.stdout).toContain("Usage:");
|
|
60
|
-
expect(result.stdout).toContain("schemaapi generate docs");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("ejecuta generate docs", async () => {
|
|
64
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "schemaapi-generate-docs-"));
|
|
65
|
-
|
|
66
|
-
const contractsDir = path.join(tmpDir, "contracts");
|
|
67
|
-
fs.mkdirSync(contractsDir, { recursive: true });
|
|
68
|
-
|
|
69
|
-
const indexPath = path.join(contractsDir, "index.js");
|
|
70
|
-
const indexContent = [
|
|
71
|
-
"const exampleContract = {",
|
|
72
|
-
" docs() {",
|
|
73
|
-
" return { routes: [",
|
|
74
|
-
' { method: "GET", path: "/users", params: [], query: [], body: [], headers: [], errors: [] },',
|
|
75
|
-
" ] };",
|
|
76
|
-
" },",
|
|
77
|
-
"};",
|
|
78
|
-
"module.exports = { exampleContract };",
|
|
79
|
-
"",
|
|
80
|
-
].join("\n");
|
|
81
|
-
fs.writeFileSync(indexPath, indexContent, "utf8");
|
|
82
|
-
|
|
83
|
-
const configPath = path.join(tmpDir, "schemaapi.config.json");
|
|
84
|
-
const config = {
|
|
85
|
-
adapter: "express",
|
|
86
|
-
contractsDir: "contracts",
|
|
87
|
-
};
|
|
88
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
89
|
-
|
|
90
|
-
const result = await runCliInDir(["generate", "docs"], tmpDir);
|
|
91
|
-
expect(result.code).toBe(0);
|
|
92
|
-
expect(result.stdout).toContain("Generating docs from contract");
|
|
93
|
-
expect(result.stdout).toContain("Docs generated at:");
|
|
94
|
-
|
|
95
|
-
const outDir = path.join(tmpDir, "schemaapi-docs");
|
|
96
|
-
const outFile = path.join(outDir, "index.html");
|
|
97
|
-
expect(fs.existsSync(outDir)).toBe(true);
|
|
98
|
-
expect(fs.existsSync(outFile)).toBe(true);
|
|
99
|
-
|
|
100
|
-
const html = fs.readFileSync(outFile, "utf8");
|
|
101
|
-
expect(html).toContain("<!doctype html>");
|
|
102
|
-
expect(html).toContain("/users");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("ejecuta generate sdk", async () => {
|
|
106
|
-
const result = await runCli(["generate", "sdk"]);
|
|
107
|
-
expect(result.code).toBe(0);
|
|
108
|
-
expect(result.stdout).toContain("Generating SDK from contract");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("ejecuta generate tests", async () => {
|
|
112
|
-
const result = await runCli(["generate", "tests"]);
|
|
113
|
-
expect(result.code).toBe(0);
|
|
114
|
-
expect(result.stdout).toContain("Generating tests from contract");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("ejecuta audit", async () => {
|
|
118
|
-
const result = await runCli(["audit"]);
|
|
119
|
-
expect(result.code).toBe(0);
|
|
120
|
-
expect(result.stdout.trim()).toBe("Running SchemaApi audit");
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("devuelve error para comando generate desconocido", async () => {
|
|
124
|
-
const result = await runCli(["generate", "unknown"]);
|
|
125
|
-
expect(result.code).not.toBe(0);
|
|
126
|
-
expect(result.stderr).toContain("Unknown generate target: unknown");
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("devuelve error para comando desconocido", async () => {
|
|
130
|
-
const result = await runCli(["unknown"]);
|
|
131
|
-
expect(result.code).not.toBe(0);
|
|
132
|
-
expect(result.stderr).toContain("Unknown command: unknown");
|
|
133
|
-
expect(result.stdout).toContain("SchemaApi CLI");
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("init next crea estructura de contratos y config", async () => {
|
|
137
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "schemaapi-init-next-"));
|
|
138
|
-
const result = await runCliInDir(["init", "next"], tmpDir);
|
|
139
|
-
expect(result.code).toBe(0);
|
|
140
|
-
expect(result.stdout).toContain("SchemaApi init completed");
|
|
141
|
-
|
|
142
|
-
const contractsDir = path.join(tmpDir, "contracts");
|
|
143
|
-
expect(fs.existsSync(contractsDir)).toBe(true);
|
|
144
|
-
|
|
145
|
-
const usersContractPath = path.join(contractsDir, "usersContract.ts");
|
|
146
|
-
expect(fs.existsSync(usersContractPath)).toBe(true);
|
|
147
|
-
const usersContent = fs.readFileSync(usersContractPath, "utf8");
|
|
148
|
-
expect(usersContent).toContain("createContract");
|
|
149
|
-
|
|
150
|
-
const configPath = path.join(tmpDir, "schemaapi.config.json");
|
|
151
|
-
expect(fs.existsSync(configPath)).toBe(true);
|
|
152
|
-
const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
153
|
-
expect(parsed.adapter).toBe("next");
|
|
154
|
-
expect(parsed.contractsDir).toBe("contracts");
|
|
155
|
-
});
|
|
156
|
-
});
|
package/tests/client.test.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { createClient } from "../src/core/client";
|
|
3
|
-
|
|
4
|
-
describe("createClient - SDK básico", () => {
|
|
5
|
-
it("construye correctamente GET con path params y query", async () => {
|
|
6
|
-
const fetchMock = vi.fn().mockResolvedValue({
|
|
7
|
-
ok: true,
|
|
8
|
-
status: 200,
|
|
9
|
-
statusText: "OK",
|
|
10
|
-
json: async () => ({ id: "123" }),
|
|
11
|
-
} as Response);
|
|
12
|
-
|
|
13
|
-
const client = createClient(
|
|
14
|
-
{ schema: {} as unknown },
|
|
15
|
-
{ baseUrl: "https://api.mysite.com", fetch: fetchMock as any }
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
const result = await client.users.get({
|
|
19
|
-
id: "123",
|
|
20
|
-
includePosts: true,
|
|
21
|
-
headers: { Authorization: "Bearer token" },
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
expect(result).toEqual({ id: "123" });
|
|
25
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
26
|
-
|
|
27
|
-
const [url, options] = fetchMock.mock.calls[0];
|
|
28
|
-
expect(url).toBe("https://api.mysite.com/users/123?includePosts=true");
|
|
29
|
-
expect(options.method).toBe("GET");
|
|
30
|
-
expect(options.headers).toMatchObject({
|
|
31
|
-
"Content-Type": "application/json",
|
|
32
|
-
Authorization: "Bearer token",
|
|
33
|
-
});
|
|
34
|
-
expect(options.body).toBeUndefined();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("construye correctamente PUT con body", async () => {
|
|
38
|
-
const fetchMock = vi.fn().mockResolvedValue({
|
|
39
|
-
ok: true,
|
|
40
|
-
status: 200,
|
|
41
|
-
statusText: "OK",
|
|
42
|
-
json: async () => ({ success: true }),
|
|
43
|
-
} as Response);
|
|
44
|
-
|
|
45
|
-
const client = createClient(
|
|
46
|
-
{ schema: {} as unknown },
|
|
47
|
-
{ baseUrl: "https://api.mysite.com", fetch: fetchMock as any }
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
const result = await client.users.put({
|
|
51
|
-
id: "123",
|
|
52
|
-
body: { username: "ferum" },
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
expect(result).toEqual({ success: true });
|
|
56
|
-
const [url, options] = fetchMock.mock.calls[0];
|
|
57
|
-
expect(url).toBe("https://api.mysite.com/users/123");
|
|
58
|
-
expect(options.method).toBe("PUT");
|
|
59
|
-
expect(options.body).toBe(JSON.stringify({ username: "ferum" }));
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("lanza error si la respuesta HTTP no es ok", async () => {
|
|
63
|
-
const fetchMock = vi.fn().mockResolvedValue({
|
|
64
|
-
ok: false,
|
|
65
|
-
status: 500,
|
|
66
|
-
statusText: "Server Error",
|
|
67
|
-
json: async () => ({}),
|
|
68
|
-
} as Response);
|
|
69
|
-
|
|
70
|
-
const client = createClient(
|
|
71
|
-
{ schema: {} as unknown },
|
|
72
|
-
{ baseUrl: "https://api.mysite.com", fetch: fetchMock as any }
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
await expect(
|
|
76
|
-
client.users.get({ id: "123" })
|
|
77
|
-
).rejects.toThrow("Request failed: 500 Server Error");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("construye conexión WS correctamente", async () => {
|
|
81
|
-
const wsInstance = {
|
|
82
|
-
close: vi.fn(),
|
|
83
|
-
};
|
|
84
|
-
const WebSocketSpy = vi.fn();
|
|
85
|
-
|
|
86
|
-
class MockWebSocket {
|
|
87
|
-
constructor(url: string) {
|
|
88
|
-
WebSocketSpy(url);
|
|
89
|
-
return wsInstance;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const client = createClient(
|
|
94
|
-
{ schema: {} as unknown },
|
|
95
|
-
{ baseUrl: "https://api.mysite.com", WebSocket: MockWebSocket as any }
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
const ws = await client.chat.ws({
|
|
99
|
-
roomId: "lobby",
|
|
100
|
-
token: "abc"
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
expect(ws).toBe(wsInstance);
|
|
104
|
-
expect(WebSocketSpy).toHaveBeenCalledTimes(1);
|
|
105
|
-
expect(WebSocketSpy).toHaveBeenCalledWith(
|
|
106
|
-
"wss://api.mysite.com/chat?roomId=lobby&token=abc"
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { createContract, SchemaApiError } from "../src/core/contract";
|
|
4
|
-
|
|
5
|
-
describe("createContract.handle - integración", () => {
|
|
6
|
-
const contract = createContract({
|
|
7
|
-
"/users/:id": {
|
|
8
|
-
GET: {
|
|
9
|
-
params: z.object({ id: z.string().uuid() }),
|
|
10
|
-
query: z.object({
|
|
11
|
-
includePosts: z.boolean().optional(),
|
|
12
|
-
}),
|
|
13
|
-
headers: z.object({
|
|
14
|
-
authorization: z.string(),
|
|
15
|
-
}),
|
|
16
|
-
roles: ["admin"],
|
|
17
|
-
response: z.object({
|
|
18
|
-
id: z.string(),
|
|
19
|
-
username: z.string(),
|
|
20
|
-
}),
|
|
21
|
-
errors: {
|
|
22
|
-
404: "USER_NOT_FOUND",
|
|
23
|
-
500: "INTERNAL_ERROR",
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("debe ejecutar el handler cuando todo es válido", async () => {
|
|
30
|
-
const handler = contract.handle(
|
|
31
|
-
"GET /users/:id",
|
|
32
|
-
async (ctx) =>
|
|
33
|
-
({
|
|
34
|
-
id: ctx.params && (ctx.params as any).id,
|
|
35
|
-
username: "admin",
|
|
36
|
-
}) as unknown
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const result = await handler({
|
|
40
|
-
params: { id: "550e8400-e29b-41d4-a716-446655440000" },
|
|
41
|
-
query: { includePosts: true },
|
|
42
|
-
headers: { authorization: "Bearer token" },
|
|
43
|
-
user: { role: "admin" },
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
expect(result).toEqual({
|
|
47
|
-
id: "550e8400-e29b-41d4-a716-446655440000",
|
|
48
|
-
username: "admin",
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("debe lanzar 404 si la ruta no existe", () => {
|
|
53
|
-
const other = createContract({
|
|
54
|
-
"/other": {
|
|
55
|
-
GET: {
|
|
56
|
-
response: z.object({ ok: z.boolean() }),
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
expect(() =>
|
|
62
|
-
other.handle("GET /missing", async () => ({ ok: true }))
|
|
63
|
-
).toThrowError(/Route \/missing not defined in contract/);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("debe lanzar 405 si el método no está definido en la ruta", () => {
|
|
67
|
-
expect(() =>
|
|
68
|
-
contract.handle("POST /users/:id", async () => ({}))
|
|
69
|
-
).toThrowError(/Method POST not defined for route \/users\/:id/);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("valida path params y lanza 400 si son inválidos", async () => {
|
|
73
|
-
const handler = contract.handle("GET /users/:id", async () => ({
|
|
74
|
-
id: "invalid",
|
|
75
|
-
username: "x",
|
|
76
|
-
}));
|
|
77
|
-
|
|
78
|
-
await expect(
|
|
79
|
-
handler({
|
|
80
|
-
params: { id: "not-a-uuid" },
|
|
81
|
-
query: {},
|
|
82
|
-
headers: { authorization: "Bearer token" },
|
|
83
|
-
user: { role: "admin" },
|
|
84
|
-
})
|
|
85
|
-
).rejects.toMatchObject({
|
|
86
|
-
code: 400,
|
|
87
|
-
} as Partial<SchemaApiError>);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("valida headers y roles, lanza 403 si el rol no está permitido", async () => {
|
|
91
|
-
const handler = contract.handle("GET /users/:id", async () => ({
|
|
92
|
-
id: "550e8400-e29b-41d4-a716-446655440000",
|
|
93
|
-
username: "x",
|
|
94
|
-
}));
|
|
95
|
-
|
|
96
|
-
await expect(
|
|
97
|
-
handler({
|
|
98
|
-
params: { id: "550e8400-e29b-41d4-a716-446655440000" },
|
|
99
|
-
query: {},
|
|
100
|
-
headers: { authorization: "Bearer token" },
|
|
101
|
-
user: { role: "user" },
|
|
102
|
-
})
|
|
103
|
-
).rejects.toMatchObject({
|
|
104
|
-
code: 403,
|
|
105
|
-
} as Partial<SchemaApiError>);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("valida la response y lanza 500 si no cumple el schema", async () => {
|
|
109
|
-
const handler = contract.handle("GET /users/:id", async () => ({
|
|
110
|
-
id: "550e8400-e29b-41d4-a716-446655440000",
|
|
111
|
-
// falta username
|
|
112
|
-
}));
|
|
113
|
-
|
|
114
|
-
await expect(
|
|
115
|
-
handler({
|
|
116
|
-
params: { id: "550e8400-e29b-41d4-a716-446655440000" },
|
|
117
|
-
query: {},
|
|
118
|
-
headers: { authorization: "Bearer token" },
|
|
119
|
-
user: { role: "admin" },
|
|
120
|
-
})
|
|
121
|
-
).rejects.toMatchObject({
|
|
122
|
-
code: 500,
|
|
123
|
-
} as Partial<SchemaApiError>);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("permite errores con status codes declarados en errors", async () => {
|
|
127
|
-
const c = createContract({
|
|
128
|
-
"/users/:id": {
|
|
129
|
-
GET: {
|
|
130
|
-
params: z.object({ id: z.string().uuid() }),
|
|
131
|
-
response: z.object({ ok: z.boolean() }),
|
|
132
|
-
errors: {
|
|
133
|
-
404: "USER_NOT_FOUND",
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const handler = c.handle("GET /users/:id", async () => {
|
|
140
|
-
throw new SchemaApiError(404, "USER_NOT_FOUND");
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
await expect(
|
|
144
|
-
handler({
|
|
145
|
-
params: { id: "550e8400-e29b-41d4-a716-446655440000" },
|
|
146
|
-
})
|
|
147
|
-
).rejects.toMatchObject({
|
|
148
|
-
code: 404,
|
|
149
|
-
} as Partial<SchemaApiError>);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("rechaza errores con status codes no declarados en errors", async () => {
|
|
153
|
-
const c = createContract({
|
|
154
|
-
"/users/:id": {
|
|
155
|
-
GET: {
|
|
156
|
-
params: z.object({ id: z.string().uuid() }),
|
|
157
|
-
response: z.object({ ok: z.boolean() }),
|
|
158
|
-
errors: {
|
|
159
|
-
404: "USER_NOT_FOUND",
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const handler = c.handle("GET /users/:id", async () => {
|
|
166
|
-
throw new SchemaApiError(418, "Teapot");
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
await expect(
|
|
170
|
-
handler({
|
|
171
|
-
params: { id: "550e8400-e29b-41d4-a716-446655440000" },
|
|
172
|
-
})
|
|
173
|
-
).rejects.toThrow("418 is not defined in contract.errors");
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("valida media upload con content-type permitido y tamaño válido", async () => {
|
|
177
|
-
const mediaContract = createContract({
|
|
178
|
-
"/upload": {
|
|
179
|
-
POST: {
|
|
180
|
-
media: {
|
|
181
|
-
kind: "upload",
|
|
182
|
-
contentTypes: ["image/png", "image/jpeg"],
|
|
183
|
-
maxSize: 1024,
|
|
184
|
-
},
|
|
185
|
-
body: z.any(),
|
|
186
|
-
response: z.object({ ok: z.boolean() }),
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const handler = mediaContract.handle("POST /upload", async () => ({
|
|
192
|
-
ok: true,
|
|
193
|
-
}));
|
|
194
|
-
|
|
195
|
-
const result = await handler({
|
|
196
|
-
headers: {
|
|
197
|
-
"content-type": "image/png",
|
|
198
|
-
"content-length": "512",
|
|
199
|
-
},
|
|
200
|
-
body: Buffer.from("test"),
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
expect(result).toEqual({ ok: true });
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("rechaza media upload con content-type no permitido", async () => {
|
|
207
|
-
const mediaContract = createContract({
|
|
208
|
-
"/upload": {
|
|
209
|
-
POST: {
|
|
210
|
-
media: {
|
|
211
|
-
kind: "upload",
|
|
212
|
-
contentTypes: ["image/png"],
|
|
213
|
-
},
|
|
214
|
-
body: z.any(),
|
|
215
|
-
response: z.object({ ok: z.boolean() }),
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const handler = mediaContract.handle("POST /upload", async () => ({
|
|
221
|
-
ok: true,
|
|
222
|
-
}));
|
|
223
|
-
|
|
224
|
-
await expect(
|
|
225
|
-
handler({
|
|
226
|
-
headers: {
|
|
227
|
-
"content-type": "application/pdf",
|
|
228
|
-
},
|
|
229
|
-
body: Buffer.from("test"),
|
|
230
|
-
})
|
|
231
|
-
).rejects.toMatchObject({
|
|
232
|
-
code: 415,
|
|
233
|
-
} as Partial<SchemaApiError>);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it("rechaza media upload cuando el tamaño excede maxSize", async () => {
|
|
237
|
-
const mediaContract = createContract({
|
|
238
|
-
"/upload": {
|
|
239
|
-
POST: {
|
|
240
|
-
media: {
|
|
241
|
-
kind: "upload",
|
|
242
|
-
contentTypes: ["image/png"],
|
|
243
|
-
maxSize: 1024,
|
|
244
|
-
},
|
|
245
|
-
body: z.any(),
|
|
246
|
-
response: z.object({ ok: z.boolean() }),
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
const handler = mediaContract.handle("POST /upload", async () => ({
|
|
252
|
-
ok: true,
|
|
253
|
-
}));
|
|
254
|
-
|
|
255
|
-
await expect(
|
|
256
|
-
handler({
|
|
257
|
-
headers: {
|
|
258
|
-
"content-type": "image/png",
|
|
259
|
-
"content-length": "2048",
|
|
260
|
-
},
|
|
261
|
-
body: Buffer.from("test"),
|
|
262
|
-
})
|
|
263
|
-
).rejects.toMatchObject({
|
|
264
|
-
code: 413,
|
|
265
|
-
} as Partial<SchemaApiError>);
|
|
266
|
-
});
|
|
267
|
-
});
|
package/tests/docs.test.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { createContract } from "../src/core/contract";
|
|
4
|
-
import {
|
|
5
|
-
renderDocs,
|
|
6
|
-
renderDocsHTML,
|
|
7
|
-
renderDocsJSON,
|
|
8
|
-
renderDocsText,
|
|
9
|
-
} from "../src/docs";
|
|
10
|
-
|
|
11
|
-
describe("docs generator", () => {
|
|
12
|
-
const contract = createContract({
|
|
13
|
-
"/users/:id": {
|
|
14
|
-
GET: {
|
|
15
|
-
params: z.object({ id: z.string().uuid() }),
|
|
16
|
-
query: z.object({ includePosts: z.boolean().optional() }),
|
|
17
|
-
headers: z.object({ authorization: z.string() }),
|
|
18
|
-
roles: ["admin"],
|
|
19
|
-
response: z.object({
|
|
20
|
-
id: z.string(),
|
|
21
|
-
username: z.string(),
|
|
22
|
-
}),
|
|
23
|
-
errors: {
|
|
24
|
-
404: "USER_NOT_FOUND",
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("renderDocsJSON devuelve un JSON legible", () => {
|
|
31
|
-
const docs = contract.docs();
|
|
32
|
-
const json = renderDocsJSON(docs);
|
|
33
|
-
const parsed = JSON.parse(json) as unknown;
|
|
34
|
-
expect(parsed).toBeDefined();
|
|
35
|
-
expect((parsed as any).routes).toHaveLength(1);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("renderDocsText genera una representación de texto", () => {
|
|
39
|
-
const docs = contract.docs();
|
|
40
|
-
const text = renderDocsText(docs);
|
|
41
|
-
expect(text).toContain("SchemaApi Contract");
|
|
42
|
-
expect(text).toContain("GET /users/:id");
|
|
43
|
-
expect(text).toContain("params:");
|
|
44
|
-
expect(text).toContain("errors:");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("renderDocsHTML genera un documento HTML con estilo propio", () => {
|
|
48
|
-
const docs = contract.docs();
|
|
49
|
-
const html = renderDocsHTML(docs, { title: "Users API", theme: "dark" });
|
|
50
|
-
expect(html).toContain("<!doctype html>");
|
|
51
|
-
expect(html).toContain("Users API");
|
|
52
|
-
expect(html).toContain("SCHEMAAPI · CONTRACT DOCS");
|
|
53
|
-
expect(html).toContain('<span class="method method-get">GET</span>');
|
|
54
|
-
expect(html).toContain('<span class="path">/users/:id</span>');
|
|
55
|
-
expect(html).toContain("params");
|
|
56
|
-
expect(html).toContain("errors");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("renderDocsHTML genera tarjetas con roles y media", () => {
|
|
60
|
-
const filesContract = createContract({
|
|
61
|
-
"/files": {
|
|
62
|
-
POST: {
|
|
63
|
-
body: z.object({ filename: z.string() }),
|
|
64
|
-
media: {
|
|
65
|
-
kind: "upload",
|
|
66
|
-
contentTypes: ["image/png"],
|
|
67
|
-
maxSize: 1024,
|
|
68
|
-
},
|
|
69
|
-
roles: ["uploader"],
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const docs = filesContract.docs();
|
|
75
|
-
const html = renderDocsHTML(docs, { title: "Files API", theme: "light" });
|
|
76
|
-
|
|
77
|
-
expect(html).toContain('class="route-card route-post"');
|
|
78
|
-
expect(html).toContain('<span class="method method-post">POST</span>');
|
|
79
|
-
expect(html).toContain('<span class="path">/files</span>');
|
|
80
|
-
expect(html).toContain("role: uploader");
|
|
81
|
-
expect(html).toContain("media kind=upload");
|
|
82
|
-
expect(html).toContain("types=[image/png]");
|
|
83
|
-
expect(html).toContain("maxSize=1024");
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("renderDocs selecciona el formato correcto", () => {
|
|
87
|
-
const docs = contract.docs();
|
|
88
|
-
const html = renderDocs(docs, { format: "html", title: "X" });
|
|
89
|
-
const json = renderDocs(docs, { format: "json" });
|
|
90
|
-
const text = renderDocs(docs, { format: "text" });
|
|
91
|
-
|
|
92
|
-
expect(html).toContain("<!doctype html>");
|
|
93
|
-
expect(() => JSON.parse(json)).not.toThrow();
|
|
94
|
-
expect(text).toContain("SchemaApi Contract");
|
|
95
|
-
});
|
|
96
|
-
});
|
package/tests/sdk.test.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { generateSDK } from "../src/sdk";
|
|
3
|
-
import { createContract } from "../src/core/contract";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
|
|
6
|
-
describe("generateSDK", () => {
|
|
7
|
-
const contract = createContract({
|
|
8
|
-
"/users/:id": {
|
|
9
|
-
GET: {
|
|
10
|
-
params: z.object({ id: z.string() }),
|
|
11
|
-
response: z.object({ id: z.string() }),
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("crea un cliente que usa baseUrl y fetch proporcionado", async () => {
|
|
17
|
-
const fetchMock = vi.fn().mockResolvedValue({
|
|
18
|
-
ok: true,
|
|
19
|
-
status: 200,
|
|
20
|
-
statusText: "OK",
|
|
21
|
-
json: async () => ({ id: "123" }),
|
|
22
|
-
} as Response);
|
|
23
|
-
|
|
24
|
-
const sdk = generateSDK(contract, {
|
|
25
|
-
baseUrl: "https://api.mysite.com",
|
|
26
|
-
fetch: fetchMock as any,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const result = await sdk["users"].get({ id: "123" });
|
|
30
|
-
|
|
31
|
-
expect(result).toEqual({ id: "123" });
|
|
32
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
33
|
-
});
|
|
34
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "Node",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"outDir": "dist",
|
|
8
|
-
"rootDir": "src",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true
|
|
13
|
-
},
|
|
14
|
-
"include": ["src/**/*"]
|
|
15
|
-
}
|