diff --git a/CHANGELOG.md b/CHANGELOG.md index 289da10..bf22c67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### 1.2.8 +* User `errReporter` to report errors, and don't use `console` by default. +* `Comparator` class (and `comparatorFields` parameter) now understand composite fields. +* Update NgRx dependencies to v15 (stable). + ### 1.2.7 * Restore global configuration feature (`COLLECTION_SERVICE_OPTIONS` token) diff --git a/README.md b/README.md index 0c41757..56588a6 100644 --- a/README.md +++ b/README.md @@ -242,7 +242,36 @@ The equality of items will be checked using the comparator that you can replace The comparator will compare items using `===` first, then it will use id fields. -You can check the id fields list of the default comparator in [comparator.ts](projects/ngx-collection/src/lib/comparator.ts) file. +You can check the default id fields list of the default comparator in [comparator.ts](projects/ngx-collection/src/lib/comparator.ts) file. + +You can easily configure this using Angular DI: +```ts + { + provide: 'COLLECTION_SERVICE_OPTIONS', + useValue: { + comparatorFields: ['uuId', 'url'], + } + }, +``` + +or by re-instantiating a Comparator: + +```ts +export class NotificationsCollectionService extends CollectionService { + override init() { + this.setComparator(new Comparator(['signature'])) + } +} +``` + +or by providing your own Comparator - it can be a class, implementing `ObjectsComparator` interface, or a function, implementing `ObjectsComparatorFn`. + +## Comparator fields + +In default comparator, every item in the list of fields can be: +1. `string` - if both objects have this field, and values are equal, objects are equal. + If both objects have this field, and values are not equal - objects are not equal and __comparison stops__. +2. `string[]` - composite field: if both objects have every field enlisted, and every value is equal, objects are equal. ## Duplicates prevention @@ -254,10 +283,10 @@ To find a duplicate, items will be compared by comparator - objects equality che A collection might have duplicates because of some data error or because of wrong fields in the comparator - you can redefine them. -If a duplicate is detected, the item will not be added to the collection and an error will be printed to the console (if `window.console` does exist). +If a duplicate is detected, the item will not be added to the collection and an error will be sent to the `errReporter` (if `errReporter` is set). You can call `setThrowOnDuplicates('some message')` to make Collection Service throw an exception with the message you expect. -Method `read()` by default will put returned items to the collection even if they have duplicates, but `console.error` will be raised (if `window.console` exists), +Method `read()` by default will put returned items to the collection even if they have duplicates, but `errReporter` will be called (if `errReporter` is set), because in this case you have a chance to damage your data in future `update()`. You can call `setAllowFetchedDuplicates(false)` to instruct `read()` to not accept items lists with duplicates. @@ -331,6 +360,7 @@ providers: [ provide: 'COLLECTION_SERVICE_OPTIONS', useValue: { allowFetchedDuplicates: environment.production, + errReporter: environment.production ? undefined : console.error } }, ] @@ -353,7 +383,10 @@ interface CollectionServiceOptions { allowFetchedDuplicates?: boolean; // in case of duplicate detection, `onError` callback function (if provided) will be called with this value as an argument - onDuplicateErrCallbackParam?: any; + onDuplicateErrCallbackParam?: any; + + // print errors. Example: ` errReporter: environment.production ? undefined : console.error ` + errReporter?: (...args: any[]) => any; } ``` @@ -647,6 +680,9 @@ interface CollectionServiceOptions { // in case of duplicate detection, `onError` callback function (if provided) will be called with this value as an argument onDuplicateErrCallbackParam?: any; + + // print errors. Example: ` errReporter: environment.production ? undefined : console.error ` + errReporter?: (...args: any[]) => any; } ``` diff --git a/libs/ngx-collection/README.md b/libs/ngx-collection/README.md index 0c41757..56588a6 100644 --- a/libs/ngx-collection/README.md +++ b/libs/ngx-collection/README.md @@ -242,7 +242,36 @@ The equality of items will be checked using the comparator that you can replace The comparator will compare items using `===` first, then it will use id fields. -You can check the id fields list of the default comparator in [comparator.ts](projects/ngx-collection/src/lib/comparator.ts) file. +You can check the default id fields list of the default comparator in [comparator.ts](projects/ngx-collection/src/lib/comparator.ts) file. + +You can easily configure this using Angular DI: +```ts + { + provide: 'COLLECTION_SERVICE_OPTIONS', + useValue: { + comparatorFields: ['uuId', 'url'], + } + }, +``` + +or by re-instantiating a Comparator: + +```ts +export class NotificationsCollectionService extends CollectionService { + override init() { + this.setComparator(new Comparator(['signature'])) + } +} +``` + +or by providing your own Comparator - it can be a class, implementing `ObjectsComparator` interface, or a function, implementing `ObjectsComparatorFn`. + +## Comparator fields + +In default comparator, every item in the list of fields can be: +1. `string` - if both objects have this field, and values are equal, objects are equal. + If both objects have this field, and values are not equal - objects are not equal and __comparison stops__. +2. `string[]` - composite field: if both objects have every field enlisted, and every value is equal, objects are equal. ## Duplicates prevention @@ -254,10 +283,10 @@ To find a duplicate, items will be compared by comparator - objects equality che A collection might have duplicates because of some data error or because of wrong fields in the comparator - you can redefine them. -If a duplicate is detected, the item will not be added to the collection and an error will be printed to the console (if `window.console` does exist). +If a duplicate is detected, the item will not be added to the collection and an error will be sent to the `errReporter` (if `errReporter` is set). You can call `setThrowOnDuplicates('some message')` to make Collection Service throw an exception with the message you expect. -Method `read()` by default will put returned items to the collection even if they have duplicates, but `console.error` will be raised (if `window.console` exists), +Method `read()` by default will put returned items to the collection even if they have duplicates, but `errReporter` will be called (if `errReporter` is set), because in this case you have a chance to damage your data in future `update()`. You can call `setAllowFetchedDuplicates(false)` to instruct `read()` to not accept items lists with duplicates. @@ -331,6 +360,7 @@ providers: [ provide: 'COLLECTION_SERVICE_OPTIONS', useValue: { allowFetchedDuplicates: environment.production, + errReporter: environment.production ? undefined : console.error } }, ] @@ -353,7 +383,10 @@ interface CollectionServiceOptions { allowFetchedDuplicates?: boolean; // in case of duplicate detection, `onError` callback function (if provided) will be called with this value as an argument - onDuplicateErrCallbackParam?: any; + onDuplicateErrCallbackParam?: any; + + // print errors. Example: ` errReporter: environment.production ? undefined : console.error ` + errReporter?: (...args: any[]) => any; } ``` @@ -647,6 +680,9 @@ interface CollectionServiceOptions { // in case of duplicate detection, `onError` callback function (if provided) will be called with this value as an argument onDuplicateErrCallbackParam?: any; + + // print errors. Example: ` errReporter: environment.production ? undefined : console.error ` + errReporter?: (...args: any[]) => any; } ``` diff --git a/libs/ngx-collection/package.json b/libs/ngx-collection/package.json index 7668128..5a056c3 100644 --- a/libs/ngx-collection/package.json +++ b/libs/ngx-collection/package.json @@ -1,6 +1,6 @@ { "name": "ngx-collection", - "version": "1.2.7", + "version": "1.2.8", "license": "MIT", "author": { "name": "Evgeniy OZ", @@ -14,9 +14,9 @@ "private": false, "peerDependencies": { "@angular/core": "^15.0.1", - "@ngrx/component-store": "^15.0.0-rc.0", - "@ngrx/effects": "^15.0.0-rc.0", - "@ngrx/store": "^15.0.0-rc.0", + "@ngrx/component-store": "^15.0.0", + "@ngrx/effects": "^15.0.0", + "@ngrx/store": "^15.0.0", "rxjs": "^7.5.7" }, "devDependencies": { diff --git a/libs/ngx-collection/src/lib/collection.service.ts b/libs/ngx-collection/src/lib/collection.service.ts index ff9531f..77908f2 100644 --- a/libs/ngx-collection/src/lib/collection.service.ts +++ b/libs/ngx-collection/src/lib/collection.service.ts @@ -96,11 +96,12 @@ export interface RefreshManyParams { export type DuplicatesMap = Record>; export interface CollectionServiceOptions { - comparatorFields?: string[]; + comparatorFields?: (string | string[])[]; comparator?: ObjectsComparator | ObjectsComparatorFn; throwOnDuplicates?: string; allowFetchedDuplicates?: boolean; onDuplicateErrCallbackParam?: any; + errReporter?: (...args: any[]) => any; } @Injectable() @@ -144,6 +145,7 @@ export class CollectionService extends Comp protected throwOnDuplicates?: string; protected allowFetchedDuplicates: boolean = true; protected onDuplicateErrCallbackParam = {status: 409}; + protected errReporter?: (...args: any[]) => any; protected addToUpdatingItems(item: Partial | Partial[]) { this.patchState((s) => { @@ -290,11 +292,11 @@ export class CollectionService extends Comp if (this.throwOnDuplicates) { throw new Error(this.throwOnDuplicates); } - if (console) { - console.error('Duplicate can not be added to collection:', item, items); + if (this.errReporter) { + this.errReporter('Duplicate can not be added to collection:', item, items); const duplicates = this.getDuplicates(items); if (duplicates) { - console.error('Duplicates are found in collection:', duplicates); + this.errReporter('Duplicates are found in collection:', duplicates); } } } @@ -314,7 +316,7 @@ export class CollectionService extends Comp }); this.setOptions(options); this.init(); - Promise.resolve().then(()=> this.postInit()); + Promise.resolve().then(() => this.postInit()); } protected init() { @@ -885,6 +887,9 @@ export class CollectionService extends Comp if (options.onDuplicateErrCallbackParam != null) { this.onDuplicateErrCallbackParam = options.onDuplicateErrCallbackParam; } + if (options.errReporter != null && typeof options.errReporter === 'function') { + this.errReporter = options.errReporter; + } } } } diff --git a/libs/ngx-collection/src/lib/comparator.spec.ts b/libs/ngx-collection/src/lib/comparator.spec.ts new file mode 100644 index 0000000..fd4cb44 --- /dev/null +++ b/libs/ngx-collection/src/lib/comparator.spec.ts @@ -0,0 +1,112 @@ +import { Comparator } from './comparator'; + +describe('comparator', () => { + it('single field', () => { + const c = new Comparator(['id']); + + expect(c.equal( + {id: 0, name: 'A'}, + {id: 0, name: 'B'}, + )).toBeTruthy(); + + expect(c.equal( + {uuId: false, name: 'A'}, + {uuId: false, name: 'B'}, + ['uuId'] + )).toBeTruthy(); + + expect(c.equal( + {uuId: null, name: 'A'}, + {uuId: 'UUID', name: 'B'}, + ['uuId'] + )).toBeFalsy(); + + expect(c.equal( + {uuId: {}, name: 'A'}, + {uuId: 'UUID', name: 'B'}, + ['uuId'] + )).toBeFalsy(); + + expect(c.equal( + {uuId: [], name: 'A'}, + {uuId: 'UUID', name: 'B'}, + ['uuId'] + )).toBeFalsy(); + + expect(c.equal( + {uuId: undefined, name: 'A'}, + {uuId: 'UUID', name: 'B'}, + ['uuId'] + )).toBeFalsy(); + + expect(c.equal( + {uuId: undefined, name: 'A'}, + {uuId: undefined, name: 'B'}, + ['uuId'] + )).toBeFalsy(); + + expect(c.equal( + {name: 'A'}, + {name: 'B'}, + ['uuId'] + )).toBeFalsy(); + + expect(c.equal( + {path: 'body/div', name: 'A'}, + {path: 'body/div', name: 'B'}, + ['path'] + )).toBeTruthy(); + + expect(c.equal( + {id: 0, name: 'A'}, + {id: 0, name: 'B'}, + [['id']] + )).toBeTruthy(); + + expect(c.equal( + {id: 0, name: 'A'}, + {id: 0, name: 'B'}, + ['id', 'name'] + )).toBeTruthy(); + + expect(c.equal( + {id: 0, name: 'A'}, + {name: 'A'}, + ['id', 'name'] + )).toBeTruthy(); + + expect(c.equal( + {id: 0, name: 'A'}, + {id: 1, name: 'A'}, + ['id', 'name'] + )).toBeFalsy(); + }); + + it('multiple fields', () => { + const c = new Comparator([['id', 'status']]); + + expect(c.equal( + {id: 0, name: 'A', status: 'N'}, + {id: 0, name: 'B', status: 'N'}, + )).toBeTruthy(); + + expect(c.equal( + {id: 0, name: 'A', status: 'N'}, + {id: 1, name: 'B', status: 'N'}, + )).toBeFalsy(); + + expect(c.equal( + {id: false, name: 'B', status: 'N'}, + {id: false, name: 'B', status: 'M'}, + [['id', 'name']] + )).toBeTruthy(); + + expect(c.equal( + {id: false, name: 'B', status: 'N'}, + {id: false, name: 'B', status: 'M'}, + [['id', 'status'], ['name']] + )).toBeTruthy(); + + }); + +}); \ No newline at end of file diff --git a/libs/ngx-collection/src/lib/comparator.ts b/libs/ngx-collection/src/lib/comparator.ts index a875941..4f6439a 100644 --- a/libs/ngx-collection/src/lib/comparator.ts +++ b/libs/ngx-collection/src/lib/comparator.ts @@ -7,9 +7,9 @@ export interface ObjectsComparator { } export class Comparator implements ObjectsComparator { - constructor(private fields: string[] = ['id']) {} + constructor(private fields: (string | string[])[] = ['uuId', 'id']) {} - equal(obj1: unknown, obj2: unknown, byFields?: string[]): boolean { + equal(obj1: unknown, obj2: unknown, byFields?: (string | string[])[]): boolean { if (obj1 === obj2) { return true; } @@ -19,17 +19,22 @@ export class Comparator implements ObjectsComparator { const fields = byFields?.length ? byFields : this.fields; if (fields.length) { for (const field of fields) { - if (obj1.hasOwnProperty(field) && obj2.hasOwnProperty(field)) { - if (!isEmptyValue((obj1 as any)[field]) - && !isEmptyValue((obj2 as any)[field]) - && !isEmptyObject((obj1 as any)[field]) - && !isEmptyObject((obj2 as any)[field]) - && (obj1 as any)[field] === (obj2 as any)[field] + if (typeof field === 'string') { + if (obj1.hasOwnProperty(field) && obj2.hasOwnProperty(field)) { + if (fieldValuesAreNotEmptyAndEqual(obj1, obj2, field)) { + return true; + } else { + // stop the chain on the first found pair of existing fields + return false; + } + } + } else { + if (!field.find(key => + !obj1.hasOwnProperty(key) + || !obj2.hasOwnProperty(key) + || !fieldValuesAreNotEmptyAndEqual(obj1, obj2, key)) ) { return true; - } else { - // stop the chain on the first found pair of existing fields - return false; } } } @@ -37,3 +42,12 @@ export class Comparator implements ObjectsComparator { return false; } } + +function fieldValuesAreNotEmptyAndEqual(obj1: unknown, obj2: unknown, field: string): boolean { + return (!isEmptyValue((obj1 as any)[field]) + && !isEmptyValue((obj2 as any)[field]) + && !isEmptyObject((obj1 as any)[field]) + && !isEmptyObject((obj2 as any)[field]) + && (obj1 as any)[field] === (obj2 as any)[field] + ); +} diff --git a/package.json b/package.json index 09a3d56..fe32103 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ngx-collection", - "version": "1.2.7", + "version": "1.2.8", "license": "MIT", "author": { "name": "Evgeniy OZ", @@ -24,9 +24,9 @@ "private": false, "dependencies": { "@angular/core": "^15.0.1", - "@ngrx/component-store": "^15.0.0-rc.0", - "@ngrx/effects": "^15.0.0-rc.0", - "@ngrx/store": "^15.0.0-rc.0", + "@ngrx/component-store": "^15.0.0", + "@ngrx/effects": "^15.0.0", + "@ngrx/store": "^15.0.0", "@nrwl/angular": "^15.2.1", "rxjs": "^7.5.7" },