@hazeljs/data 0.2.4 → 0.3.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/dist/connectors/__tests__/jsonl.connector.test.d.ts +2 -0
- package/dist/connectors/__tests__/jsonl.connector.test.d.ts.map +1 -0
- package/dist/connectors/__tests__/jsonl.connector.test.js +261 -0
- package/dist/connectors/jsonl.connector.d.ts +51 -0
- package/dist/connectors/jsonl.connector.d.ts.map +1 -0
- package/dist/connectors/jsonl.connector.js +100 -0
- package/dist/connectors/postgres.connector.d.ts +78 -0
- package/dist/connectors/postgres.connector.d.ts.map +1 -0
- package/dist/connectors/postgres.connector.js +224 -0
- package/dist/contracts/__tests__/contract-registry.test.d.ts +2 -0
- package/dist/contracts/__tests__/contract-registry.test.d.ts.map +1 -0
- package/dist/contracts/__tests__/contract-registry.test.js +770 -0
- package/dist/contracts/__tests__/contract.decorator.test.d.ts +2 -0
- package/dist/contracts/__tests__/contract.decorator.test.d.ts.map +1 -0
- package/dist/contracts/__tests__/contract.decorator.test.js +177 -0
- package/dist/contracts/contract-registry.d.ts +57 -0
- package/dist/contracts/contract-registry.d.ts.map +1 -0
- package/dist/contracts/contract-registry.js +285 -0
- package/dist/contracts/contract.decorator.d.ts +70 -0
- package/dist/contracts/contract.decorator.d.ts.map +1 -0
- package/dist/contracts/contract.decorator.js +55 -0
- package/dist/contracts/contract.types.d.ts +65 -0
- package/dist/contracts/contract.types.d.ts.map +1 -0
- package/dist/contracts/contract.types.js +5 -0
- package/dist/contracts/index.d.ts +9 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +16 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -2
- package/dist/testing/schema-faker.test.js +33 -0
- package/dist/transformers/transformer.service.test.js +40 -0
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.decorator.test.d.ts","sourceRoot":"","sources":["../../../src/contracts/__tests__/contract.decorator.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
const contract_decorator_1 = require("../contract.decorator");
|
|
10
|
+
describe('DataContract decorator', () => {
|
|
11
|
+
it('should attach metadata to class', () => {
|
|
12
|
+
let TestContract = class TestContract {
|
|
13
|
+
};
|
|
14
|
+
TestContract = __decorate([
|
|
15
|
+
(0, contract_decorator_1.DataContract)({
|
|
16
|
+
name: 'test-contract',
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
description: 'Test contract',
|
|
19
|
+
owner: 'data-team',
|
|
20
|
+
schema: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
id: { type: 'string' },
|
|
24
|
+
name: { type: 'string' },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
], TestContract);
|
|
29
|
+
const metadata = (0, contract_decorator_1.getDataContractMetadata)(TestContract);
|
|
30
|
+
expect(metadata).toBeDefined();
|
|
31
|
+
expect(metadata?.name).toBe('test-contract');
|
|
32
|
+
expect(metadata?.version).toBe('1.0.0');
|
|
33
|
+
expect(metadata?.description).toBe('Test contract');
|
|
34
|
+
expect(metadata?.owner).toBe('data-team');
|
|
35
|
+
expect(metadata?.status).toBe('active');
|
|
36
|
+
expect(metadata?.createdAt).toBeInstanceOf(Date);
|
|
37
|
+
expect(metadata?.updatedAt).toBeInstanceOf(Date);
|
|
38
|
+
});
|
|
39
|
+
it('should handle optional fields', () => {
|
|
40
|
+
let MinimalContract = class MinimalContract {
|
|
41
|
+
};
|
|
42
|
+
MinimalContract = __decorate([
|
|
43
|
+
(0, contract_decorator_1.DataContract)({
|
|
44
|
+
name: 'minimal-contract',
|
|
45
|
+
version: '1.0.0',
|
|
46
|
+
owner: 'team-a',
|
|
47
|
+
schema: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {},
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
], MinimalContract);
|
|
53
|
+
const metadata = (0, contract_decorator_1.getDataContractMetadata)(MinimalContract);
|
|
54
|
+
expect(metadata).toBeDefined();
|
|
55
|
+
expect(metadata?.name).toBe('minimal-contract');
|
|
56
|
+
expect(metadata?.description).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
it('should handle consumers and producers', () => {
|
|
59
|
+
let ConsumedContract = class ConsumedContract {
|
|
60
|
+
};
|
|
61
|
+
ConsumedContract = __decorate([
|
|
62
|
+
(0, contract_decorator_1.DataContract)({
|
|
63
|
+
name: 'consumed-contract',
|
|
64
|
+
version: '1.0.0',
|
|
65
|
+
owner: 'data-platform',
|
|
66
|
+
schema: { type: 'object', properties: {} },
|
|
67
|
+
consumers: ['service-a', 'service-b'],
|
|
68
|
+
producers: ['producer-1'],
|
|
69
|
+
})
|
|
70
|
+
], ConsumedContract);
|
|
71
|
+
const metadata = (0, contract_decorator_1.getDataContractMetadata)(ConsumedContract);
|
|
72
|
+
expect(metadata?.consumers).toEqual(['service-a', 'service-b']);
|
|
73
|
+
expect(metadata?.producers).toEqual(['producer-1']);
|
|
74
|
+
});
|
|
75
|
+
it('should handle SLA configuration', () => {
|
|
76
|
+
let SLAContract = class SLAContract {
|
|
77
|
+
};
|
|
78
|
+
SLAContract = __decorate([
|
|
79
|
+
(0, contract_decorator_1.DataContract)({
|
|
80
|
+
name: 'sla-contract',
|
|
81
|
+
version: '1.0.0',
|
|
82
|
+
owner: 'analytics-team',
|
|
83
|
+
schema: { type: 'object', properties: {} },
|
|
84
|
+
sla: {
|
|
85
|
+
freshness: {
|
|
86
|
+
maxDelayMinutes: 60,
|
|
87
|
+
checkIntervalMinutes: 15,
|
|
88
|
+
},
|
|
89
|
+
completeness: {
|
|
90
|
+
minCompleteness: 0.95,
|
|
91
|
+
requiredFields: ['id', 'timestamp'],
|
|
92
|
+
},
|
|
93
|
+
quality: {
|
|
94
|
+
minQualityScore: 0.9,
|
|
95
|
+
checks: ['no-nulls', 'valid-format'],
|
|
96
|
+
},
|
|
97
|
+
availability: {
|
|
98
|
+
minUptime: 0.99,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
], SLAContract);
|
|
103
|
+
const metadata = (0, contract_decorator_1.getDataContractMetadata)(SLAContract);
|
|
104
|
+
expect(metadata?.sla?.freshness?.maxDelayMinutes).toBe(60);
|
|
105
|
+
expect(metadata?.sla?.completeness?.minCompleteness).toBe(0.95);
|
|
106
|
+
expect(metadata?.sla?.quality?.minQualityScore).toBe(0.9);
|
|
107
|
+
expect(metadata?.sla?.availability?.minUptime).toBe(0.99);
|
|
108
|
+
});
|
|
109
|
+
it('should return undefined for non-decorated class', () => {
|
|
110
|
+
class NotDecorated {
|
|
111
|
+
}
|
|
112
|
+
const metadata = (0, contract_decorator_1.getDataContractMetadata)(NotDecorated);
|
|
113
|
+
expect(metadata).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
it('should handle complex schema', () => {
|
|
116
|
+
let ComplexContract = class ComplexContract {
|
|
117
|
+
};
|
|
118
|
+
ComplexContract = __decorate([
|
|
119
|
+
(0, contract_decorator_1.DataContract)({
|
|
120
|
+
name: 'complex-contract',
|
|
121
|
+
version: '2.0.0',
|
|
122
|
+
owner: 'ml-team',
|
|
123
|
+
schema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
id: { type: 'string' },
|
|
127
|
+
metadata: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
required: ['id'],
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
], ComplexContract);
|
|
138
|
+
const metadata = (0, contract_decorator_1.getDataContractMetadata)(ComplexContract);
|
|
139
|
+
expect(metadata?.schema.properties).toBeDefined();
|
|
140
|
+
expect(metadata?.schema.required).toEqual(['id']);
|
|
141
|
+
});
|
|
142
|
+
it('should handle tags', () => {
|
|
143
|
+
let TaggedContract = class TaggedContract {
|
|
144
|
+
};
|
|
145
|
+
TaggedContract = __decorate([
|
|
146
|
+
(0, contract_decorator_1.DataContract)({
|
|
147
|
+
name: 'tagged-contract',
|
|
148
|
+
version: '1.0.0',
|
|
149
|
+
owner: 'platform-team',
|
|
150
|
+
schema: { type: 'object', properties: {} },
|
|
151
|
+
tags: ['pii', 'critical', 'production'],
|
|
152
|
+
})
|
|
153
|
+
], TaggedContract);
|
|
154
|
+
const metadata = (0, contract_decorator_1.getDataContractMetadata)(TaggedContract);
|
|
155
|
+
expect(metadata?.tags).toEqual(['pii', 'critical', 'production']);
|
|
156
|
+
});
|
|
157
|
+
describe('hasDataContractMetadata', () => {
|
|
158
|
+
it('should return true for decorated class', () => {
|
|
159
|
+
let DecoratedClass = class DecoratedClass {
|
|
160
|
+
};
|
|
161
|
+
DecoratedClass = __decorate([
|
|
162
|
+
(0, contract_decorator_1.DataContract)({
|
|
163
|
+
name: 'test',
|
|
164
|
+
version: '1.0.0',
|
|
165
|
+
owner: 'team',
|
|
166
|
+
schema: { type: 'object', properties: {} },
|
|
167
|
+
})
|
|
168
|
+
], DecoratedClass);
|
|
169
|
+
expect((0, contract_decorator_1.hasDataContractMetadata)(DecoratedClass)).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
it('should return false for non-decorated class', () => {
|
|
172
|
+
class NonDecoratedClass {
|
|
173
|
+
}
|
|
174
|
+
expect((0, contract_decorator_1.hasDataContractMetadata)(NonDecoratedClass)).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract Registry - Manage data contracts and their versions
|
|
3
|
+
*/
|
|
4
|
+
import type { DataContract, ContractViolation, ContractDiff, ContractValidationResult } from './contract.types';
|
|
5
|
+
export declare class ContractRegistry {
|
|
6
|
+
private contracts;
|
|
7
|
+
private violations;
|
|
8
|
+
/**
|
|
9
|
+
* Register a data contract
|
|
10
|
+
*/
|
|
11
|
+
register(contract: DataContract): void;
|
|
12
|
+
/**
|
|
13
|
+
* Get a specific contract version
|
|
14
|
+
*/
|
|
15
|
+
get(name: string, version?: string): DataContract | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* List all versions of a contract
|
|
18
|
+
*/
|
|
19
|
+
listVersions(name: string): string[];
|
|
20
|
+
/**
|
|
21
|
+
* List all contracts
|
|
22
|
+
*/
|
|
23
|
+
listContracts(): Array<{
|
|
24
|
+
name: string;
|
|
25
|
+
versions: string[];
|
|
26
|
+
owner: string;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Deprecate a contract version
|
|
30
|
+
*/
|
|
31
|
+
deprecate(name: string, version: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* Compare two contract versions to detect breaking changes
|
|
34
|
+
*/
|
|
35
|
+
diff(name: string, oldVersion: string, newVersion: string): ContractDiff;
|
|
36
|
+
/**
|
|
37
|
+
* Validate data against a contract
|
|
38
|
+
*/
|
|
39
|
+
validate(name: string, data: unknown, version?: string): ContractValidationResult;
|
|
40
|
+
/**
|
|
41
|
+
* Record a contract violation
|
|
42
|
+
*/
|
|
43
|
+
recordViolation(violation: ContractViolation): void;
|
|
44
|
+
/**
|
|
45
|
+
* Get violations for a contract
|
|
46
|
+
*/
|
|
47
|
+
getViolations(name: string, version?: string): ContractViolation[];
|
|
48
|
+
/**
|
|
49
|
+
* Clear violations older than specified days
|
|
50
|
+
*/
|
|
51
|
+
clearOldViolations(days: number): void;
|
|
52
|
+
private detectSchemaChanges;
|
|
53
|
+
private validateSchema;
|
|
54
|
+
private validateCompleteness;
|
|
55
|
+
private compareVersions;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=contract-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-registry.d.ts","sourceRoot":"","sources":["../../src/contracts/contract-registry.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EAEjB,YAAY,EACZ,wBAAwB,EACzB,MAAM,kBAAkB,CAAC;AAE1B,qBACa,gBAAgB;IAC3B,OAAO,CAAC,SAAS,CAAqD;IACtE,OAAO,CAAC,UAAU,CAA2B;IAE7C;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI;IAOtC;;OAEG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAa7D;;OAEG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE;IAMpC;;OAEG;IACH,aAAa,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAe3E;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAW9C;;OAEG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,YAAY;IAqBxE;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,wBAAwB;IAwCjF;;OAEG;IACH,eAAe,CAAC,SAAS,EAAE,iBAAiB,GAAG,IAAI;IASnD;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,iBAAiB,EAAE;IAMlE;;OAEG;IACH,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAQtC,OAAO,CAAC,mBAAmB;IAmD3B,OAAO,CAAC,cAAc;IAuCtB,OAAO,CAAC,oBAAoB;IA4B5B,OAAO,CAAC,eAAe;CAYxB"}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Contract Registry - Manage data contracts and their versions
|
|
4
|
+
*/
|
|
5
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
6
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
7
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
8
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
9
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.ContractRegistry = void 0;
|
|
16
|
+
const core_1 = require("@hazeljs/core");
|
|
17
|
+
const core_2 = __importDefault(require("@hazeljs/core"));
|
|
18
|
+
let ContractRegistry = class ContractRegistry {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.contracts = new Map();
|
|
21
|
+
this.violations = [];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Register a data contract
|
|
25
|
+
*/
|
|
26
|
+
register(contract) {
|
|
27
|
+
const versions = this.contracts.get(contract.name) ?? new Map();
|
|
28
|
+
versions.set(contract.version, contract);
|
|
29
|
+
this.contracts.set(contract.name, versions);
|
|
30
|
+
core_2.default.debug(`Registered contract: ${contract.name}@${contract.version}`);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get a specific contract version
|
|
34
|
+
*/
|
|
35
|
+
get(name, version) {
|
|
36
|
+
const versions = this.contracts.get(name);
|
|
37
|
+
if (!versions)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (version) {
|
|
40
|
+
return versions.get(version);
|
|
41
|
+
}
|
|
42
|
+
// Return latest version
|
|
43
|
+
const sorted = Array.from(versions.entries()).sort((a, b) => this.compareVersions(b[0], a[0]));
|
|
44
|
+
return sorted[0]?.[1];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* List all versions of a contract
|
|
48
|
+
*/
|
|
49
|
+
listVersions(name) {
|
|
50
|
+
const versions = this.contracts.get(name);
|
|
51
|
+
if (!versions)
|
|
52
|
+
return [];
|
|
53
|
+
return Array.from(versions.keys()).sort((a, b) => this.compareVersions(b, a));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* List all contracts
|
|
57
|
+
*/
|
|
58
|
+
listContracts() {
|
|
59
|
+
const result = [];
|
|
60
|
+
for (const [name, versions] of this.contracts) {
|
|
61
|
+
const latest = this.get(name);
|
|
62
|
+
result.push({
|
|
63
|
+
name,
|
|
64
|
+
versions: Array.from(versions.keys()),
|
|
65
|
+
owner: latest?.owner ?? 'unknown',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Deprecate a contract version
|
|
72
|
+
*/
|
|
73
|
+
deprecate(name, version) {
|
|
74
|
+
const contract = this.get(name, version);
|
|
75
|
+
if (!contract) {
|
|
76
|
+
throw new Error(`Contract not found: ${name}@${version}`);
|
|
77
|
+
}
|
|
78
|
+
contract.status = 'deprecated';
|
|
79
|
+
contract.updatedAt = new Date();
|
|
80
|
+
core_2.default.debug(`Deprecated contract: ${name}@${version}`);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Compare two contract versions to detect breaking changes
|
|
84
|
+
*/
|
|
85
|
+
diff(name, oldVersion, newVersion) {
|
|
86
|
+
const oldContract = this.get(name, oldVersion);
|
|
87
|
+
const newContract = this.get(name, newVersion);
|
|
88
|
+
if (!oldContract || !newContract) {
|
|
89
|
+
throw new Error(`Cannot compare: contract versions not found`);
|
|
90
|
+
}
|
|
91
|
+
const changes = this.detectSchemaChanges(oldContract.schema, newContract.schema);
|
|
92
|
+
const breakingChanges = changes.filter((c) => c.breaking);
|
|
93
|
+
return {
|
|
94
|
+
contractName: name,
|
|
95
|
+
oldVersion,
|
|
96
|
+
newVersion,
|
|
97
|
+
changes,
|
|
98
|
+
breakingChanges,
|
|
99
|
+
isBreaking: breakingChanges.length > 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Validate data against a contract
|
|
104
|
+
*/
|
|
105
|
+
validate(name, data, version) {
|
|
106
|
+
const contract = this.get(name, version);
|
|
107
|
+
if (!contract) {
|
|
108
|
+
return {
|
|
109
|
+
valid: false,
|
|
110
|
+
violations: [
|
|
111
|
+
{
|
|
112
|
+
contractName: name,
|
|
113
|
+
contractVersion: version ?? 'latest',
|
|
114
|
+
violationType: 'schema',
|
|
115
|
+
severity: 'error',
|
|
116
|
+
message: `Contract not found: ${name}@${version ?? 'latest'}`,
|
|
117
|
+
details: {},
|
|
118
|
+
timestamp: new Date(),
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
warnings: [],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const violations = [];
|
|
125
|
+
const warnings = [];
|
|
126
|
+
// Schema validation
|
|
127
|
+
const schemaViolations = this.validateSchema(contract, data);
|
|
128
|
+
violations.push(...schemaViolations);
|
|
129
|
+
// SLA validation
|
|
130
|
+
if (contract.sla?.completeness) {
|
|
131
|
+
const completenessViolations = this.validateCompleteness(contract, data);
|
|
132
|
+
violations.push(...completenessViolations);
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
valid: violations.length === 0,
|
|
136
|
+
violations,
|
|
137
|
+
warnings,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Record a contract violation
|
|
142
|
+
*/
|
|
143
|
+
recordViolation(violation) {
|
|
144
|
+
this.violations.push(violation);
|
|
145
|
+
core_2.default.warn(`Contract violation: ${violation.contractName}@${violation.contractVersion}`, {
|
|
146
|
+
type: violation.violationType,
|
|
147
|
+
severity: violation.severity,
|
|
148
|
+
message: violation.message,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get violations for a contract
|
|
153
|
+
*/
|
|
154
|
+
getViolations(name, version) {
|
|
155
|
+
return this.violations.filter((v) => v.contractName === name && (!version || v.contractVersion === version));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Clear violations older than specified days
|
|
159
|
+
*/
|
|
160
|
+
clearOldViolations(days) {
|
|
161
|
+
const cutoff = new Date();
|
|
162
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
163
|
+
this.violations = this.violations.filter((v) => v.timestamp > cutoff);
|
|
164
|
+
}
|
|
165
|
+
// ─── Private Helpers ─────────────────────────────────────────────────────────
|
|
166
|
+
detectSchemaChanges(oldSchema, newSchema) {
|
|
167
|
+
const changes = [];
|
|
168
|
+
const allFields = new Set([...Object.keys(oldSchema), ...Object.keys(newSchema)]);
|
|
169
|
+
for (const field of allFields) {
|
|
170
|
+
const oldValue = oldSchema[field];
|
|
171
|
+
const newValue = newSchema[field];
|
|
172
|
+
if (oldValue === undefined && newValue !== undefined) {
|
|
173
|
+
// Field added
|
|
174
|
+
changes.push({
|
|
175
|
+
field,
|
|
176
|
+
changeType: 'added',
|
|
177
|
+
newValue,
|
|
178
|
+
breaking: false,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
else if (oldValue !== undefined && newValue === undefined) {
|
|
182
|
+
// Field removed - BREAKING
|
|
183
|
+
changes.push({
|
|
184
|
+
field,
|
|
185
|
+
changeType: 'removed',
|
|
186
|
+
oldValue,
|
|
187
|
+
breaking: true,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else if (typeof oldValue !== typeof newValue) {
|
|
191
|
+
// Type changed - BREAKING
|
|
192
|
+
changes.push({
|
|
193
|
+
field,
|
|
194
|
+
changeType: 'type_changed',
|
|
195
|
+
oldValue,
|
|
196
|
+
newValue,
|
|
197
|
+
breaking: true,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
|
|
201
|
+
// Field modified
|
|
202
|
+
changes.push({
|
|
203
|
+
field,
|
|
204
|
+
changeType: 'modified',
|
|
205
|
+
oldValue,
|
|
206
|
+
newValue,
|
|
207
|
+
breaking: false,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return changes;
|
|
212
|
+
}
|
|
213
|
+
validateSchema(contract, data) {
|
|
214
|
+
const violations = [];
|
|
215
|
+
if (typeof data !== 'object' || data === null) {
|
|
216
|
+
violations.push({
|
|
217
|
+
contractName: contract.name,
|
|
218
|
+
contractVersion: contract.version,
|
|
219
|
+
violationType: 'schema',
|
|
220
|
+
severity: 'error',
|
|
221
|
+
message: 'Data must be an object',
|
|
222
|
+
details: { data },
|
|
223
|
+
timestamp: new Date(),
|
|
224
|
+
});
|
|
225
|
+
return violations;
|
|
226
|
+
}
|
|
227
|
+
const record = data;
|
|
228
|
+
// Check required fields from schema
|
|
229
|
+
for (const [field, fieldSchema] of Object.entries(contract.schema)) {
|
|
230
|
+
if (typeof fieldSchema === 'object' && fieldSchema !== null) {
|
|
231
|
+
const schema = fieldSchema;
|
|
232
|
+
if (schema.required === true && record[field] === undefined) {
|
|
233
|
+
violations.push({
|
|
234
|
+
contractName: contract.name,
|
|
235
|
+
contractVersion: contract.version,
|
|
236
|
+
violationType: 'schema',
|
|
237
|
+
severity: 'error',
|
|
238
|
+
message: `Required field missing: ${field}`,
|
|
239
|
+
details: { field, schema },
|
|
240
|
+
timestamp: new Date(),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return violations;
|
|
246
|
+
}
|
|
247
|
+
validateCompleteness(contract, data) {
|
|
248
|
+
const violations = [];
|
|
249
|
+
const sla = contract.sla?.completeness;
|
|
250
|
+
if (!sla)
|
|
251
|
+
return violations;
|
|
252
|
+
const record = data;
|
|
253
|
+
const missingFields = sla.requiredFields.filter((f) => record[f] === undefined || record[f] === null);
|
|
254
|
+
if (missingFields.length > 0) {
|
|
255
|
+
const completeness = 1 - missingFields.length / sla.requiredFields.length;
|
|
256
|
+
if (completeness < sla.minCompleteness) {
|
|
257
|
+
violations.push({
|
|
258
|
+
contractName: contract.name,
|
|
259
|
+
contractVersion: contract.version,
|
|
260
|
+
violationType: 'sla',
|
|
261
|
+
severity: 'warning',
|
|
262
|
+
message: `Completeness ${(completeness * 100).toFixed(1)}% below SLA ${(sla.minCompleteness * 100).toFixed(1)}%`,
|
|
263
|
+
details: { missingFields, completeness, minCompleteness: sla.minCompleteness },
|
|
264
|
+
timestamp: new Date(),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return violations;
|
|
269
|
+
}
|
|
270
|
+
compareVersions(a, b) {
|
|
271
|
+
const aParts = a.split('.').map(Number);
|
|
272
|
+
const bParts = b.split('.').map(Number);
|
|
273
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
274
|
+
const aPart = aParts[i] ?? 0;
|
|
275
|
+
const bPart = bParts[i] ?? 0;
|
|
276
|
+
if (aPart !== bPart)
|
|
277
|
+
return aPart - bPart;
|
|
278
|
+
}
|
|
279
|
+
return 0;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
exports.ContractRegistry = ContractRegistry;
|
|
283
|
+
exports.ContractRegistry = ContractRegistry = __decorate([
|
|
284
|
+
(0, core_1.Service)()
|
|
285
|
+
], ContractRegistry);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @DataContract decorator - Define a data contract for a pipeline or dataset
|
|
3
|
+
*/
|
|
4
|
+
import 'reflect-metadata';
|
|
5
|
+
export declare const DATA_CONTRACT_METADATA_KEY: unique symbol;
|
|
6
|
+
export interface DataContractOptions {
|
|
7
|
+
name: string;
|
|
8
|
+
version: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
owner: string;
|
|
11
|
+
schema: Record<string, unknown>;
|
|
12
|
+
sla?: {
|
|
13
|
+
freshness?: {
|
|
14
|
+
maxDelayMinutes: number;
|
|
15
|
+
checkIntervalMinutes?: number;
|
|
16
|
+
};
|
|
17
|
+
completeness?: {
|
|
18
|
+
minCompleteness: number;
|
|
19
|
+
requiredFields: string[];
|
|
20
|
+
};
|
|
21
|
+
quality?: {
|
|
22
|
+
minQualityScore: number;
|
|
23
|
+
checks: string[];
|
|
24
|
+
};
|
|
25
|
+
availability?: {
|
|
26
|
+
minUptime: number;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
consumers?: string[];
|
|
30
|
+
producers?: string[];
|
|
31
|
+
tags?: string[];
|
|
32
|
+
}
|
|
33
|
+
export interface DataContractMetadata extends DataContractOptions {
|
|
34
|
+
status: 'active' | 'deprecated' | 'breaking';
|
|
35
|
+
createdAt: Date;
|
|
36
|
+
updatedAt: Date;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Mark a pipeline or class as having a data contract.
|
|
40
|
+
* The contract defines the schema, SLA, and ownership of the data.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* @DataContract({
|
|
45
|
+
* name: 'user-events',
|
|
46
|
+
* version: '1.0.0',
|
|
47
|
+
* owner: 'analytics-team',
|
|
48
|
+
* schema: {
|
|
49
|
+
* userId: { type: 'string', required: true },
|
|
50
|
+
* eventType: { type: 'string', required: true },
|
|
51
|
+
* timestamp: { type: 'date', required: true },
|
|
52
|
+
* },
|
|
53
|
+
* sla: {
|
|
54
|
+
* freshness: { maxDelayMinutes: 5 },
|
|
55
|
+
* completeness: { minCompleteness: 0.95, requiredFields: ['userId', 'eventType'] }
|
|
56
|
+
* },
|
|
57
|
+
* consumers: ['recommendation-service', 'analytics-dashboard']
|
|
58
|
+
* })
|
|
59
|
+
* @Pipeline('user-events-pipeline')
|
|
60
|
+
* class UserEventsPipeline extends PipelineBase {
|
|
61
|
+
* // ...
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare function DataContract(options: DataContractOptions): <T extends {
|
|
66
|
+
new (...args: unknown[]): object;
|
|
67
|
+
}>(target: T) => void;
|
|
68
|
+
export declare function getDataContractMetadata(target: object): DataContractMetadata | undefined;
|
|
69
|
+
export declare function hasDataContractMetadata(target: object): boolean;
|
|
70
|
+
//# sourceMappingURL=contract.decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.decorator.d.ts","sourceRoot":"","sources":["../../src/contracts/contract.decorator.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,kBAAkB,CAAC;AAE1B,eAAO,MAAM,0BAA0B,eAAyC,CAAC;AAEjF,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,GAAG,CAAC,EAAE;QACJ,SAAS,CAAC,EAAE;YACV,eAAe,EAAE,MAAM,CAAC;YACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;SAC/B,CAAC;QACF,YAAY,CAAC,EAAE;YACb,eAAe,EAAE,MAAM,CAAC;YACxB,cAAc,EAAE,MAAM,EAAE,CAAC;SAC1B,CAAC;QACF,OAAO,CAAC,EAAE;YACR,eAAe,EAAE,MAAM,CAAC;YACxB,MAAM,EAAE,MAAM,EAAE,CAAC;SAClB,CAAC;QACF,YAAY,CAAC,EAAE;YACb,SAAS,EAAE,MAAM,CAAC;SACnB,CAAC;KACH,CAAC;IACF,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,oBAAqB,SAAQ,mBAAmB;IAC/D,MAAM,EAAE,QAAQ,GAAG,YAAY,GAAG,UAAU,CAAC;IAC7C,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,IACtC,CAAC,SAAS;IAAE,KAAK,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,CAAA;CAAE,EAAE,QAAQ,CAAC,KAAG,IAAI,CAUlF;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS,CAExF;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE/D"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @DataContract decorator - Define a data contract for a pipeline or dataset
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DATA_CONTRACT_METADATA_KEY = void 0;
|
|
7
|
+
exports.DataContract = DataContract;
|
|
8
|
+
exports.getDataContractMetadata = getDataContractMetadata;
|
|
9
|
+
exports.hasDataContractMetadata = hasDataContractMetadata;
|
|
10
|
+
require("reflect-metadata");
|
|
11
|
+
exports.DATA_CONTRACT_METADATA_KEY = Symbol('hazel:data-contract:metadata');
|
|
12
|
+
/**
|
|
13
|
+
* Mark a pipeline or class as having a data contract.
|
|
14
|
+
* The contract defines the schema, SLA, and ownership of the data.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* @DataContract({
|
|
19
|
+
* name: 'user-events',
|
|
20
|
+
* version: '1.0.0',
|
|
21
|
+
* owner: 'analytics-team',
|
|
22
|
+
* schema: {
|
|
23
|
+
* userId: { type: 'string', required: true },
|
|
24
|
+
* eventType: { type: 'string', required: true },
|
|
25
|
+
* timestamp: { type: 'date', required: true },
|
|
26
|
+
* },
|
|
27
|
+
* sla: {
|
|
28
|
+
* freshness: { maxDelayMinutes: 5 },
|
|
29
|
+
* completeness: { minCompleteness: 0.95, requiredFields: ['userId', 'eventType'] }
|
|
30
|
+
* },
|
|
31
|
+
* consumers: ['recommendation-service', 'analytics-dashboard']
|
|
32
|
+
* })
|
|
33
|
+
* @Pipeline('user-events-pipeline')
|
|
34
|
+
* class UserEventsPipeline extends PipelineBase {
|
|
35
|
+
* // ...
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
function DataContract(options) {
|
|
40
|
+
return function (target) {
|
|
41
|
+
const metadata = {
|
|
42
|
+
...options,
|
|
43
|
+
status: 'active',
|
|
44
|
+
createdAt: new Date(),
|
|
45
|
+
updatedAt: new Date(),
|
|
46
|
+
};
|
|
47
|
+
Reflect.defineMetadata(exports.DATA_CONTRACT_METADATA_KEY, metadata, target);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function getDataContractMetadata(target) {
|
|
51
|
+
return Reflect.getMetadata(exports.DATA_CONTRACT_METADATA_KEY, target);
|
|
52
|
+
}
|
|
53
|
+
function hasDataContractMetadata(target) {
|
|
54
|
+
return Reflect.hasMetadata(exports.DATA_CONTRACT_METADATA_KEY, target);
|
|
55
|
+
}
|