@futdevpro/nts-dynamo 1.15.24 → 1.15.29
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/_specifications/BACKLOG.md +28 -0
- package/build/_models/interfaces/compare-data-options.interface.d.ts +27 -0
- package/build/_models/interfaces/compare-data-options.interface.d.ts.map +1 -0
- package/build/_models/interfaces/compare-data-options.interface.js +3 -0
- package/build/_models/interfaces/compare-data-options.interface.js.map +1 -0
- package/build/_models/interfaces/compare-data-result.interface.d.ts +13 -0
- package/build/_models/interfaces/compare-data-result.interface.d.ts.map +1 -0
- package/build/_models/interfaces/compare-data-result.interface.js +3 -0
- package/build/_models/interfaces/compare-data-result.interface.js.map +1 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.d.ts +14 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.d.ts.map +1 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.js +3 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.js.map +1 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.d.ts +50 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.d.ts.map +1 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.js +3 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.js.map +1 -0
- package/build/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.d.ts.map +1 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.js +32 -0
- package/build/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.js.map +1 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.d.ts.map +1 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.js +20 -2
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.js.map +1 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.d.ts +4 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.d.ts.map +1 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.js +28 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.js.map +1 -1
- package/build/_modules/ai/_services/ai-provider.service-base.d.ts +21 -0
- package/build/_modules/ai/_services/ai-provider.service-base.d.ts.map +1 -1
- package/build/_modules/ai/_services/ai-provider.service-base.js +32 -0
- package/build/_modules/ai/_services/ai-provider.service-base.js.map +1 -1
- package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.d.ts +17 -1
- package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.d.ts.map +1 -1
- package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.js +16 -0
- package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.js.map +1 -1
- package/build/_modules/local-vector-search/_services/lvs-bm25.util.d.ts +89 -0
- package/build/_modules/local-vector-search/_services/lvs-bm25.util.d.ts.map +1 -0
- package/build/_modules/local-vector-search/_services/lvs-bm25.util.js +190 -0
- package/build/_modules/local-vector-search/_services/lvs-bm25.util.js.map +1 -0
- package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.d.ts +18 -2
- package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.d.ts.map +1 -1
- package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.js +57 -3
- package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.js.map +1 -1
- package/build/_services/base/data.service.d.ts +63 -0
- package/build/_services/base/data.service.d.ts.map +1 -1
- package/build/_services/base/data.service.js +189 -0
- package/build/_services/base/data.service.js.map +1 -1
- package/package.json +1 -1
- package/src/_models/interfaces/compare-data-options.interface.ts +27 -0
- package/src/_models/interfaces/compare-data-result.interface.ts +12 -0
- package/src/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.ts +14 -0
- package/src/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.ts +56 -0
- package/src/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.spec.ts +92 -0
- package/src/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.ts +38 -4
- package/src/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.ts +24 -5
- package/src/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.spec.ts +52 -0
- package/src/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.ts +39 -10
- package/src/_modules/ai/_services/ai-provider.service-base.spec.ts +79 -0
- package/src/_modules/ai/_services/ai-provider.service-base.ts +41 -3
- package/src/_modules/local-vector-search/_enums/lvs-search-mode.enum.ts +16 -0
- package/src/_modules/local-vector-search/_services/lvs-bm25.util.spec.ts +159 -0
- package/src/_modules/local-vector-search/_services/lvs-bm25.util.ts +206 -0
- package/src/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.spec.ts +135 -0
- package/src/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.ts +95 -9
- package/src/_services/base/data.service.spec.ts +181 -0
- package/src/_services/base/data.service.ts +196 -2
|
@@ -489,5 +489,186 @@ describe('| DyNTS_DataService', () => {
|
|
|
489
489
|
}).toThrow();
|
|
490
490
|
});
|
|
491
491
|
});
|
|
492
|
+
|
|
493
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
494
|
+
// FR-001 — Generic compareData()
|
|
495
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
496
|
+
|
|
497
|
+
describe('| compareData() — FR-001', (): void => {
|
|
498
|
+
let svc: DyNTS_DataService<TestMetadata>;
|
|
499
|
+
|
|
500
|
+
beforeEach((): void => {
|
|
501
|
+
// Re-use existing TestMetadata + testDataParams.
|
|
502
|
+
const t: TestMetadata = new TestMetadata();
|
|
503
|
+
svc = new DyNTS_DataService<TestMetadata>(t, testDataParams, 'test-issuer');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('| equal POJO returns { result: "equal" } — sin changedFields', (): void => {
|
|
507
|
+
const a: TestMetadata = Object.assign(new TestMetadata(), { name: 'alice', email: 'a@x' });
|
|
508
|
+
const b: TestMetadata = Object.assign(new TestMetadata(), { name: 'alice', email: 'a@x' });
|
|
509
|
+
const r = svc.compareData(a, b);
|
|
510
|
+
expect(r.result).toBe('equal');
|
|
511
|
+
expect(r.changedFields).toBeUndefined();
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('| modified single field — changedFields tartalmazza azt', (): void => {
|
|
515
|
+
const a: TestMetadata = Object.assign(new TestMetadata(), { name: 'alice', email: 'a@x' });
|
|
516
|
+
const b: TestMetadata = Object.assign(new TestMetadata(), { name: 'bob', email: 'a@x' });
|
|
517
|
+
const r = svc.compareData(a, b);
|
|
518
|
+
expect(r.result).toBe('modified');
|
|
519
|
+
expect(r.changedFields).toEqual(['name']);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('| modified multi-field — changedFields TELJES listat ad (nem early-return)', (): void => {
|
|
523
|
+
const a: TestMetadata = Object.assign(new TestMetadata(), { name: 'alice', email: 'a@x', userId: 'u1' });
|
|
524
|
+
const b: TestMetadata = Object.assign(new TestMetadata(), { name: 'bob', email: 'b@y', userId: 'u2' });
|
|
525
|
+
const r = svc.compareData(a, b);
|
|
526
|
+
expect(r.result).toBe('modified');
|
|
527
|
+
expect(r.changedFields?.sort()).toEqual(['email', 'name', 'userId'] as any);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it('| auto-discovery SKIP-eli a metadata fields-et (_id, __created, __lastModified)', (): void => {
|
|
531
|
+
const now: Date = new Date();
|
|
532
|
+
const a: TestMetadata = Object.assign(new TestMetadata(), { name: 'alice', _id: 'X', __created: now, __lastModified: now });
|
|
533
|
+
const b: TestMetadata = Object.assign(new TestMetadata(), { name: 'alice', _id: 'Y', __created: new Date(2000, 0, 1), __lastModified: new Date(2001, 0, 1) });
|
|
534
|
+
const r = svc.compareData(a, b);
|
|
535
|
+
expect(r.result).toBe('equal');
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('| explicit fields override — KIFEJEZETT _id-val VIZSGALJA a skip-listet is', (): void => {
|
|
539
|
+
const a: TestMetadata = Object.assign(new TestMetadata(), { name: 'alice', _id: 'X' });
|
|
540
|
+
const b: TestMetadata = Object.assign(new TestMetadata(), { name: 'alice', _id: 'Y' });
|
|
541
|
+
const r = svc.compareData(a, b, { fields: ['_id' as keyof TestMetadata] });
|
|
542
|
+
expect(r.result).toBe('modified');
|
|
543
|
+
expect(r.changedFields).toEqual(['_id' as keyof TestMetadata]);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('| explicit fields scope-szukit — csak a listazott fields szamit', (): void => {
|
|
547
|
+
const a: TestMetadata = Object.assign(new TestMetadata(), { name: 'alice', email: 'a@x' });
|
|
548
|
+
const b: TestMetadata = Object.assign(new TestMetadata(), { name: 'alice', email: 'b@y' });
|
|
549
|
+
const r = svc.compareData(a, b, { fields: ['name'] });
|
|
550
|
+
expect(r.result).toBe('equal'); // email change ignored, only 'name' checked
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('| customComparator override (case-insensitive string)', (): void => {
|
|
554
|
+
const a: TestMetadata = Object.assign(new TestMetadata(), { name: 'Alice' });
|
|
555
|
+
const b: TestMetadata = Object.assign(new TestMetadata(), { name: 'alice' });
|
|
556
|
+
const r = svc.compareData(a, b, {
|
|
557
|
+
customComparators: { name: (x: string, y: string) => x.toLowerCase() === y.toLowerCase() },
|
|
558
|
+
});
|
|
559
|
+
expect(r.result).toBe('equal');
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('| customComparator — array-set-equality eltero sorrendre', (): void => {
|
|
563
|
+
class WithArr extends DyFM_Metadata {
|
|
564
|
+
name?: string;
|
|
565
|
+
tags?: string[];
|
|
566
|
+
}
|
|
567
|
+
const arrSvc = new DyNTS_DataService<WithArr>(
|
|
568
|
+
new WithArr(),
|
|
569
|
+
new DyFM_DataModel_Params<WithArr>({
|
|
570
|
+
dataName: 'with_arr',
|
|
571
|
+
properties: { name: { key: 'name', type: DyFM_BasicProperty_Type.string } },
|
|
572
|
+
}),
|
|
573
|
+
'test-issuer',
|
|
574
|
+
);
|
|
575
|
+
const a: WithArr = Object.assign(new WithArr(), { name: 'x', tags: ['a', 'b', 'c'] });
|
|
576
|
+
const b: WithArr = Object.assign(new WithArr(), { name: 'x', tags: ['c', 'b', 'a'] });
|
|
577
|
+
const r = arrSvc.compareData(a, b, {
|
|
578
|
+
customComparators: {
|
|
579
|
+
tags: (x: string[], y: string[]) => {
|
|
580
|
+
const s: Set<string> = new Set<string>(x);
|
|
581
|
+
return y.length === x.length && y.every((v: string) => s.has(v));
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
expect(r.result).toBe('equal');
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('| deep-equal: Date instances ugyanazon time-stamp-pel equal-nek szamitanak', (): void => {
|
|
589
|
+
class WithDate extends DyFM_Metadata {
|
|
590
|
+
when?: Date;
|
|
591
|
+
}
|
|
592
|
+
const dateSvc = new DyNTS_DataService<WithDate>(
|
|
593
|
+
new WithDate(),
|
|
594
|
+
new DyFM_DataModel_Params<WithDate>({
|
|
595
|
+
dataName: 'with_date',
|
|
596
|
+
properties: {},
|
|
597
|
+
}),
|
|
598
|
+
'test-issuer',
|
|
599
|
+
);
|
|
600
|
+
const t: number = 1_700_000_000_000;
|
|
601
|
+
const a: WithDate = Object.assign(new WithDate(), { when: new Date(t) });
|
|
602
|
+
const b: WithDate = Object.assign(new WithDate(), { when: new Date(t) });
|
|
603
|
+
expect(dateSvc.compareData(a, b).result).toBe('equal');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('| deep-equal: nested object', (): void => {
|
|
607
|
+
class WithNested extends DyFM_Metadata {
|
|
608
|
+
nested?: { a: number; b: { c: string } };
|
|
609
|
+
}
|
|
610
|
+
const nSvc = new DyNTS_DataService<WithNested>(
|
|
611
|
+
new WithNested(),
|
|
612
|
+
new DyFM_DataModel_Params<WithNested>({ dataName: 'with_nested', properties: {} }),
|
|
613
|
+
'test-issuer',
|
|
614
|
+
);
|
|
615
|
+
const a: WithNested = Object.assign(new WithNested(), { nested: { a: 1, b: { c: 'x' } } });
|
|
616
|
+
const b: WithNested = Object.assign(new WithNested(), { nested: { a: 1, b: { c: 'x' } } });
|
|
617
|
+
const c: WithNested = Object.assign(new WithNested(), { nested: { a: 1, b: { c: 'y' } } });
|
|
618
|
+
expect(nSvc.compareData(a, b).result).toBe('equal');
|
|
619
|
+
expect(nSvc.compareData(a, c).result).toBe('modified');
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('| deep-equal: array index-szerinti compare (eltero sorrend → modified default-ban)', (): void => {
|
|
623
|
+
class WithArr2 extends DyFM_Metadata {
|
|
624
|
+
items?: number[];
|
|
625
|
+
}
|
|
626
|
+
const aSvc = new DyNTS_DataService<WithArr2>(
|
|
627
|
+
new WithArr2(),
|
|
628
|
+
new DyFM_DataModel_Params<WithArr2>({ dataName: 'with_arr2', properties: {} }),
|
|
629
|
+
'test-issuer',
|
|
630
|
+
);
|
|
631
|
+
const a: WithArr2 = Object.assign(new WithArr2(), { items: [1, 2, 3] });
|
|
632
|
+
const b: WithArr2 = Object.assign(new WithArr2(), { items: [1, 2, 3] });
|
|
633
|
+
const c: WithArr2 = Object.assign(new WithArr2(), { items: [3, 2, 1] });
|
|
634
|
+
expect(aSvc.compareData(a, b).result).toBe('equal');
|
|
635
|
+
expect(aSvc.compareData(a, c).result).toBe('modified');
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('| throw 400 ha newData null', (): void => {
|
|
639
|
+
const b: TestMetadata = Object.assign(new TestMetadata(), { name: 'x' });
|
|
640
|
+
let thrown: any = null;
|
|
641
|
+
try { svc.compareData(null as any, b); } catch (e) { thrown = e; }
|
|
642
|
+
expect(thrown).not.toBeNull();
|
|
643
|
+
expect(DyFM_Error.getErrorStatus(thrown)).toBe(400);
|
|
644
|
+
expect(DyFM_Error.getErrorCode(thrown)).toContain('DyNTS-DS0-CD1');
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('| throw 400 ha oldData undefined', (): void => {
|
|
648
|
+
const a: TestMetadata = Object.assign(new TestMetadata(), { name: 'x' });
|
|
649
|
+
let thrown: any = null;
|
|
650
|
+
try { svc.compareData(a, undefined as any); } catch (e) { thrown = e; }
|
|
651
|
+
expect(thrown).not.toBeNull();
|
|
652
|
+
expect(DyFM_Error.getErrorStatus(thrown)).toBe(400);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('| throw 400 ha options.fields ures array', (): void => {
|
|
656
|
+
const a: TestMetadata = Object.assign(new TestMetadata(), { name: 'alice' });
|
|
657
|
+
const b: TestMetadata = Object.assign(new TestMetadata(), { name: 'alice' });
|
|
658
|
+
let thrown: any = null;
|
|
659
|
+
try { svc.compareData(a, b, { fields: [] }); } catch (e) { thrown = e; }
|
|
660
|
+
expect(thrown).not.toBeNull();
|
|
661
|
+
expect(DyFM_Error.getErrorStatus(thrown)).toBe(400);
|
|
662
|
+
expect(DyFM_Error.getErrorCode(thrown)).toContain('DyNTS-DS0-CD2');
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('| asymmetric keys: ha az egyik objektum-bol hianyzik a key, modified-nek szamit', (): void => {
|
|
666
|
+
const a: TestMetadata = Object.assign(new TestMetadata(), { name: 'alice', email: 'a@x' });
|
|
667
|
+
const b: TestMetadata = Object.assign(new TestMetadata(), { name: 'alice' }); // no email
|
|
668
|
+
const r = svc.compareData(a, b);
|
|
669
|
+
expect(r.result).toBe('modified');
|
|
670
|
+
expect(r.changedFields).toEqual(['email']);
|
|
671
|
+
});
|
|
672
|
+
});
|
|
492
673
|
});
|
|
493
674
|
|
|
@@ -29,11 +29,30 @@ import {
|
|
|
29
29
|
|
|
30
30
|
import { DyNTS_getArchivedDBName } from '../../_collections/archive.util';
|
|
31
31
|
import { DyNTS_global_settings } from '../../_collections/global-settings.const';
|
|
32
|
+
import { DyNTS_CompareData_Options } from '../../_models/interfaces/compare-data-options.interface';
|
|
33
|
+
import { DyNTS_CompareData_Result } from '../../_models/interfaces/compare-data-result.interface';
|
|
32
34
|
import { DyNTS_DBUpdate } from '../../_models/types/db-update.type';
|
|
33
35
|
import { DyNTS_GlobalService } from '../core/global.service';
|
|
34
36
|
import { DyNTS_ArchiveDataService } from './archive-data.service';
|
|
35
37
|
import { DyNTS_DBService } from './db.service';
|
|
36
38
|
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* `DyFM_Metadata` field-ek amik auto-discovery modban (fields opcio nelkul)
|
|
42
|
+
* KI vannak hagyva a compareData()-bol. Ezek normal mukodes kozben kezelt
|
|
43
|
+
* mezok (id-allocation, audit timestamps, audit usernames) — szemantikus
|
|
44
|
+
* adatvaltozas-ellenorzeshez irrelevansak.
|
|
45
|
+
*
|
|
46
|
+
* Explicit `fields: ['_id']`-vel meg vizsgaltathato, ha valamiert kell.
|
|
47
|
+
*/
|
|
48
|
+
const COMPARE_DATA_METADATA_SKIP_FIELDS: ReadonlySet<string> = new Set<string>([
|
|
49
|
+
'_id',
|
|
50
|
+
'__created',
|
|
51
|
+
'__createdBy',
|
|
52
|
+
'__lastModified',
|
|
53
|
+
'__lastModifiedBy',
|
|
54
|
+
]);
|
|
55
|
+
|
|
37
56
|
// TODO: 2 type of archiving service system: within list, or separate db elements
|
|
38
57
|
|
|
39
58
|
/**
|
|
@@ -2503,7 +2522,7 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
|
|
|
2503
2522
|
}
|
|
2504
2523
|
|
|
2505
2524
|
protected getDefaultErrorSettings(
|
|
2506
|
-
fnName: string,
|
|
2525
|
+
fnName: string,
|
|
2507
2526
|
error: DyFM_AnyError,
|
|
2508
2527
|
/** @deprecated we wont support the separate user message in the future, use the message instead */
|
|
2509
2528
|
useMessageAsUserMessage: boolean = true,
|
|
@@ -2514,7 +2533,7 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
|
|
|
2514
2533
|
(error as DyFM_Error)?._message ??
|
|
2515
2534
|
`${fnName} was UNSUCCESSFUL (${DyNTS_global_settings.systemShortCodeName})`,
|
|
2516
2535
|
addECToUserMsg: !(error as DyFM_Error)?.__userMessage,
|
|
2517
|
-
userMessage: (error as DyFM_Error)?.__userMessage ??
|
|
2536
|
+
userMessage: (error as DyFM_Error)?.__userMessage ??
|
|
2518
2537
|
(useMessageAsUserMessage ? (error as Error)?.message : this.defaultErrorUserMsg),
|
|
2519
2538
|
issuer: this.issuer,
|
|
2520
2539
|
issuerService: this.constructor?.name,
|
|
@@ -2522,4 +2541,179 @@ export class DyNTS_DataService<T extends DyFM_Metadata> {
|
|
|
2522
2541
|
error: error,
|
|
2523
2542
|
};
|
|
2524
2543
|
}
|
|
2544
|
+
|
|
2545
|
+
|
|
2546
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
2547
|
+
// GENERIC DATA COMPARE (FR-001)
|
|
2548
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
2549
|
+
|
|
2550
|
+
/**
|
|
2551
|
+
* Generic, field-szintu osszehasonlitas ket adat-objektum kozott.
|
|
2552
|
+
*
|
|
2553
|
+
* **Auto-discovery mod** (`options.fields` nelkul): a metodus mindket
|
|
2554
|
+
* objektum kulcs-uniojan iter, KIVEVE a `DyFM_Metadata` skip-listet
|
|
2555
|
+
* (`_id`, `__created`, `__createdBy`, `__lastModified`, `__lastModifiedBy`).
|
|
2556
|
+
*
|
|
2557
|
+
* **Scope-szukito mod** (`options.fields = [...]`): KIZAROLAG a felsorolt
|
|
2558
|
+
* mezoket vizsgalja. A skip-list IGNORALODIK — explicit fields override.
|
|
2559
|
+
*
|
|
2560
|
+
* **Custom comparator** (`options.customComparators`): per-field override
|
|
2561
|
+
* a default deep-equal helyett (pl. set-equality array-ekre,
|
|
2562
|
+
* case-insensitive string compare-re). A custom function `true`-val signal-ozza
|
|
2563
|
+
* az equality-t.
|
|
2564
|
+
*
|
|
2565
|
+
* **Return:** `'equal'` ha minden vizsgalt field egyezik; `'modified'` ha
|
|
2566
|
+
* legalabb egy elter — ilyenkor `changedFields` tartalmazza a TELJES
|
|
2567
|
+
* mismatch-listat (nem early-return).
|
|
2568
|
+
*
|
|
2569
|
+
* **Throws:**
|
|
2570
|
+
* - `newData` vagy `oldData` null/undefined → `DyFM_Error(400, DyNTS-DS0-CD1)`
|
|
2571
|
+
* - `options.fields = []` ures array → `DyFM_Error(400, DyNTS-DS0-CD2)`
|
|
2572
|
+
*
|
|
2573
|
+
* **Sync method** (nincs I/O szukseglet); a host wrappel-i ha async kell.
|
|
2574
|
+
*
|
|
2575
|
+
* @example
|
|
2576
|
+
* const result = userService.compareData(newUser, oldUser);
|
|
2577
|
+
* if (result.result === 'modified') {
|
|
2578
|
+
* console.log('Changed:', result.changedFields);
|
|
2579
|
+
* }
|
|
2580
|
+
*
|
|
2581
|
+
* @example // Scope-szukites + custom comparator
|
|
2582
|
+
* userService.compareData(newUser, oldUser, {
|
|
2583
|
+
* fields: ['email', 'roles'],
|
|
2584
|
+
* customComparators: {
|
|
2585
|
+
* roles: (a, b) => new Set(a).size === new Set([...a, ...b]).size,
|
|
2586
|
+
* },
|
|
2587
|
+
* });
|
|
2588
|
+
*/
|
|
2589
|
+
compareData(
|
|
2590
|
+
newData: T,
|
|
2591
|
+
oldData: T,
|
|
2592
|
+
options?: DyNTS_CompareData_Options<T>,
|
|
2593
|
+
): DyNTS_CompareData_Result<T> {
|
|
2594
|
+
// Input guard
|
|
2595
|
+
if (newData === null || newData === undefined || oldData === null || oldData === undefined) {
|
|
2596
|
+
throw new DyFM_Error({
|
|
2597
|
+
...this.getDefaultErrorSettings('compareData', new Error('newData/oldData required')),
|
|
2598
|
+
status: 400,
|
|
2599
|
+
errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DS0-CD1`,
|
|
2600
|
+
message: 'compareData: newData and oldData are required (both must be non-null/undefined)',
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
if (options?.fields !== undefined && options.fields.length === 0) {
|
|
2605
|
+
throw new DyFM_Error({
|
|
2606
|
+
...this.getDefaultErrorSettings('compareData', new Error('options.fields must not be empty')),
|
|
2607
|
+
status: 400,
|
|
2608
|
+
errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DS0-CD2`,
|
|
2609
|
+
message: 'compareData: options.fields must not be an empty array',
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
const fieldsToCheck: (keyof T)[] = this._resolveCompareFields(newData, oldData, options);
|
|
2614
|
+
const comparators: Partial<Record<keyof T, (a: any, b: any) => boolean>> =
|
|
2615
|
+
options?.customComparators ?? {};
|
|
2616
|
+
|
|
2617
|
+
const changedFields: (keyof T)[] = [];
|
|
2618
|
+
for (const field of fieldsToCheck) {
|
|
2619
|
+
const a: any = (newData as any)[field];
|
|
2620
|
+
const b: any = (oldData as any)[field];
|
|
2621
|
+
|
|
2622
|
+
const isEqual: boolean = comparators[field]
|
|
2623
|
+
? comparators[field]!(a, b)
|
|
2624
|
+
: this._deepEqual(a, b);
|
|
2625
|
+
|
|
2626
|
+
if (!isEqual) {
|
|
2627
|
+
changedFields.push(field);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
if (changedFields.length === 0) {
|
|
2632
|
+
return { result: 'equal' };
|
|
2633
|
+
}
|
|
2634
|
+
return { result: 'modified', changedFields: changedFields };
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
/**
|
|
2638
|
+
* Eldonti az osszehasonlitando fields-listat:
|
|
2639
|
+
* - explicit `options.fields` esetan annak masolata (skip-list ignoralt)
|
|
2640
|
+
* - egyebkent `Object.keys(newData) UNION Object.keys(oldData)` minus skip-list
|
|
2641
|
+
*/
|
|
2642
|
+
private _resolveCompareFields(
|
|
2643
|
+
newData: T,
|
|
2644
|
+
oldData: T,
|
|
2645
|
+
options?: DyNTS_CompareData_Options<T>,
|
|
2646
|
+
): (keyof T)[] {
|
|
2647
|
+
if (options?.fields !== undefined) {
|
|
2648
|
+
// Explicit fields — copy-spread (defensive against caller mutation mid-flight)
|
|
2649
|
+
return [...options.fields];
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
const keySet: Set<string> = new Set<string>();
|
|
2653
|
+
for (const k of Object.keys(newData as object)) { keySet.add(k); }
|
|
2654
|
+
for (const k of Object.keys(oldData as object)) { keySet.add(k); }
|
|
2655
|
+
|
|
2656
|
+
const result: (keyof T)[] = [];
|
|
2657
|
+
for (const k of keySet) {
|
|
2658
|
+
if (COMPARE_DATA_METADATA_SKIP_FIELDS.has(k)) { continue; }
|
|
2659
|
+
result.push(k as keyof T);
|
|
2660
|
+
}
|
|
2661
|
+
return result;
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
/**
|
|
2665
|
+
* Deep-equal helper.
|
|
2666
|
+
*
|
|
2667
|
+
* - Primitive (`string`/`number`/`boolean`/`null`/`undefined`): `===` / `Object.is`
|
|
2668
|
+
* (a `NaN === NaN` esetet `Object.is` kezeli helyesen)
|
|
2669
|
+
* - `Date`: `.getTime()` egyenloseg (kulonbozo Date instance-ok same-time-mal egyenlonek)
|
|
2670
|
+
* - Array: length + index-szerinti rekurziv deep-equal
|
|
2671
|
+
* - POJO: keys-union + per-key rekurzio
|
|
2672
|
+
* - Egyeb (RegExp / Map / Set / Buffer): `Object.is` fallback (reference compare)
|
|
2673
|
+
*
|
|
2674
|
+
* NEM kezelt: cyclic references — a hivo objektum-grafja flat / fa kell legyen
|
|
2675
|
+
* (a tipikus FDP data-model esete; cycle eseten stack overflow lesz, ami ertelmes
|
|
2676
|
+
* jelzes a programozonak).
|
|
2677
|
+
*/
|
|
2678
|
+
private _deepEqual(a: any, b: any): boolean {
|
|
2679
|
+
if (a === b) { return true; }
|
|
2680
|
+
if (Object.is(a, b)) { return true; }
|
|
2681
|
+
|
|
2682
|
+
// Null/undefined es a masik nem ugyanaz → false
|
|
2683
|
+
if (a === null || a === undefined || b === null || b === undefined) { return false; }
|
|
2684
|
+
|
|
2685
|
+
// Date
|
|
2686
|
+
if (a instanceof Date && b instanceof Date) {
|
|
2687
|
+
return a.getTime() === b.getTime();
|
|
2688
|
+
}
|
|
2689
|
+
if (a instanceof Date || b instanceof Date) { return false; }
|
|
2690
|
+
|
|
2691
|
+
// Array
|
|
2692
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
2693
|
+
if (a.length !== b.length) { return false; }
|
|
2694
|
+
for (let i: number = 0; i < a.length; i++) {
|
|
2695
|
+
if (!this._deepEqual(a[i], b[i])) { return false; }
|
|
2696
|
+
}
|
|
2697
|
+
return true;
|
|
2698
|
+
}
|
|
2699
|
+
if (Array.isArray(a) || Array.isArray(b)) { return false; }
|
|
2700
|
+
|
|
2701
|
+
// POJO (Object.prototype) — keys union, per-key compare
|
|
2702
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
2703
|
+
const keysA: string[] = Object.keys(a);
|
|
2704
|
+
const keysB: string[] = Object.keys(b);
|
|
2705
|
+
if (keysA.length !== keysB.length) { return false; }
|
|
2706
|
+
const keysAset: Set<string> = new Set<string>(keysA);
|
|
2707
|
+
for (const k of keysB) {
|
|
2708
|
+
if (!keysAset.has(k)) { return false; }
|
|
2709
|
+
}
|
|
2710
|
+
for (const k of keysA) {
|
|
2711
|
+
if (!this._deepEqual(a[k], b[k])) { return false; }
|
|
2712
|
+
}
|
|
2713
|
+
return true;
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
// Fallback (function, symbol, BigInt, stb.) — strict equality mar nem stimmelt
|
|
2717
|
+
return false;
|
|
2718
|
+
}
|
|
2525
2719
|
}
|