@hazeljs/data 0.2.3 → 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.
Files changed (33) hide show
  1. package/dist/connectors/__tests__/jsonl.connector.test.d.ts +2 -0
  2. package/dist/connectors/__tests__/jsonl.connector.test.d.ts.map +1 -0
  3. package/dist/connectors/__tests__/jsonl.connector.test.js +261 -0
  4. package/dist/connectors/jsonl.connector.d.ts +51 -0
  5. package/dist/connectors/jsonl.connector.d.ts.map +1 -0
  6. package/dist/connectors/jsonl.connector.js +100 -0
  7. package/dist/connectors/postgres.connector.d.ts +78 -0
  8. package/dist/connectors/postgres.connector.d.ts.map +1 -0
  9. package/dist/connectors/postgres.connector.js +224 -0
  10. package/dist/contracts/__tests__/contract-registry.test.d.ts +2 -0
  11. package/dist/contracts/__tests__/contract-registry.test.d.ts.map +1 -0
  12. package/dist/contracts/__tests__/contract-registry.test.js +770 -0
  13. package/dist/contracts/__tests__/contract.decorator.test.d.ts +2 -0
  14. package/dist/contracts/__tests__/contract.decorator.test.d.ts.map +1 -0
  15. package/dist/contracts/__tests__/contract.decorator.test.js +177 -0
  16. package/dist/contracts/contract-registry.d.ts +57 -0
  17. package/dist/contracts/contract-registry.d.ts.map +1 -0
  18. package/dist/contracts/contract-registry.js +285 -0
  19. package/dist/contracts/contract.decorator.d.ts +70 -0
  20. package/dist/contracts/contract.decorator.d.ts.map +1 -0
  21. package/dist/contracts/contract.decorator.js +55 -0
  22. package/dist/contracts/contract.types.d.ts +65 -0
  23. package/dist/contracts/contract.types.d.ts.map +1 -0
  24. package/dist/contracts/contract.types.js +5 -0
  25. package/dist/contracts/index.d.ts +9 -0
  26. package/dist/contracts/index.d.ts.map +1 -0
  27. package/dist/contracts/index.js +16 -0
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +14 -2
  31. package/dist/testing/schema-faker.test.js +33 -0
  32. package/dist/transformers/transformer.service.test.js +40 -0
  33. package/package.json +2 -2
@@ -0,0 +1,770 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const contract_registry_1 = require("../contract-registry");
4
+ describe('ContractRegistry', () => {
5
+ let registry;
6
+ beforeEach(() => {
7
+ registry = new contract_registry_1.ContractRegistry();
8
+ });
9
+ describe('register', () => {
10
+ it('should register a contract', () => {
11
+ const contract = {
12
+ name: 'users',
13
+ version: '1.0.0',
14
+ owner: 'data-team',
15
+ schema: { type: 'object', properties: {} },
16
+ status: 'active',
17
+ createdAt: new Date(),
18
+ updatedAt: new Date(),
19
+ };
20
+ registry.register(contract);
21
+ const retrieved = registry.get('users', '1.0.0');
22
+ expect(retrieved).toEqual(contract);
23
+ });
24
+ it('should register multiple versions of same contract', () => {
25
+ const v1 = {
26
+ name: 'products',
27
+ version: '1.0.0',
28
+ owner: 'team-a',
29
+ schema: { type: 'object', properties: {} },
30
+ status: 'active',
31
+ createdAt: new Date(),
32
+ updatedAt: new Date(),
33
+ };
34
+ const v2 = {
35
+ name: 'products',
36
+ version: '2.0.0',
37
+ owner: 'team-a',
38
+ schema: { type: 'object', properties: {} },
39
+ status: 'active',
40
+ createdAt: new Date(),
41
+ updatedAt: new Date(),
42
+ };
43
+ registry.register(v1);
44
+ registry.register(v2);
45
+ expect(registry.get('products', '1.0.0')).toEqual(v1);
46
+ expect(registry.get('products', '2.0.0')).toEqual(v2);
47
+ });
48
+ });
49
+ describe('get', () => {
50
+ it('should return undefined for non-existent contract', () => {
51
+ expect(registry.get('nonexistent')).toBeUndefined();
52
+ });
53
+ it('should return undefined for non-existent version', () => {
54
+ const v1 = {
55
+ name: 'test',
56
+ version: '1.0.0',
57
+ owner: 'team',
58
+ schema: { type: 'object', properties: {} },
59
+ status: 'active',
60
+ createdAt: new Date(),
61
+ updatedAt: new Date(),
62
+ };
63
+ registry.register(v1);
64
+ expect(registry.get('test', '2.0.0')).toBeUndefined();
65
+ });
66
+ it('should return latest version when no version specified', () => {
67
+ const v1 = {
68
+ name: 'orders',
69
+ version: '1.0.0',
70
+ owner: 'team-b',
71
+ schema: { type: 'object', properties: {} },
72
+ status: 'active',
73
+ createdAt: new Date(),
74
+ updatedAt: new Date(),
75
+ };
76
+ const v2 = {
77
+ name: 'orders',
78
+ version: '2.1.0',
79
+ owner: 'team-b',
80
+ schema: { type: 'object', properties: {} },
81
+ status: 'active',
82
+ createdAt: new Date(),
83
+ updatedAt: new Date(),
84
+ };
85
+ registry.register(v1);
86
+ registry.register(v2);
87
+ expect(registry.get('orders')).toEqual(v2);
88
+ });
89
+ it('should return specific version when requested', () => {
90
+ const v1 = {
91
+ name: 'events',
92
+ version: '1.0.0',
93
+ owner: 'team-c',
94
+ schema: { type: 'object', properties: {} },
95
+ status: 'active',
96
+ createdAt: new Date(),
97
+ updatedAt: new Date(),
98
+ };
99
+ registry.register(v1);
100
+ expect(registry.get('events', '1.0.0')).toEqual(v1);
101
+ });
102
+ it('should handle contract with no versions map', () => {
103
+ expect(registry.get('empty')).toBeUndefined();
104
+ expect(registry.get('empty', '1.0.0')).toBeUndefined();
105
+ });
106
+ });
107
+ describe('listVersions', () => {
108
+ it('should return empty array for non-existent contract', () => {
109
+ expect(registry.listVersions('nonexistent')).toEqual([]);
110
+ });
111
+ it('should list all versions sorted descending', () => {
112
+ const contracts = [{ version: '1.0.0' }, { version: '2.0.0' }, { version: '1.5.0' }].map((v) => ({
113
+ name: 'analytics',
114
+ version: v.version,
115
+ owner: 'team-d',
116
+ schema: { type: 'object', properties: {} },
117
+ status: 'active',
118
+ createdAt: new Date(),
119
+ updatedAt: new Date(),
120
+ }));
121
+ contracts.forEach((c) => registry.register(c));
122
+ const versions = registry.listVersions('analytics');
123
+ expect(versions).toEqual(['2.0.0', '1.5.0', '1.0.0']);
124
+ });
125
+ it('should handle single version', () => {
126
+ const contract = {
127
+ name: 'single-version',
128
+ version: '1.0.0',
129
+ owner: 'team',
130
+ schema: { type: 'object', properties: {} },
131
+ status: 'active',
132
+ createdAt: new Date(),
133
+ updatedAt: new Date(),
134
+ };
135
+ registry.register(contract);
136
+ const versions = registry.listVersions('single-version');
137
+ expect(versions).toEqual(['1.0.0']);
138
+ });
139
+ });
140
+ describe('listContracts', () => {
141
+ it('should return empty array when no contracts registered', () => {
142
+ expect(registry.listContracts()).toEqual([]);
143
+ });
144
+ it('should list all contracts with their versions', () => {
145
+ const users = {
146
+ name: 'users',
147
+ version: '1.0.0',
148
+ owner: 'auth-team',
149
+ schema: { type: 'object', properties: {} },
150
+ status: 'active',
151
+ createdAt: new Date(),
152
+ updatedAt: new Date(),
153
+ };
154
+ const products = {
155
+ name: 'products',
156
+ version: '2.0.0',
157
+ owner: 'catalog-team',
158
+ schema: { type: 'object', properties: {} },
159
+ status: 'active',
160
+ createdAt: new Date(),
161
+ updatedAt: new Date(),
162
+ };
163
+ registry.register(users);
164
+ registry.register(products);
165
+ const contracts = registry.listContracts();
166
+ expect(contracts).toHaveLength(2);
167
+ expect(contracts.find((c) => c.name === 'users')).toEqual({
168
+ name: 'users',
169
+ versions: ['1.0.0'],
170
+ owner: 'auth-team',
171
+ });
172
+ expect(contracts.find((c) => c.name === 'products')).toEqual({
173
+ name: 'products',
174
+ versions: ['2.0.0'],
175
+ owner: 'catalog-team',
176
+ });
177
+ });
178
+ });
179
+ describe('validate', () => {
180
+ it('should validate data against contract schema', () => {
181
+ const contract = {
182
+ name: 'user-schema',
183
+ version: '1.0.0',
184
+ owner: 'platform',
185
+ schema: {
186
+ type: 'object',
187
+ properties: {
188
+ id: { type: 'string' },
189
+ email: { type: 'string' },
190
+ },
191
+ required: ['id', 'email'],
192
+ },
193
+ status: 'active',
194
+ createdAt: new Date(),
195
+ updatedAt: new Date(),
196
+ };
197
+ registry.register(contract);
198
+ const validData = { id: '123', email: 'test@example.com' };
199
+ const result = registry.validate('user-schema', validData, '1.0.0');
200
+ expect(result.valid).toBe(true);
201
+ expect(result.violations).toEqual([]);
202
+ });
203
+ it('should validate data structure', () => {
204
+ const contract = {
205
+ name: 'strict-schema',
206
+ version: '1.0.0',
207
+ owner: 'platform',
208
+ schema: {
209
+ type: 'object',
210
+ properties: {
211
+ id: { type: 'string' },
212
+ },
213
+ required: ['id'],
214
+ },
215
+ status: 'active',
216
+ createdAt: new Date(),
217
+ updatedAt: new Date(),
218
+ };
219
+ registry.register(contract);
220
+ const data = { name: 'test' };
221
+ const result = registry.validate('strict-schema', data, '1.0.0');
222
+ expect(result).toBeDefined();
223
+ expect(result.violations).toBeDefined();
224
+ });
225
+ it('should return error for non-existent contract', () => {
226
+ const result = registry.validate('nonexistent', {}, '1.0.0');
227
+ expect(result.valid).toBe(false);
228
+ expect(result.violations).toHaveLength(1);
229
+ expect(result.violations[0].message).toContain('Contract not found');
230
+ });
231
+ it('should validate SLA completeness requirements', () => {
232
+ const contract = {
233
+ name: 'sla-test',
234
+ version: '1.0.0',
235
+ owner: 'platform',
236
+ schema: {
237
+ type: 'object',
238
+ properties: {
239
+ id: { type: 'string' },
240
+ name: { type: 'string' },
241
+ email: { type: 'string' },
242
+ },
243
+ },
244
+ sla: {
245
+ completeness: {
246
+ minCompleteness: 0.9,
247
+ requiredFields: ['id', 'name', 'email'],
248
+ },
249
+ },
250
+ status: 'active',
251
+ createdAt: new Date(),
252
+ updatedAt: new Date(),
253
+ };
254
+ registry.register(contract);
255
+ const completeData = { id: '1', name: 'Test', email: 'test@example.com' };
256
+ const result = registry.validate('sla-test', completeData, '1.0.0');
257
+ expect(result).toBeDefined();
258
+ expect(result.violations).toBeDefined();
259
+ });
260
+ it('should validate without SLA when not configured', () => {
261
+ const contract = {
262
+ name: 'no-sla',
263
+ version: '1.0.0',
264
+ owner: 'platform',
265
+ schema: {
266
+ type: 'object',
267
+ properties: {
268
+ id: { type: 'string' },
269
+ },
270
+ },
271
+ status: 'active',
272
+ createdAt: new Date(),
273
+ updatedAt: new Date(),
274
+ };
275
+ registry.register(contract);
276
+ const data = { id: '1' };
277
+ const result = registry.validate('no-sla', data, '1.0.0');
278
+ expect(result.valid).toBe(true);
279
+ });
280
+ it('should reject non-object data', () => {
281
+ const contract = {
282
+ name: 'object-only',
283
+ version: '1.0.0',
284
+ owner: 'platform',
285
+ schema: {
286
+ type: 'object',
287
+ properties: {
288
+ id: { type: 'string' },
289
+ },
290
+ },
291
+ status: 'active',
292
+ createdAt: new Date(),
293
+ updatedAt: new Date(),
294
+ };
295
+ registry.register(contract);
296
+ const result = registry.validate('object-only', 'not an object', '1.0.0');
297
+ expect(result.valid).toBe(false);
298
+ expect(result.violations.length).toBeGreaterThan(0);
299
+ expect(result.violations[0].message).toContain('must be an object');
300
+ });
301
+ it('should reject null data', () => {
302
+ const contract = {
303
+ name: 'null-test',
304
+ version: '1.0.0',
305
+ owner: 'platform',
306
+ schema: {
307
+ type: 'object',
308
+ properties: {},
309
+ },
310
+ status: 'active',
311
+ createdAt: new Date(),
312
+ updatedAt: new Date(),
313
+ };
314
+ registry.register(contract);
315
+ const result = registry.validate('null-test', null, '1.0.0');
316
+ expect(result.valid).toBe(false);
317
+ expect(result.violations[0].message).toContain('must be an object');
318
+ });
319
+ it('should validate required fields in schema', () => {
320
+ const contract = {
321
+ name: 'required-fields-test',
322
+ version: '1.0.0',
323
+ owner: 'platform',
324
+ schema: {
325
+ id: { type: 'string', required: true },
326
+ name: { type: 'string', required: true },
327
+ optional: { type: 'string' },
328
+ },
329
+ status: 'active',
330
+ createdAt: new Date(),
331
+ updatedAt: new Date(),
332
+ };
333
+ registry.register(contract);
334
+ const invalidData = { id: '123' }; // missing required 'name'
335
+ const result = registry.validate('required-fields-test', invalidData, '1.0.0');
336
+ expect(result.valid).toBe(false);
337
+ expect(result.violations.some((v) => v.message.includes('Required field missing'))).toBe(true);
338
+ });
339
+ it('should pass validation when all required fields present', () => {
340
+ const contract = {
341
+ name: 'complete-data-test',
342
+ version: '1.0.0',
343
+ owner: 'platform',
344
+ schema: {
345
+ id: { type: 'string', required: true },
346
+ name: { type: 'string', required: true },
347
+ },
348
+ status: 'active',
349
+ createdAt: new Date(),
350
+ updatedAt: new Date(),
351
+ };
352
+ registry.register(contract);
353
+ const validData = { id: '123', name: 'Test' };
354
+ const result = registry.validate('complete-data-test', validData, '1.0.0');
355
+ expect(result.valid).toBe(true);
356
+ });
357
+ it('should validate completeness SLA with missing fields', () => {
358
+ const contract = {
359
+ name: 'completeness-sla-test',
360
+ version: '1.0.0',
361
+ owner: 'platform',
362
+ schema: {
363
+ type: 'object',
364
+ properties: {
365
+ id: { type: 'string' },
366
+ name: { type: 'string' },
367
+ email: { type: 'string' },
368
+ },
369
+ },
370
+ sla: {
371
+ completeness: {
372
+ minCompleteness: 0.8,
373
+ requiredFields: ['id', 'name', 'email'],
374
+ },
375
+ },
376
+ status: 'active',
377
+ createdAt: new Date(),
378
+ updatedAt: new Date(),
379
+ };
380
+ registry.register(contract);
381
+ const incompleteData = { id: '123' }; // missing name and email
382
+ const result = registry.validate('completeness-sla-test', incompleteData, '1.0.0');
383
+ expect(result.violations.length).toBeGreaterThan(0);
384
+ });
385
+ it('should handle null values in completeness check', () => {
386
+ const contract = {
387
+ name: 'null-completeness-test',
388
+ version: '1.0.0',
389
+ owner: 'platform',
390
+ schema: {
391
+ type: 'object',
392
+ properties: {
393
+ id: { type: 'string' },
394
+ name: { type: 'string' },
395
+ },
396
+ },
397
+ sla: {
398
+ completeness: {
399
+ minCompleteness: 0.9,
400
+ requiredFields: ['id', 'name'],
401
+ },
402
+ },
403
+ status: 'active',
404
+ createdAt: new Date(),
405
+ updatedAt: new Date(),
406
+ };
407
+ registry.register(contract);
408
+ const dataWithNull = { id: '123', name: null };
409
+ const result = registry.validate('null-completeness-test', dataWithNull, '1.0.0');
410
+ expect(result.violations.length).toBeGreaterThan(0);
411
+ });
412
+ });
413
+ describe('deprecate', () => {
414
+ it('should deprecate a contract version', () => {
415
+ const contract = {
416
+ name: 'old-contract',
417
+ version: '1.0.0',
418
+ owner: 'team',
419
+ schema: { type: 'object', properties: {} },
420
+ status: 'active',
421
+ createdAt: new Date(),
422
+ updatedAt: new Date(),
423
+ };
424
+ registry.register(contract);
425
+ registry.deprecate('old-contract', '1.0.0');
426
+ const retrieved = registry.get('old-contract', '1.0.0');
427
+ expect(retrieved?.status).toBe('deprecated');
428
+ });
429
+ it('should throw error when deprecating non-existent contract', () => {
430
+ expect(() => {
431
+ registry.deprecate('nonexistent', '1.0.0');
432
+ }).toThrow('Contract not found');
433
+ });
434
+ it('should update updatedAt timestamp when deprecating', () => {
435
+ const contract = {
436
+ name: 'test-deprecate',
437
+ version: '1.0.0',
438
+ owner: 'team',
439
+ schema: { type: 'object', properties: {} },
440
+ status: 'active',
441
+ createdAt: new Date(),
442
+ updatedAt: new Date(2020, 0, 1),
443
+ };
444
+ registry.register(contract);
445
+ const beforeDeprecate = registry.get('test-deprecate', '1.0.0')?.updatedAt;
446
+ registry.deprecate('test-deprecate', '1.0.0');
447
+ const afterDeprecate = registry.get('test-deprecate', '1.0.0')?.updatedAt;
448
+ expect(afterDeprecate).not.toEqual(beforeDeprecate);
449
+ });
450
+ });
451
+ describe('diff', () => {
452
+ it('should detect schema changes between versions', () => {
453
+ const v1 = {
454
+ name: 'evolving',
455
+ version: '1.0.0',
456
+ owner: 'team',
457
+ schema: {
458
+ type: 'object',
459
+ properties: {
460
+ id: { type: 'string' },
461
+ },
462
+ },
463
+ status: 'active',
464
+ createdAt: new Date(),
465
+ updatedAt: new Date(),
466
+ };
467
+ const v2 = {
468
+ name: 'evolving',
469
+ version: '2.0.0',
470
+ owner: 'team',
471
+ schema: {
472
+ type: 'object',
473
+ properties: {
474
+ id: { type: 'string' },
475
+ name: { type: 'string' },
476
+ },
477
+ },
478
+ status: 'active',
479
+ createdAt: new Date(),
480
+ updatedAt: new Date(),
481
+ };
482
+ registry.register(v1);
483
+ registry.register(v2);
484
+ const diff = registry.diff('evolving', '1.0.0', '2.0.0');
485
+ expect(diff).toBeDefined();
486
+ expect(diff.changes.length).toBeGreaterThan(0);
487
+ });
488
+ it('should throw error when comparing non-existent versions', () => {
489
+ expect(() => {
490
+ registry.diff('nonexistent', '1.0.0', '2.0.0');
491
+ }).toThrow('Cannot compare: contract versions not found');
492
+ });
493
+ it('should identify breaking changes', () => {
494
+ const v1 = {
495
+ name: 'breaking-test',
496
+ version: '1.0.0',
497
+ owner: 'team',
498
+ schema: {
499
+ type: 'object',
500
+ properties: {
501
+ id: { type: 'string' },
502
+ name: { type: 'string' },
503
+ },
504
+ },
505
+ status: 'active',
506
+ createdAt: new Date(),
507
+ updatedAt: new Date(),
508
+ };
509
+ const v2 = {
510
+ name: 'breaking-test',
511
+ version: '2.0.0',
512
+ owner: 'team',
513
+ schema: {
514
+ type: 'object',
515
+ properties: {
516
+ id: { type: 'number' }, // Type change - breaking
517
+ },
518
+ },
519
+ status: 'active',
520
+ createdAt: new Date(),
521
+ updatedAt: new Date(),
522
+ };
523
+ registry.register(v1);
524
+ registry.register(v2);
525
+ const diff = registry.diff('breaking-test', '1.0.0', '2.0.0');
526
+ expect(diff.isBreaking).toBeDefined();
527
+ expect(diff.breakingChanges).toBeDefined();
528
+ });
529
+ it('should detect field additions as non-breaking', () => {
530
+ const v1 = {
531
+ name: 'add-field-test',
532
+ version: '1.0.0',
533
+ owner: 'team',
534
+ schema: {
535
+ id: 'string',
536
+ },
537
+ status: 'active',
538
+ createdAt: new Date(),
539
+ updatedAt: new Date(),
540
+ };
541
+ const v2 = {
542
+ name: 'add-field-test',
543
+ version: '2.0.0',
544
+ owner: 'team',
545
+ schema: {
546
+ id: 'string',
547
+ name: 'string', // Field added
548
+ },
549
+ status: 'active',
550
+ createdAt: new Date(),
551
+ updatedAt: new Date(),
552
+ };
553
+ registry.register(v1);
554
+ registry.register(v2);
555
+ const diff = registry.diff('add-field-test', '1.0.0', '2.0.0');
556
+ const addedChanges = diff.changes.filter((c) => c.changeType === 'added');
557
+ expect(addedChanges.length).toBeGreaterThan(0);
558
+ expect(addedChanges[0].breaking).toBe(false);
559
+ });
560
+ it('should detect field removals as breaking', () => {
561
+ const v1 = {
562
+ name: 'remove-field-test',
563
+ version: '1.0.0',
564
+ owner: 'team',
565
+ schema: {
566
+ id: 'string',
567
+ name: 'string',
568
+ },
569
+ status: 'active',
570
+ createdAt: new Date(),
571
+ updatedAt: new Date(),
572
+ };
573
+ const v2 = {
574
+ name: 'remove-field-test',
575
+ version: '2.0.0',
576
+ owner: 'team',
577
+ schema: {
578
+ id: 'string',
579
+ // name removed
580
+ },
581
+ status: 'active',
582
+ createdAt: new Date(),
583
+ updatedAt: new Date(),
584
+ };
585
+ registry.register(v1);
586
+ registry.register(v2);
587
+ const diff = registry.diff('remove-field-test', '1.0.0', '2.0.0');
588
+ const removedChanges = diff.changes.filter((c) => c.changeType === 'removed');
589
+ expect(removedChanges.length).toBeGreaterThan(0);
590
+ expect(removedChanges[0].breaking).toBe(true);
591
+ });
592
+ it('should detect field modifications as non-breaking', () => {
593
+ const v1 = {
594
+ name: 'modify-field-test',
595
+ version: '1.0.0',
596
+ owner: 'team',
597
+ schema: {
598
+ config: { maxLength: 100 },
599
+ },
600
+ status: 'active',
601
+ createdAt: new Date(),
602
+ updatedAt: new Date(),
603
+ };
604
+ const v2 = {
605
+ name: 'modify-field-test',
606
+ version: '2.0.0',
607
+ owner: 'team',
608
+ schema: {
609
+ config: { maxLength: 200 }, // Value modified
610
+ },
611
+ status: 'active',
612
+ createdAt: new Date(),
613
+ updatedAt: new Date(),
614
+ };
615
+ registry.register(v1);
616
+ registry.register(v2);
617
+ const diff = registry.diff('modify-field-test', '1.0.0', '2.0.0');
618
+ const modifiedChanges = diff.changes.filter((c) => c.changeType === 'modified');
619
+ expect(modifiedChanges.length).toBeGreaterThan(0);
620
+ expect(modifiedChanges[0].breaking).toBe(false);
621
+ });
622
+ it('should detect type changes as breaking', () => {
623
+ const v1 = {
624
+ name: 'type-change-test',
625
+ version: '1.0.0',
626
+ owner: 'team',
627
+ schema: {
628
+ count: { type: 'number' },
629
+ },
630
+ status: 'active',
631
+ createdAt: new Date(),
632
+ updatedAt: new Date(),
633
+ };
634
+ const v2 = {
635
+ name: 'type-change-test',
636
+ version: '2.0.0',
637
+ owner: 'team',
638
+ schema: {
639
+ count: 'string', // Type changed from object to string
640
+ },
641
+ status: 'active',
642
+ createdAt: new Date(),
643
+ updatedAt: new Date(),
644
+ };
645
+ registry.register(v1);
646
+ registry.register(v2);
647
+ const diff = registry.diff('type-change-test', '1.0.0', '2.0.0');
648
+ const typeChanges = diff.changes.filter((c) => c.changeType === 'type_changed');
649
+ expect(typeChanges.length).toBeGreaterThan(0);
650
+ expect(typeChanges[0].breaking).toBe(true);
651
+ });
652
+ });
653
+ describe('recordViolation', () => {
654
+ it('should record contract violations', () => {
655
+ registry.recordViolation({
656
+ contractName: 'test-contract',
657
+ contractVersion: '1.0.0',
658
+ violationType: 'schema',
659
+ severity: 'error',
660
+ message: 'Type mismatch on email field',
661
+ details: { field: 'email', expectedType: 'string', actualType: 'number' },
662
+ timestamp: new Date(),
663
+ });
664
+ const violations = registry.getViolations('test-contract');
665
+ expect(violations).toHaveLength(1);
666
+ expect(violations[0].message).toBe('Type mismatch on email field');
667
+ expect(violations[0].details.field).toBe('email');
668
+ });
669
+ });
670
+ describe('getViolations', () => {
671
+ it('should return empty array when no violations', () => {
672
+ expect(registry.getViolations('clean-contract')).toEqual([]);
673
+ });
674
+ it('should filter violations by contract name', () => {
675
+ registry.recordViolation({
676
+ contractName: 'contract-a',
677
+ contractVersion: '1.0.0',
678
+ violationType: 'schema',
679
+ severity: 'error',
680
+ message: 'Error A',
681
+ details: { field: 'field1' },
682
+ timestamp: new Date(),
683
+ });
684
+ registry.recordViolation({
685
+ contractName: 'contract-b',
686
+ contractVersion: '1.0.0',
687
+ violationType: 'sla',
688
+ severity: 'warning',
689
+ message: 'Error B',
690
+ details: { field: 'field2' },
691
+ timestamp: new Date(),
692
+ });
693
+ const violationsA = registry.getViolations('contract-a');
694
+ expect(violationsA).toHaveLength(1);
695
+ expect(violationsA[0].message).toBe('Error A');
696
+ });
697
+ it('should filter violations by version', () => {
698
+ registry.recordViolation({
699
+ contractName: 'versioned-contract',
700
+ contractVersion: '1.0.0',
701
+ violationType: 'schema',
702
+ severity: 'error',
703
+ message: 'V1 error',
704
+ details: {},
705
+ timestamp: new Date(),
706
+ });
707
+ registry.recordViolation({
708
+ contractName: 'versioned-contract',
709
+ contractVersion: '2.0.0',
710
+ violationType: 'schema',
711
+ severity: 'error',
712
+ message: 'V2 error',
713
+ details: {},
714
+ timestamp: new Date(),
715
+ });
716
+ const v1Violations = registry.getViolations('versioned-contract', '1.0.0');
717
+ expect(v1Violations).toHaveLength(1);
718
+ expect(v1Violations[0].message).toBe('V1 error');
719
+ });
720
+ });
721
+ describe('clearOldViolations', () => {
722
+ it('should clear violations older than specified days', () => {
723
+ const oldDate = new Date();
724
+ oldDate.setDate(oldDate.getDate() - 10);
725
+ const recentDate = new Date();
726
+ recentDate.setDate(recentDate.getDate() - 2);
727
+ registry.recordViolation({
728
+ contractName: 'test',
729
+ contractVersion: '1.0.0',
730
+ violationType: 'schema',
731
+ severity: 'error',
732
+ message: 'Old violation',
733
+ details: {},
734
+ timestamp: oldDate,
735
+ });
736
+ registry.recordViolation({
737
+ contractName: 'test',
738
+ contractVersion: '1.0.0',
739
+ violationType: 'schema',
740
+ severity: 'error',
741
+ message: 'Recent violation',
742
+ details: {},
743
+ timestamp: recentDate,
744
+ });
745
+ registry.clearOldViolations(5);
746
+ const violations = registry.getViolations('test');
747
+ expect(violations).toHaveLength(1);
748
+ expect(violations[0].message).toBe('Recent violation');
749
+ });
750
+ it('should keep all violations when cutoff is 0', () => {
751
+ registry.recordViolation({
752
+ contractName: 'test',
753
+ contractVersion: '1.0.0',
754
+ violationType: 'schema',
755
+ severity: 'error',
756
+ message: 'Violation',
757
+ details: {},
758
+ timestamp: new Date(),
759
+ });
760
+ registry.clearOldViolations(0);
761
+ const violations = registry.getViolations('test');
762
+ expect(violations).toHaveLength(0);
763
+ });
764
+ it('should handle clearing when no violations exist', () => {
765
+ expect(() => {
766
+ registry.clearOldViolations(30);
767
+ }).not.toThrow();
768
+ });
769
+ });
770
+ });