api-json-server 1.0.1 → 1.1.0
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/README.md +286 -0
- package/dist/behavior.js +32 -0
- package/dist/index.js +115 -5
- package/dist/loadSpec.js +32 -0
- package/dist/openapi.js +152 -0
- package/dist/registerEndpoints.js +80 -0
- package/dist/requestMatch.js +60 -0
- package/dist/responseRenderer.js +98 -0
- package/dist/server.js +150 -0
- package/dist/spec.js +110 -0
- package/dist/stringTemplate.js +55 -0
- package/examples/auth-variants.json +31 -0
- package/examples/basic-crud.json +46 -0
- package/examples/companies-nested.json +47 -0
- package/examples/orders-and-matches.json +49 -0
- package/examples/users-faker.json +35 -0
- package/mock.spec.json +1 -0
- package/mockserve.spec.schema.json +7 -0
- package/package.json +15 -3
- package/scripts/build-schema.ts +21 -0
- package/src/behavior.ts +42 -0
- package/src/index.ts +108 -82
- package/src/loadSpec.ts +5 -2
- package/src/openapi.ts +203 -0
- package/src/registerEndpoints.ts +77 -163
- package/src/requestMatch.ts +62 -0
- package/src/responseRenderer.ts +112 -0
- package/src/server.ts +164 -15
- package/src/spec.ts +70 -8
- package/src/stringTemplate.ts +55 -0
- package/tests/behavior.test.ts +88 -0
- package/tests/faker.test.ts +175 -0
- package/tests/fixtures/spec.basic.json +39 -0
- package/tests/helpers.ts +28 -0
- package/tests/matching.test.ts +136 -0
- package/tests/server.test.ts +73 -0
- package/tests/template.test.ts +90 -0
- package/src/template.ts +0 -61
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildTestServerFromFixture, loadFixture } from "./helpers.js";
|
|
3
|
+
|
|
4
|
+
describe("mockserve - core behavior", () => {
|
|
5
|
+
it("renders templates using path params + query", async () => {
|
|
6
|
+
const app = await buildTestServerFromFixture("tests/fixtures/spec.basic.json");
|
|
7
|
+
|
|
8
|
+
const res = await app.inject({
|
|
9
|
+
method: "GET",
|
|
10
|
+
url: "/users/42?type=basic"
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
expect(res.statusCode).toBe(200);
|
|
14
|
+
expect(res.json()).toEqual({ id: "42", type: "basic" });
|
|
15
|
+
|
|
16
|
+
await app.close();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("enforces endpoint-level match rules (query match -> 404 when mismatch)", async () => {
|
|
20
|
+
const app = await buildTestServerFromFixture("tests/fixtures/spec.basic.json");
|
|
21
|
+
|
|
22
|
+
const ok = await app.inject({ method: "GET", url: "/search?type=premium" });
|
|
23
|
+
expect(ok.statusCode).toBe(200);
|
|
24
|
+
|
|
25
|
+
const bad = await app.inject({ method: "GET", url: "/search?type=basic" });
|
|
26
|
+
expect(bad.statusCode).toBe(404);
|
|
27
|
+
|
|
28
|
+
await app.close();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns variant response when variant match is satisfied", async () => {
|
|
32
|
+
const app = await buildTestServerFromFixture("tests/fixtures/spec.basic.json");
|
|
33
|
+
|
|
34
|
+
const res = await app.inject({
|
|
35
|
+
method: "POST",
|
|
36
|
+
url: "/login",
|
|
37
|
+
payload: { email: "a@b.com", password: "wrong" }
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(res.statusCode).toBe(401);
|
|
41
|
+
expect(res.json()).toEqual({ ok: false, error: "Invalid credentials" });
|
|
42
|
+
|
|
43
|
+
await app.close();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("falls back to base endpoint response when no variant matches", async () => {
|
|
47
|
+
const app = await buildTestServerFromFixture("tests/fixtures/spec.basic.json");
|
|
48
|
+
|
|
49
|
+
const res = await app.inject({
|
|
50
|
+
method: "POST",
|
|
51
|
+
url: "/login",
|
|
52
|
+
payload: { email: "a@b.com", password: "ok" }
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(res.statusCode).toBe(200);
|
|
56
|
+
expect(res.json()).toEqual({ ok: true, message: "default" });
|
|
57
|
+
|
|
58
|
+
await app.close();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("exposes /__spec for debugging", async () => {
|
|
62
|
+
const app = await buildTestServerFromFixture("tests/fixtures/spec.basic.json");
|
|
63
|
+
|
|
64
|
+
const res = await app.inject({ method: "GET", url: "/__spec" });
|
|
65
|
+
|
|
66
|
+
expect(res.statusCode).toBe(200);
|
|
67
|
+
const body = res.json();
|
|
68
|
+
expect(body.spec.version).toBe(1);
|
|
69
|
+
expect(body.meta.specPath).toBe("tests/fixtures/spec.basic.json");
|
|
70
|
+
|
|
71
|
+
await app.close();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildTestServer } from "./helpers.js";
|
|
3
|
+
|
|
4
|
+
const baseSettings = {
|
|
5
|
+
delayMs: 0,
|
|
6
|
+
errorRate: 0,
|
|
7
|
+
errorStatus: 500,
|
|
8
|
+
errorResponse: { error: "Mock error" }
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe("template rendering", () => {
|
|
12
|
+
it("renders params and query placeholders in responses", async () => {
|
|
13
|
+
const app = buildTestServer({
|
|
14
|
+
version: 1,
|
|
15
|
+
settings: baseSettings,
|
|
16
|
+
endpoints: [
|
|
17
|
+
{
|
|
18
|
+
method: "GET",
|
|
19
|
+
path: "/users/:id",
|
|
20
|
+
response: {
|
|
21
|
+
id: "{{params.id}}",
|
|
22
|
+
type: "{{query.type}}",
|
|
23
|
+
nested: { tag: "{{query.tag}}" }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const res = await app.inject({ method: "GET", url: "/users/42?type=basic&tag=vip" });
|
|
30
|
+
expect(res.statusCode).toBe(200);
|
|
31
|
+
expect(res.json()).toEqual({ id: "42", type: "basic", nested: { tag: "vip" } });
|
|
32
|
+
|
|
33
|
+
await app.close();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("renders body placeholders in responses", async () => {
|
|
37
|
+
const app = buildTestServer({
|
|
38
|
+
version: 1,
|
|
39
|
+
settings: baseSettings,
|
|
40
|
+
endpoints: [
|
|
41
|
+
{
|
|
42
|
+
method: "POST",
|
|
43
|
+
path: "/echo",
|
|
44
|
+
response: {
|
|
45
|
+
email: "{{body.email}}",
|
|
46
|
+
name: "{{body.profile.name}}"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const res = await app.inject({
|
|
53
|
+
method: "POST",
|
|
54
|
+
url: "/echo",
|
|
55
|
+
payload: { email: "user@example.com", profile: { name: "Mia" } }
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(res.statusCode).toBe(200);
|
|
59
|
+
expect(res.json()).toEqual({ email: "user@example.com", name: "Mia" });
|
|
60
|
+
|
|
61
|
+
await app.close();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("renders arrays of templated items", async () => {
|
|
65
|
+
const app = buildTestServer({
|
|
66
|
+
version: 1,
|
|
67
|
+
settings: baseSettings,
|
|
68
|
+
endpoints: [
|
|
69
|
+
{
|
|
70
|
+
method: "GET",
|
|
71
|
+
path: "/tags/:id",
|
|
72
|
+
response: {
|
|
73
|
+
id: "{{params.id}}",
|
|
74
|
+
tags: ["{{query.primary}}", "{{query.secondary}}"]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const res = await app.inject({
|
|
81
|
+
method: "GET",
|
|
82
|
+
url: "/tags/abc?primary=gold&secondary=silver"
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(res.statusCode).toBe(200);
|
|
86
|
+
expect(res.json()).toEqual({ id: "abc", tags: ["gold", "silver"] });
|
|
87
|
+
|
|
88
|
+
await app.close();
|
|
89
|
+
});
|
|
90
|
+
});
|
package/src/template.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
type TemplateContext = {
|
|
2
|
-
params: Record<string, unknown>;
|
|
3
|
-
query: Record<string, unknown>;
|
|
4
|
-
body: unknown;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
function getPath(obj: unknown, path: string): unknown {
|
|
8
|
-
if (!path) return undefined;
|
|
9
|
-
|
|
10
|
-
const parts = path.split(".");
|
|
11
|
-
let cur: any = obj;
|
|
12
|
-
|
|
13
|
-
for (const p of parts) {
|
|
14
|
-
if (cur == null) return undefined;
|
|
15
|
-
cur = cur[p];
|
|
16
|
-
}
|
|
17
|
-
return cur;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function renderStringTemplate(input: string, ctx: TemplateContext): string {
|
|
21
|
-
// Replaces occurrences like {{params.id}} or {{query.type}} or {{body.email}}
|
|
22
|
-
return input.replace(/\{\{\s*([a-zA-Z]+)\.([a-zA-Z0-9_.]+)\s*\}\}/g, (_m, root, path) => {
|
|
23
|
-
let source: unknown;
|
|
24
|
-
if (root === "params") source = ctx.params;
|
|
25
|
-
else if (root === "query") source = ctx.query;
|
|
26
|
-
else if (root === "body") source = ctx.body;
|
|
27
|
-
else return "";
|
|
28
|
-
|
|
29
|
-
const value = root === "body" ? getPath(source, path) : getPath(source, path);
|
|
30
|
-
|
|
31
|
-
if (value === undefined || value === null) return "";
|
|
32
|
-
if (typeof value === "string") return value;
|
|
33
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
34
|
-
|
|
35
|
-
// For objects/arrays, stringify to keep output valid (still a string substitution)
|
|
36
|
-
try {
|
|
37
|
-
return JSON.stringify(value);
|
|
38
|
-
} catch {
|
|
39
|
-
return "";
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function renderTemplate(value: unknown, ctx: TemplateContext): unknown {
|
|
45
|
-
if (typeof value === "string") return renderStringTemplate(value, ctx);
|
|
46
|
-
|
|
47
|
-
if (Array.isArray(value)) {
|
|
48
|
-
return value.map((v) => renderTemplate(v, ctx));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (value && typeof value === "object") {
|
|
52
|
-
const out: Record<string, unknown> = {};
|
|
53
|
-
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
54
|
-
out[k] = renderTemplate(v, ctx);
|
|
55
|
-
}
|
|
56
|
-
return out;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// numbers, booleans, null, undefined stay as-is
|
|
60
|
-
return value;
|
|
61
|
-
}
|