From 3e992f93328a94cdb5ef66795cef417d6df25698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20=C5=A0p=C3=A1c?= Date: Tue, 4 Jun 2019 11:38:09 +0200 Subject: [PATCH] refactor custom isEqual to utilise lodash.isequal --- packages/apollo-utilities/package-lock.json | 20 ++++- packages/apollo-utilities/package.json | 4 + .../src/util/__tests__/isEqual.ts | 81 +++++++++++++++++++ packages/apollo-utilities/src/util/isEqual.ts | 45 +---------- 4 files changed, 107 insertions(+), 43 deletions(-) diff --git a/packages/apollo-utilities/package-lock.json b/packages/apollo-utilities/package-lock.json index 250091be6a6..002098f0350 100644 --- a/packages/apollo-utilities/package-lock.json +++ b/packages/apollo-utilities/package-lock.json @@ -1,14 +1,32 @@ { "name": "apollo-utilities", - "version": "1.2.1", + "version": "1.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/lodash": { + "version": "4.14.133", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.133.tgz", + "integrity": "sha512-/3JqnvPnY58GLzG3Y7fpphOhATV1DDZ/Ak3DQufjlRK5E4u+s0CfClfNFtAGBabw+jDGtRFbOZe+Z02ZMWCBNQ==" + }, + "@types/lodash.isequal": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.5.tgz", + "integrity": "sha512-4IKbinG7MGP131wRfceK6W4E/Qt3qssEFLF30LnJbjYiSfHGGRU/Io8YxXrZX109ir+iDETC8hw8QsDijukUVg==", + "requires": { + "@types/lodash": "*" + } + }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "ts-invariant": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.0.tgz", diff --git a/packages/apollo-utilities/package.json b/packages/apollo-utilities/package.json index f1563de4ff8..c69c15ec743 100644 --- a/packages/apollo-utilities/package.json +++ b/packages/apollo-utilities/package.json @@ -40,7 +40,11 @@ }, "dependencies": { "fast-json-stable-stringify": "^2.0.0", + "lodash.isequal": "^4.5.0", "ts-invariant": "^0.4.0", "tslib": "^1.9.3" + }, + "devDependencies": { + "@types/lodash.isequal": "^4.5.5" } } diff --git a/packages/apollo-utilities/src/util/__tests__/isEqual.ts b/packages/apollo-utilities/src/util/__tests__/isEqual.ts index 2865d43a038..e621a837082 100644 --- a/packages/apollo-utilities/src/util/__tests__/isEqual.ts +++ b/packages/apollo-utilities/src/util/__tests__/isEqual.ts @@ -90,4 +90,85 @@ describe('isEqual', () => { expect(!isEqual([1, 2, 3], [1, 2, 4])).toBe(true); delete Array.prototype.foo; }); + + describe('comparing objects with circular refs', () => { + // copied with slight modification from lodash test suite + it('should compare objects with circular references', () => { + const object1 = {}, + object2 = {}; + + object1.a = object1; + object2.a = object2; + + expect(isEqual(object1, object2)).toBe(true); + + object1.b = 0; + object2.b = Object(0); + + expect(isEqual(object1, object2)).toBe(true); + + object1.c = Object(1); + object2.c = Object(2); + + expect(isEqual(object1, object2)).toBe(false); + + object1 = { a: 1, b: 2, c: 3 }; + object1.b = object1; + object2 = { a: 1, b: { a: 1, b: 2, c: 3 }, c: 3 }; + + expect(isEqual(object1, object2)).toBe(false); + }); + + it('should have transitive equivalence for circular references of objects', () => { + const object1 = {}, + object2 = { a: object1 }, + object3 = { a: object2 }; + + object1.a = object1; + + expect(isEqual(object1, object2)).toBe(true); + expect(isEqual(object2, object3)).toBe(true); + expect(isEqual(object1, object3)).toBe(true); + }); + + it('should compare objects with multiple circular references', () => { + const array1 = [{}], + array2 = [{}]; + + (array1[0].a = array1).push(array1); + (array2[0].a = array2).push(array2); + + expect(isEqual(array1, array2)).toBe(true); + + array1[0].b = 0; + array2[0].b = Object(0); + + expect(isEqual(array1, array2)).toBe(true); + + array1[0].c = Object(1); + array2[0].c = Object(2); + + expect(isEqual(array1, array2)).toBe(false); + }); + + it('should compare objects with complex circular references', () => { + const object1 = { + foo: { b: { c: { d: {} } } }, + bar: { a: 2 }, + }; + + const object2 = { + foo: { b: { c: { d: {} } } }, + bar: { a: 2 }, + }; + + object1.foo.b.c.d = object1; + object1.bar.b = object1.foo.b; + + object2.foo.b.c.d = object2; + object2.bar.b = object2.foo.b; + + expect(isEqual(object1, object2)).toBe(true); + }); + }); }); diff --git a/packages/apollo-utilities/src/util/isEqual.ts b/packages/apollo-utilities/src/util/isEqual.ts index 15bedc2696b..7713a31a082 100644 --- a/packages/apollo-utilities/src/util/isEqual.ts +++ b/packages/apollo-utilities/src/util/isEqual.ts @@ -1,47 +1,8 @@ +import isEqualLodash from 'lodash.isequal'; + /** * Performs a deep equality check on two JavaScript values. */ export function isEqual(a: any, b: any): boolean { - // If the two values are strictly equal, we are good. - if (a === b) { - return true; - } - // Dates are equivalent if their time values are equal. - if (a instanceof Date && b instanceof Date) { - return a.getTime() === b.getTime(); - } - // If a and b are both objects, we will compare their properties. This will compare arrays as - // well. - if ( - a != null && - typeof a === 'object' && - b != null && - typeof b === 'object' - ) { - // Compare all of the keys in `a`. If one of the keys has a different value, or that key does - // not exist in `b` return false immediately. - for (const key in a) { - if (Object.prototype.hasOwnProperty.call(a, key)) { - if (!Object.prototype.hasOwnProperty.call(b, key)) { - return false; - } - if (!isEqual(a[key], b[key])) { - return false; - } - } - } - // Look through all the keys in `b`. If `b` has a key that `a` does not, return false. - for (const key in b) { - if ( - Object.prototype.hasOwnProperty.call(b, key) && - !Object.prototype.hasOwnProperty.call(a, key) - ) { - return false; - } - } - // If we made it this far the objects are equal! - return true; - } - // Otherwise the values are not equal. - return false; + return isEqualLodash(a, b); }