heicat 0.1.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/.eslintrc.js +29 -0
- package/README.md +346 -0
- package/examples/express-app/README.md +72 -0
- package/examples/express-app/contracts/auth.contract.json +38 -0
- package/examples/express-app/contracts/users.contract.json +49 -0
- package/examples/express-app/debug.js +13 -0
- package/examples/express-app/package-lock.json +913 -0
- package/examples/express-app/package.json +21 -0
- package/examples/express-app/server.js +116 -0
- package/jest.config.js +5 -0
- package/package.json +43 -0
- package/packages/cli/jest.config.js +7 -0
- package/packages/cli/package-lock.json +5041 -0
- package/packages/cli/package.json +37 -0
- package/packages/cli/src/cli.ts +49 -0
- package/packages/cli/src/commands/init.ts +103 -0
- package/packages/cli/src/commands/status.ts +75 -0
- package/packages/cli/src/commands/test.ts +188 -0
- package/packages/cli/src/commands/validate.ts +73 -0
- package/packages/cli/src/commands/watch.ts +655 -0
- package/packages/cli/src/index.ts +3 -0
- package/packages/cli/tsconfig.json +18 -0
- package/packages/core/jest.config.js +7 -0
- package/packages/core/package-lock.json +4581 -0
- package/packages/core/package.json +45 -0
- package/packages/core/src/__tests__/contract-loader.test.ts +112 -0
- package/packages/core/src/__tests__/validation-engine.test.ts +213 -0
- package/packages/core/src/contract-loader.ts +55 -0
- package/packages/core/src/engine.ts +95 -0
- package/packages/core/src/index.ts +9 -0
- package/packages/core/src/middleware.ts +97 -0
- package/packages/core/src/types/contract.ts +28 -0
- package/packages/core/src/types/options.ts +7 -0
- package/packages/core/src/types/violation.ts +19 -0
- package/packages/core/src/validation-engine.ts +157 -0
- package/packages/core/src/violation-store.ts +46 -0
- package/packages/core/tsconfig.json +18 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Contract, RequestContract, ResponseContract } from './types/contract';
|
|
3
|
+
import { Violation, ValidationResult } from './types/violation';
|
|
4
|
+
|
|
5
|
+
export class ValidationEngine {
|
|
6
|
+
private createSchema(obj: Record<string, any>): z.ZodTypeAny {
|
|
7
|
+
if (!obj || typeof obj !== 'object') {
|
|
8
|
+
return z.any();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const shape: Record<string, z.ZodTypeAny> = {};
|
|
12
|
+
|
|
13
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
14
|
+
if (typeof value === 'object' && value !== null) {
|
|
15
|
+
if (value.type) {
|
|
16
|
+
// Handle type-based schema
|
|
17
|
+
shape[key] = this.createTypeSchema(value);
|
|
18
|
+
} else {
|
|
19
|
+
// Handle nested object
|
|
20
|
+
shape[key] = this.createSchema(value);
|
|
21
|
+
}
|
|
22
|
+
} else if (typeof value === 'string') {
|
|
23
|
+
// Simple string type
|
|
24
|
+
shape[key] = z.string();
|
|
25
|
+
} else {
|
|
26
|
+
shape[key] = z.any();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return z.object(shape);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private createTypeSchema(config: any): z.ZodTypeAny {
|
|
34
|
+
let schema: z.ZodTypeAny;
|
|
35
|
+
|
|
36
|
+
switch (config.type) {
|
|
37
|
+
case 'string':
|
|
38
|
+
schema = z.string();
|
|
39
|
+
if (config.minLength) schema = (schema as z.ZodString).min(config.minLength);
|
|
40
|
+
if (config.maxLength) schema = (schema as z.ZodString).max(config.maxLength);
|
|
41
|
+
break;
|
|
42
|
+
case 'number':
|
|
43
|
+
schema = z.number();
|
|
44
|
+
if (config.minimum) schema = (schema as z.ZodNumber).min(config.minimum);
|
|
45
|
+
if (config.maximum) schema = (schema as z.ZodNumber).max(config.maximum);
|
|
46
|
+
break;
|
|
47
|
+
case 'boolean':
|
|
48
|
+
schema = z.boolean();
|
|
49
|
+
break;
|
|
50
|
+
case 'array':
|
|
51
|
+
schema = z.array(this.createTypeSchema(config.items || {}));
|
|
52
|
+
break;
|
|
53
|
+
default:
|
|
54
|
+
schema = z.any();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (config.required === false) {
|
|
58
|
+
schema = schema.optional();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return schema;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
validateRequest(
|
|
65
|
+
contract: Contract,
|
|
66
|
+
req: any
|
|
67
|
+
): ValidationResult {
|
|
68
|
+
const violations: Violation[] = [];
|
|
69
|
+
|
|
70
|
+
if (contract.request?.body) {
|
|
71
|
+
const bodySchema = this.createSchema(contract.request.body);
|
|
72
|
+
const result = bodySchema.safeParse(req.body);
|
|
73
|
+
if (!result.success) {
|
|
74
|
+
violations.push({
|
|
75
|
+
id: `req-${Date.now()}-${Math.random()}`,
|
|
76
|
+
timestamp: new Date(),
|
|
77
|
+
type: 'request',
|
|
78
|
+
contractPath: contract.path,
|
|
79
|
+
endpoint: { method: contract.method, path: contract.path },
|
|
80
|
+
expected: contract.request.body,
|
|
81
|
+
actual: req.body,
|
|
82
|
+
message: `Request body validation failed: ${result.error.message}`,
|
|
83
|
+
severity: 'error'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// TODO: Add query, params, headers validation
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
isValid: violations.length === 0,
|
|
92
|
+
violations
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
validateResponse(
|
|
97
|
+
contract: Contract,
|
|
98
|
+
statusCode: number,
|
|
99
|
+
body: any
|
|
100
|
+
): ValidationResult {
|
|
101
|
+
const violations: Violation[] = [];
|
|
102
|
+
|
|
103
|
+
if (contract.response && contract.response[statusCode]) {
|
|
104
|
+
const responseSchema = this.createSchema(contract.response[statusCode]);
|
|
105
|
+
const result = responseSchema.safeParse(body);
|
|
106
|
+
if (!result.success) {
|
|
107
|
+
violations.push({
|
|
108
|
+
id: `res-${Date.now()}-${Math.random()}`,
|
|
109
|
+
timestamp: new Date(),
|
|
110
|
+
type: 'response',
|
|
111
|
+
contractPath: contract.path,
|
|
112
|
+
endpoint: { method: contract.method, path: contract.path },
|
|
113
|
+
expected: contract.response[statusCode],
|
|
114
|
+
actual: body,
|
|
115
|
+
message: `Response validation failed for status ${statusCode}: ${result.error.message}`,
|
|
116
|
+
severity: 'warning' // In dev mode, responses are warnings
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
isValid: violations.length === 0,
|
|
123
|
+
violations
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
validateError(
|
|
128
|
+
contract: Contract,
|
|
129
|
+
statusCode: number,
|
|
130
|
+
body: any
|
|
131
|
+
): ValidationResult {
|
|
132
|
+
const violations: Violation[] = [];
|
|
133
|
+
|
|
134
|
+
if (contract.errors && contract.errors[statusCode]) {
|
|
135
|
+
const errorSchema = this.createSchema(contract.errors[statusCode]);
|
|
136
|
+
const result = errorSchema.safeParse(body);
|
|
137
|
+
if (!result.success) {
|
|
138
|
+
violations.push({
|
|
139
|
+
id: `err-${Date.now()}-${Math.random()}`,
|
|
140
|
+
timestamp: new Date(),
|
|
141
|
+
type: 'error',
|
|
142
|
+
contractPath: contract.path,
|
|
143
|
+
endpoint: { method: contract.method, path: contract.path },
|
|
144
|
+
expected: contract.errors[statusCode],
|
|
145
|
+
actual: body,
|
|
146
|
+
message: `Error validation failed for status ${statusCode}: ${result.error.message}`,
|
|
147
|
+
severity: 'warning'
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
isValid: violations.length === 0,
|
|
154
|
+
violations
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Violation } from './types/violation';
|
|
2
|
+
|
|
3
|
+
export class ViolationStore {
|
|
4
|
+
private violations: Violation[] = [];
|
|
5
|
+
private maxViolations = 1000;
|
|
6
|
+
|
|
7
|
+
add(violation: Violation): void {
|
|
8
|
+
this.violations.unshift(violation);
|
|
9
|
+
if (this.violations.length > this.maxViolations) {
|
|
10
|
+
this.violations = this.violations.slice(0, this.maxViolations);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getAll(): Violation[] {
|
|
15
|
+
return [...this.violations];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getByEndpoint(method: string, path: string): Violation[] {
|
|
19
|
+
return this.violations.filter(v =>
|
|
20
|
+
v.endpoint.method === method && v.endpoint.path === path
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getByType(type: Violation['type']): Violation[] {
|
|
25
|
+
return this.violations.filter(v => v.type === type);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
clear(): void {
|
|
29
|
+
this.violations = [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getStats() {
|
|
33
|
+
const total = this.violations.length;
|
|
34
|
+
const byType = this.violations.reduce((acc, v) => {
|
|
35
|
+
acc[v.type] = (acc[v.type] || 0) + 1;
|
|
36
|
+
return acc;
|
|
37
|
+
}, {} as Record<string, number>);
|
|
38
|
+
|
|
39
|
+
const bySeverity = this.violations.reduce((acc, v) => {
|
|
40
|
+
acc[v.severity] = (acc[v.severity] || 0) + 1;
|
|
41
|
+
return acc;
|
|
42
|
+
}, {} as Record<string, number>);
|
|
43
|
+
|
|
44
|
+
return { total, byType, bySeverity };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
18
|
+
}
|