@mandujs/core 0.9.39 → 0.9.40
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.ko.md +27 -0
- package/README.md +21 -5
- package/package.json +1 -1
- package/src/config/index.ts +1 -0
- package/src/config/mandu.ts +60 -0
- package/src/contract/client-safe.test.ts +42 -0
- package/src/contract/client-safe.ts +114 -0
- package/src/contract/client.ts +12 -11
- package/src/contract/handler.ts +10 -11
- package/src/contract/index.ts +25 -16
- package/src/contract/registry.test.ts +206 -0
- package/src/contract/registry.ts +568 -0
- package/src/contract/schema.ts +48 -12
- package/src/contract/types.ts +58 -35
- package/src/contract/validator.ts +32 -17
- package/src/filling/context.ts +103 -0
- package/src/generator/templates.ts +70 -17
- package/src/guard/analyzer.ts +9 -4
- package/src/guard/check.ts +66 -30
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/file-type.test.ts +24 -0
- package/src/guard/presets/index.ts +193 -60
- package/src/guard/rules.ts +12 -6
- package/src/guard/statistics.ts +6 -0
- package/src/guard/suggestions.ts +9 -2
- package/src/guard/types.ts +11 -1
- package/src/guard/validator.ts +160 -9
- package/src/guard/watcher.ts +2 -0
- package/src/index.ts +8 -1
- package/src/runtime/index.ts +1 -0
- package/src/runtime/streaming-ssr.ts +123 -2
- package/src/seo/index.ts +214 -0
- package/src/seo/integration/ssr.ts +307 -0
- package/src/seo/render/basic.ts +427 -0
- package/src/seo/render/index.ts +143 -0
- package/src/seo/render/jsonld.ts +539 -0
- package/src/seo/render/opengraph.ts +191 -0
- package/src/seo/render/robots.ts +116 -0
- package/src/seo/render/sitemap.ts +137 -0
- package/src/seo/render/twitter.ts +126 -0
- package/src/seo/resolve/index.ts +353 -0
- package/src/seo/resolve/opengraph.ts +143 -0
- package/src/seo/resolve/robots.ts +73 -0
- package/src/seo/resolve/title.ts +94 -0
- package/src/seo/resolve/twitter.ts +73 -0
- package/src/seo/resolve/url.ts +97 -0
- package/src/seo/routes/index.ts +290 -0
- package/src/seo/types.ts +575 -0
- package/src/slot/validator.ts +39 -16
package/README.ko.md
CHANGED
|
@@ -133,6 +133,33 @@ if (!result.passed) {
|
|
|
133
133
|
| `COMPONENT_NOT_FOUND` | 컴포넌트 파일 없음 | ❌ |
|
|
134
134
|
| `SLOT_NOT_FOUND` | slot 파일 없음 | ✅ |
|
|
135
135
|
|
|
136
|
+
## Contract 모듈
|
|
137
|
+
|
|
138
|
+
Zod 기반 계약(Contract) 정의 및 타입 안전 클라이언트 생성.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { Mandu } from "@mandujs/core";
|
|
142
|
+
import { z } from "zod";
|
|
143
|
+
|
|
144
|
+
const userContract = Mandu.contract({
|
|
145
|
+
request: {
|
|
146
|
+
GET: { query: z.object({ id: z.string() }) },
|
|
147
|
+
POST: { body: z.object({ name: z.string() }) },
|
|
148
|
+
},
|
|
149
|
+
response: {
|
|
150
|
+
200: z.object({ data: z.any() }),
|
|
151
|
+
400: z.object({ error: z.string() }),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// 클라이언트에 노출할 스키마만 선택
|
|
156
|
+
const clientContract = Mandu.clientContract(userContract, {
|
|
157
|
+
request: { POST: { body: true } },
|
|
158
|
+
response: [200],
|
|
159
|
+
includeErrors: true,
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
136
163
|
## Runtime 모듈
|
|
137
164
|
|
|
138
165
|
서버 시작 및 라우팅.
|
package/README.md
CHANGED
|
@@ -160,7 +160,7 @@ listPresets().forEach(p => console.log(p.name, p.description));
|
|
|
160
160
|
|
|
161
161
|
| Preset | Layers | Use Case |
|
|
162
162
|
|--------|--------|----------|
|
|
163
|
-
| `mandu` |
|
|
163
|
+
| `mandu` | client/*, shared/(contracts, types, utils/*, schema, env), server/* | Fullstack (default) |
|
|
164
164
|
| `fsd` | app, pages, widgets, features, entities, shared | Frontend |
|
|
165
165
|
| `clean` | api, application, domain, infra, shared | Backend |
|
|
166
166
|
| `hexagonal` | adapters, ports, application, domain | DDD |
|
|
@@ -295,10 +295,26 @@ const handlers = Mandu.handler(userContract, {
|
|
|
295
295
|
POST: (ctx) => ({ data: createUser(ctx.body) })
|
|
296
296
|
});
|
|
297
297
|
|
|
298
|
-
// Type-safe client
|
|
299
|
-
const client = Mandu.client(userContract, { baseUrl: "/api/users" });
|
|
300
|
-
const result = await client.GET({ query: { id: "123" } });
|
|
301
|
-
```
|
|
298
|
+
// Type-safe client
|
|
299
|
+
const client = Mandu.client(userContract, { baseUrl: "/api/users" });
|
|
300
|
+
const result = await client.GET({ query: { id: "123" } });
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Client-safe Contract
|
|
304
|
+
|
|
305
|
+
Limit schemas exposed to client-side usage (forms/UI validation):
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
const clientContract = Mandu.clientContract(userContract, {
|
|
309
|
+
request: {
|
|
310
|
+
POST: { body: true },
|
|
311
|
+
},
|
|
312
|
+
response: [201],
|
|
313
|
+
includeErrors: true,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const client = Mandu.client(clientContract, { baseUrl: "/api/users" });
|
|
317
|
+
```
|
|
302
318
|
|
|
303
319
|
---
|
|
304
320
|
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./mandu";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
|
|
4
|
+
export type GuardRuleSeverity = "error" | "warn" | "off";
|
|
5
|
+
|
|
6
|
+
export interface ManduConfig {
|
|
7
|
+
guard?: {
|
|
8
|
+
rules?: Record<string, GuardRuleSeverity>;
|
|
9
|
+
contractRequired?: GuardRuleSeverity;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const CONFIG_FILES = [
|
|
14
|
+
"mandu.config.ts",
|
|
15
|
+
"mandu.config.js",
|
|
16
|
+
"mandu.config.json",
|
|
17
|
+
path.join(".mandu", "guard.json"),
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function coerceConfig(raw: unknown, source: string): ManduConfig {
|
|
21
|
+
if (!raw || typeof raw !== "object") return {};
|
|
22
|
+
|
|
23
|
+
// .mandu/guard.json can be guard-only
|
|
24
|
+
if (source.endsWith("guard.json") && !("guard" in (raw as Record<string, unknown>))) {
|
|
25
|
+
return { guard: raw as ManduConfig["guard"] };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return raw as ManduConfig;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function loadManduConfig(rootDir: string): Promise<ManduConfig> {
|
|
32
|
+
for (const fileName of CONFIG_FILES) {
|
|
33
|
+
const filePath = path.join(rootDir, fileName);
|
|
34
|
+
try {
|
|
35
|
+
await fs.access(filePath);
|
|
36
|
+
} catch {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (fileName.endsWith(".json")) {
|
|
41
|
+
try {
|
|
42
|
+
const content = await Bun.file(filePath).text();
|
|
43
|
+
const parsed = JSON.parse(content);
|
|
44
|
+
return coerceConfig(parsed, fileName);
|
|
45
|
+
} catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const module = await import(filePath);
|
|
52
|
+
const raw = module?.default ?? module;
|
|
53
|
+
return coerceConfig(raw, fileName);
|
|
54
|
+
} catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { Mandu, createClientContract } from "../index";
|
|
4
|
+
|
|
5
|
+
describe("Client-safe contract", () => {
|
|
6
|
+
const contract = Mandu.contract({
|
|
7
|
+
request: {
|
|
8
|
+
GET: {
|
|
9
|
+
query: z.object({ id: z.string() }),
|
|
10
|
+
},
|
|
11
|
+
POST: {
|
|
12
|
+
body: z.object({ name: z.string() }),
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
response: {
|
|
16
|
+
200: z.object({ ok: z.boolean() }),
|
|
17
|
+
201: z.object({ id: z.string() }),
|
|
18
|
+
400: z.object({ error: z.string() }),
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should pick only selected schemas", () => {
|
|
23
|
+
const clientContract = createClientContract(contract, {
|
|
24
|
+
request: {
|
|
25
|
+
POST: { body: true },
|
|
26
|
+
},
|
|
27
|
+
response: [201],
|
|
28
|
+
includeErrors: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(clientContract.request.GET).toBeUndefined();
|
|
32
|
+
expect(clientContract.request.POST?.body).toBeDefined();
|
|
33
|
+
expect(clientContract.response[200]).toBeUndefined();
|
|
34
|
+
expect(clientContract.response[201]).toBeDefined();
|
|
35
|
+
expect(clientContract.response[400]).toBeDefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should return original contract when no options are provided", () => {
|
|
39
|
+
const clientContract = createClientContract(contract);
|
|
40
|
+
expect(clientContract).toBe(contract);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Client-safe Contract Utilities
|
|
3
|
+
* Reduce contract exposure for client usage (forms, UI validation)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ContractSchema,
|
|
8
|
+
ContractMethod,
|
|
9
|
+
MethodRequestSchema,
|
|
10
|
+
ClientSafeOptions,
|
|
11
|
+
ContractRequestSchema,
|
|
12
|
+
ContractResponseSchema,
|
|
13
|
+
} from "./schema";
|
|
14
|
+
|
|
15
|
+
const ERROR_STATUS_CODES = [400, 401, 403, 404, 500] as const;
|
|
16
|
+
|
|
17
|
+
function normalizeResponseSelection(
|
|
18
|
+
selection: ClientSafeOptions["response"]
|
|
19
|
+
): number[] {
|
|
20
|
+
if (!selection) return [];
|
|
21
|
+
if (Array.isArray(selection)) return selection;
|
|
22
|
+
|
|
23
|
+
const result: number[] = [];
|
|
24
|
+
for (const [code, enabled] of Object.entries(selection)) {
|
|
25
|
+
if (enabled) {
|
|
26
|
+
const num = Number(code);
|
|
27
|
+
if (!Number.isNaN(num)) {
|
|
28
|
+
result.push(num);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pickRequestSchema(
|
|
36
|
+
methodSchema: MethodRequestSchema,
|
|
37
|
+
selection: NonNullable<ClientSafeOptions["request"]>[ContractMethod]
|
|
38
|
+
): MethodRequestSchema | undefined {
|
|
39
|
+
if (!selection) return undefined;
|
|
40
|
+
|
|
41
|
+
const picked: MethodRequestSchema = {};
|
|
42
|
+
|
|
43
|
+
if (selection.query && methodSchema.query) {
|
|
44
|
+
picked.query = methodSchema.query;
|
|
45
|
+
}
|
|
46
|
+
if (selection.body && methodSchema.body) {
|
|
47
|
+
picked.body = methodSchema.body;
|
|
48
|
+
}
|
|
49
|
+
if (selection.params && methodSchema.params) {
|
|
50
|
+
picked.params = methodSchema.params;
|
|
51
|
+
}
|
|
52
|
+
if (selection.headers && methodSchema.headers) {
|
|
53
|
+
picked.headers = methodSchema.headers;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Object.keys(picked).length > 0 ? picked : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create a client-safe contract by selecting exposed schemas.
|
|
61
|
+
* If options are omitted and contract.clientSafe is not defined,
|
|
62
|
+
* the original contract is returned (with a warning).
|
|
63
|
+
*/
|
|
64
|
+
export function createClientContract<T extends ContractSchema>(
|
|
65
|
+
contract: T,
|
|
66
|
+
options?: ClientSafeOptions
|
|
67
|
+
): ContractSchema {
|
|
68
|
+
const resolved = options ?? contract.clientSafe;
|
|
69
|
+
|
|
70
|
+
if (!resolved) {
|
|
71
|
+
console.warn(
|
|
72
|
+
"[Mandu] clientContract: no clientSafe options provided. Returning original contract."
|
|
73
|
+
);
|
|
74
|
+
return contract;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const requestSelection = resolved.request ?? {};
|
|
78
|
+
const responseSelection = normalizeResponseSelection(resolved.response);
|
|
79
|
+
const safeRequest: ContractRequestSchema = {};
|
|
80
|
+
const safeResponse: ContractResponseSchema = {};
|
|
81
|
+
|
|
82
|
+
for (const method of Object.keys(requestSelection) as ContractMethod[]) {
|
|
83
|
+
const methodSchema = contract.request[method] as MethodRequestSchema | undefined;
|
|
84
|
+
if (!methodSchema) continue;
|
|
85
|
+
|
|
86
|
+
const picked = pickRequestSchema(methodSchema, requestSelection[method]);
|
|
87
|
+
if (picked) {
|
|
88
|
+
safeRequest[method] = picked;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const allowedResponses = new Set(responseSelection);
|
|
93
|
+
|
|
94
|
+
if (resolved.includeErrors) {
|
|
95
|
+
for (const code of ERROR_STATUS_CODES) {
|
|
96
|
+
if (contract.response[code]) {
|
|
97
|
+
allowedResponses.add(code);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const code of allowedResponses) {
|
|
103
|
+
const schema = contract.response[code];
|
|
104
|
+
if (schema) {
|
|
105
|
+
safeResponse[code] = schema;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
...contract,
|
|
111
|
+
request: safeRequest,
|
|
112
|
+
response: safeResponse,
|
|
113
|
+
};
|
|
114
|
+
}
|
package/src/contract/client.ts
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { z } from "zod";
|
|
11
|
-
import type {
|
|
12
|
-
ContractSchema,
|
|
13
|
-
ContractMethod,
|
|
14
|
-
MethodRequestSchema,
|
|
15
|
-
} from "./schema";
|
|
11
|
+
import type {
|
|
12
|
+
ContractSchema,
|
|
13
|
+
ContractMethod,
|
|
14
|
+
MethodRequestSchema,
|
|
15
|
+
} from "./schema";
|
|
16
|
+
import type { InferResponseSchema } from "./types";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Client options for making requests
|
|
@@ -72,12 +73,12 @@ type InferRequestOptions<T extends MethodRequestSchema | undefined> =
|
|
|
72
73
|
/**
|
|
73
74
|
* Infer success response from contract
|
|
74
75
|
*/
|
|
75
|
-
type InferSuccessResponse<TResponse extends ContractSchema["response"]> =
|
|
76
|
-
TResponse[200] extends
|
|
77
|
-
?
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
type InferSuccessResponse<TResponse extends ContractSchema["response"]> =
|
|
77
|
+
InferResponseSchema<TResponse[200]> extends never
|
|
78
|
+
? InferResponseSchema<TResponse[201]> extends never
|
|
79
|
+
? unknown
|
|
80
|
+
: InferResponseSchema<TResponse[201]>
|
|
81
|
+
: InferResponseSchema<TResponse[200]>;
|
|
81
82
|
|
|
82
83
|
/**
|
|
83
84
|
* Contract client method
|
package/src/contract/handler.ts
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
* Elysia 패턴 채택: Contract → Handler 타입 자동 추론
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { z } from "zod";
|
|
9
|
-
import type {
|
|
10
|
-
ContractSchema,
|
|
11
|
-
ContractMethod,
|
|
12
|
-
MethodRequestSchema,
|
|
13
|
-
} from "./schema";
|
|
8
|
+
import type { z } from "zod";
|
|
9
|
+
import type {
|
|
10
|
+
ContractSchema,
|
|
11
|
+
ContractMethod,
|
|
12
|
+
MethodRequestSchema,
|
|
13
|
+
} from "./schema";
|
|
14
|
+
import type { InferResponseSchema } from "./types";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Typed request context for a handler
|
|
@@ -61,11 +62,9 @@ export type HandlerFn<TContext, TResponse> = (
|
|
|
61
62
|
/**
|
|
62
63
|
* Infer response type union from contract response schema
|
|
63
64
|
*/
|
|
64
|
-
type InferResponseUnion<TResponse extends ContractSchema["response"]> = {
|
|
65
|
-
[K in keyof TResponse]: TResponse[K]
|
|
66
|
-
|
|
67
|
-
: never;
|
|
68
|
-
}[keyof TResponse];
|
|
65
|
+
type InferResponseUnion<TResponse extends ContractSchema["response"]> = {
|
|
66
|
+
[K in keyof TResponse]: InferResponseSchema<TResponse[K]>;
|
|
67
|
+
}[keyof TResponse];
|
|
69
68
|
|
|
70
69
|
/**
|
|
71
70
|
* Handler definition for all methods in a contract
|
package/src/contract/index.ts
CHANGED
|
@@ -10,15 +10,18 @@
|
|
|
10
10
|
|
|
11
11
|
export * from "./schema";
|
|
12
12
|
export * from "./types";
|
|
13
|
-
export * from "./validator";
|
|
14
|
-
export * from "./handler";
|
|
15
|
-
export * from "./client";
|
|
16
|
-
export * from "./normalize";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
13
|
+
export * from "./validator";
|
|
14
|
+
export * from "./handler";
|
|
15
|
+
export * from "./client";
|
|
16
|
+
export * from "./normalize";
|
|
17
|
+
export * from "./registry";
|
|
18
|
+
export * from "./client-safe";
|
|
19
|
+
|
|
20
|
+
import type { ContractDefinition, ContractInstance, ContractSchema } from "./schema";
|
|
21
|
+
import type { ContractHandlers, RouteDefinition } from "./handler";
|
|
22
|
+
import { defineHandler, defineRoute } from "./handler";
|
|
23
|
+
import { createClient, contractFetch, type ClientOptions } from "./client";
|
|
24
|
+
import { createClientContract } from "./client-safe";
|
|
22
25
|
|
|
23
26
|
/**
|
|
24
27
|
* Create a Mandu API Contract
|
|
@@ -120,7 +123,7 @@ export function createContract<T extends ContractDefinition>(definition: T): T &
|
|
|
120
123
|
* Contract-specific Mandu functions
|
|
121
124
|
* Note: Use `ManduContract` to avoid conflict with other Mandu exports
|
|
122
125
|
*/
|
|
123
|
-
export const ManduContract = {
|
|
126
|
+
export const ManduContract = {
|
|
124
127
|
/**
|
|
125
128
|
* Create a typed Contract
|
|
126
129
|
* Contract 스키마 정의 및 타입 추론
|
|
@@ -162,9 +165,9 @@ export const ManduContract = {
|
|
|
162
165
|
*/
|
|
163
166
|
route: defineRoute,
|
|
164
167
|
|
|
165
|
-
/**
|
|
166
|
-
* Create a type-safe API client from contract
|
|
167
|
-
* Contract 기반 타입 안전 클라이언트 생성
|
|
168
|
+
/**
|
|
169
|
+
* Create a type-safe API client from contract
|
|
170
|
+
* Contract 기반 타입 안전 클라이언트 생성
|
|
168
171
|
*
|
|
169
172
|
* @example
|
|
170
173
|
* ```typescript
|
|
@@ -177,7 +180,13 @@ export const ManduContract = {
|
|
|
177
180
|
* const newUser = await client.POST({ body: { name: "Alice" } });
|
|
178
181
|
* ```
|
|
179
182
|
*/
|
|
180
|
-
client: createClient,
|
|
183
|
+
client: createClient,
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Create a client-safe contract
|
|
187
|
+
* Client에서 노출할 스키마만 선택
|
|
188
|
+
*/
|
|
189
|
+
clientContract: createClientContract,
|
|
181
190
|
|
|
182
191
|
/**
|
|
183
192
|
* Single type-safe fetch call
|
|
@@ -190,8 +199,8 @@ export const ManduContract = {
|
|
|
190
199
|
* });
|
|
191
200
|
* ```
|
|
192
201
|
*/
|
|
193
|
-
fetch: contractFetch,
|
|
194
|
-
} as const;
|
|
202
|
+
fetch: contractFetch,
|
|
203
|
+
} as const;
|
|
195
204
|
|
|
196
205
|
/**
|
|
197
206
|
* Alias for backward compatibility within contract module
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { diffContractRegistry, type ContractRegistry } from "./registry";
|
|
3
|
+
|
|
4
|
+
describe("Contract registry diff", () => {
|
|
5
|
+
it("should mark added optional fields as minor", () => {
|
|
6
|
+
const prev: ContractRegistry = {
|
|
7
|
+
version: 1,
|
|
8
|
+
generatedAt: "",
|
|
9
|
+
contracts: [
|
|
10
|
+
{
|
|
11
|
+
id: "users",
|
|
12
|
+
routeId: "users",
|
|
13
|
+
file: "spec/contracts/users.contract.ts",
|
|
14
|
+
methods: ["POST"],
|
|
15
|
+
request: {
|
|
16
|
+
POST: { query: false, body: true, params: false, headers: false },
|
|
17
|
+
},
|
|
18
|
+
response: [201],
|
|
19
|
+
hash: null,
|
|
20
|
+
schemas: {
|
|
21
|
+
request: {
|
|
22
|
+
POST: {
|
|
23
|
+
body: {
|
|
24
|
+
type: "object",
|
|
25
|
+
keys: ["name"],
|
|
26
|
+
required: ["name"],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
response: {
|
|
31
|
+
201: {
|
|
32
|
+
type: "object",
|
|
33
|
+
keys: ["id"],
|
|
34
|
+
required: ["id"],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const next: ContractRegistry = {
|
|
43
|
+
version: 1,
|
|
44
|
+
generatedAt: "",
|
|
45
|
+
contracts: [
|
|
46
|
+
{
|
|
47
|
+
id: "users",
|
|
48
|
+
routeId: "users",
|
|
49
|
+
file: "spec/contracts/users.contract.ts",
|
|
50
|
+
methods: ["POST"],
|
|
51
|
+
request: {
|
|
52
|
+
POST: { query: false, body: true, params: false, headers: false },
|
|
53
|
+
},
|
|
54
|
+
response: [201],
|
|
55
|
+
hash: null,
|
|
56
|
+
schemas: {
|
|
57
|
+
request: {
|
|
58
|
+
POST: {
|
|
59
|
+
body: {
|
|
60
|
+
type: "object",
|
|
61
|
+
keys: ["age", "name"],
|
|
62
|
+
required: ["name"],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
response: {
|
|
67
|
+
201: {
|
|
68
|
+
type: "object",
|
|
69
|
+
keys: ["id"],
|
|
70
|
+
required: ["id"],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const diff = diffContractRegistry(prev, next);
|
|
79
|
+
expect(diff.summary.major).toBe(0);
|
|
80
|
+
expect(diff.summary.minor).toBeGreaterThan(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should mark required field addition as major", () => {
|
|
84
|
+
const prev: ContractRegistry = {
|
|
85
|
+
version: 1,
|
|
86
|
+
generatedAt: "",
|
|
87
|
+
contracts: [
|
|
88
|
+
{
|
|
89
|
+
id: "users",
|
|
90
|
+
routeId: "users",
|
|
91
|
+
file: "spec/contracts/users.contract.ts",
|
|
92
|
+
methods: ["POST"],
|
|
93
|
+
request: {
|
|
94
|
+
POST: { query: false, body: true, params: false, headers: false },
|
|
95
|
+
},
|
|
96
|
+
response: [201],
|
|
97
|
+
hash: null,
|
|
98
|
+
schemas: {
|
|
99
|
+
request: {
|
|
100
|
+
POST: {
|
|
101
|
+
body: {
|
|
102
|
+
type: "object",
|
|
103
|
+
keys: ["name", "age"],
|
|
104
|
+
required: ["name"],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const next: ContractRegistry = {
|
|
114
|
+
version: 1,
|
|
115
|
+
generatedAt: "",
|
|
116
|
+
contracts: [
|
|
117
|
+
{
|
|
118
|
+
id: "users",
|
|
119
|
+
routeId: "users",
|
|
120
|
+
file: "spec/contracts/users.contract.ts",
|
|
121
|
+
methods: ["POST"],
|
|
122
|
+
request: {
|
|
123
|
+
POST: { query: false, body: true, params: false, headers: false },
|
|
124
|
+
},
|
|
125
|
+
response: [201],
|
|
126
|
+
hash: null,
|
|
127
|
+
schemas: {
|
|
128
|
+
request: {
|
|
129
|
+
POST: {
|
|
130
|
+
body: {
|
|
131
|
+
type: "object",
|
|
132
|
+
keys: ["name", "age"],
|
|
133
|
+
required: ["name", "age"],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const diff = diffContractRegistry(prev, next);
|
|
143
|
+
expect(diff.summary.major).toBeGreaterThan(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should mark enum value removal as major", () => {
|
|
147
|
+
const prev: ContractRegistry = {
|
|
148
|
+
version: 1,
|
|
149
|
+
generatedAt: "",
|
|
150
|
+
contracts: [
|
|
151
|
+
{
|
|
152
|
+
id: "users",
|
|
153
|
+
routeId: "users",
|
|
154
|
+
file: "spec/contracts/users.contract.ts",
|
|
155
|
+
methods: ["GET"],
|
|
156
|
+
request: {
|
|
157
|
+
GET: { query: true, body: false, params: false, headers: false },
|
|
158
|
+
},
|
|
159
|
+
response: [200],
|
|
160
|
+
hash: null,
|
|
161
|
+
schemas: {
|
|
162
|
+
request: {
|
|
163
|
+
GET: {
|
|
164
|
+
query: {
|
|
165
|
+
type: "enum",
|
|
166
|
+
values: ["active", "inactive"],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const next: ContractRegistry = {
|
|
176
|
+
version: 1,
|
|
177
|
+
generatedAt: "",
|
|
178
|
+
contracts: [
|
|
179
|
+
{
|
|
180
|
+
id: "users",
|
|
181
|
+
routeId: "users",
|
|
182
|
+
file: "spec/contracts/users.contract.ts",
|
|
183
|
+
methods: ["GET"],
|
|
184
|
+
request: {
|
|
185
|
+
GET: { query: true, body: false, params: false, headers: false },
|
|
186
|
+
},
|
|
187
|
+
response: [200],
|
|
188
|
+
hash: null,
|
|
189
|
+
schemas: {
|
|
190
|
+
request: {
|
|
191
|
+
GET: {
|
|
192
|
+
query: {
|
|
193
|
+
type: "enum",
|
|
194
|
+
values: ["active"],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const diff = diffContractRegistry(prev, next);
|
|
204
|
+
expect(diff.summary.major).toBeGreaterThan(0);
|
|
205
|
+
});
|
|
206
|
+
});
|