From b78efd19469f0adc0a7513ea5ef3619be4f61738 Mon Sep 17 00:00:00 2001 From: David East Date: Tue, 7 Jun 2016 06:21:58 -0700 Subject: [PATCH 1/3] fix(query): Methods for sorting object keys --- src/querybase.ts | 73 ++++++++++++++++++++-- tests/unit/querybase-utils.spec.js | 99 ++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 4 deletions(-) diff --git a/src/querybase.ts b/src/querybase.ts index 1caded4..7ea83f6 100644 --- a/src/querybase.ts +++ b/src/querybase.ts @@ -27,6 +27,10 @@ interface QuerybaseUtils { values(obj): any[]; encodeBase64(data: string): string; arraysToObject(keys, values): Object; + lexicographicallySort(a: string, b: string): number; + getKeyIndexPositions(arr: string[]): Object; + createSortedObject(keys: string[], values: any[]); + sortObjectLexicographically(obj: Object): Object; } const _: QuerybaseUtils = { @@ -160,7 +164,68 @@ const _: QuerybaseUtils = { count++; }); return indexHash; + }, + + /** + * A function for lexicographically comparing keys. Used for + * array sort methods. + * @param {string} a + * @param {string} b + * @return {number} + */ + lexicographicallySort(a: string, b: string): number { + return a.localeCompare(b); + }, + + /** + * Creates an object with the key name and position in an array + * @param {string[]} arr + * @return {Object} + * @example + * const keys = ['name', 'age', 'location']; + * const indexKeys = _.getKeyIndexPositions(keys); + * => { name: 0, age: 1, location: 2 } + */ + getKeyIndexPositions(arr: string[]): Object { + const indexOfKeys = {}; + arr.forEach((key, index) => indexOfKeys[key] = index); + return indexOfKeys; + }, + + /** + * Creates an object whose keys are lexicographically sorted + * @param {string[]} keys + * @param {any[]} values + * @return {Object} + * @example + * const keys = ['name', 'age', 'location']; + * const values = ['David', '28', 'SF']; + * const sortedObj = _.createSortedObject(keys, values); + * => { age: '28', location: 'SF', name: 'David' } + */ + createSortedObject(keys: string[], values: any[]) { + const sortedRecord = {}; + const indexOfKeys = this.getKeyIndexPositions(keys); + const indexKeys = this.keys(indexOfKeys); + indexKeys.forEach((key, index) => sortedRecord[key] = values[index]); + return sortedRecord; + }, + + /** + * Creates an object whose keys are lexicographically sorted + * @param {obj} Object + * @return {Object} + * @example + * const record = { name: 'David', age: '28', location: 'SF' }; + * const sortedObj = _.sortObjectLexicographically(record); + * => { age: '28', location: 'SF', name: 'David' } + */ + sortObjectLexicographically(obj: Object): Object { + const keys = this.keys(obj); + const values = this.values(obj); + return this.createSortedObject(keys, values); } + } /** @@ -171,8 +236,8 @@ const _: QuerybaseUtils = { * * @example * // Querybase for multiple equivalency - * const firebaseRef = new Firebase('/people'); - * const querybaseRef = new Querybase(firebaseRef, ['name', 'age', 'location']); + * const firebaseRef = firebase.database.ref().child('people'); + * const querybaseRef = querybase.ref(firebaseRef, ['name', 'age', 'location']); * * // Automatically handles composite keys * querybaseRef.push({ @@ -228,13 +293,13 @@ class Querybase { } /** - * Check for a Firebase reference. Throw an exception if not provided. + * Check for a Firebase Database reference. Throw an exception if not provided. * @parameter {Firebase} * @return {void} */ private _assertFirebaseRef(ref) { if (ref === null || ref === undefined || !ref.on) { - throw new Error(`No Firebase Reference provided in the Querybase constructor.`); + throw new Error(`No Firebase Database Reference provided in the Querybase constructor.`); } } diff --git a/tests/unit/querybase-utils.spec.js b/tests/unit/querybase-utils.spec.js index fb6eb41..69f3864 100644 --- a/tests/unit/querybase-utils.spec.js +++ b/tests/unit/querybase-utils.spec.js @@ -10,6 +10,42 @@ const expect = chai.expect; describe('QuerybaseUtils', () => { + const smallRecord = { + zed: 'a', + name: 'David', + time: '5:01' + }; + + const mediumRecord = { + zed: 'a', + zzzz: 'zzzz', + name: 'David', + time: '5:01', + alpha: 'a', + yogi: 'bear', + jon: 'jon' + }; + + const smallRecordKeys = _.keys(smallRecord); + const smallRecordValues = _.values(smallRecord); + const mediumRecordKeys = _.keys(mediumRecord); + const mediumRecordValues = _.values(mediumRecord); + + const sortedSmallRecord = { + name: 'David', + time: '5:01', + zed: 'a' + }; + + const sortedMediumRecord = { + 'alpha': 'a', + 'jon': 'jon', + 'name': 'David', + 'time': '5:01', + 'yogi': 'bear', + 'zed': 'a', + 'zzzz': 'zzzz' + }; it('should exist', () => { expect(_).to.exist; }) @@ -96,4 +132,67 @@ describe('QuerybaseUtils', () => { }); + describe('lexicographicallySort', () => { + + it('should lexicographically sort', () => { + const sorted = smallRecordKeys.slice().sort(_.lexicographicallySort); + assert.deepEqual(['name', 'time', 'zed'], sorted); + }); + + it('should lexicographically sort', () => { + const sorted = mediumRecordKeys.slice().sort(_.lexicographicallySort); + const expectedOrder = [ + 'alpha', + 'jon', + 'name', + 'time', + 'yogi', + 'zed', + 'zzzz' + ]; + assert.deepEqual(expectedOrder, sorted); + }); + + }); + + describe('getKeyIndexPositions', () => { + + it('should create an object with the index positions', () => { + + const indexOfKeys = _.getKeyIndexPositions(smallRecordKeys); + console.log(indexOfKeys); + assert.deepEqual({ 'zed': 0, 'name': 1, 'time': 2 }, indexOfKeys); + + }); + + }); + + describe('createSortedObject', () => { + + it('should create a sorted object', () => { + const sortedRecord = _.createSortedObject(smallRecordKeys, smallRecordValues); + assert.deepEqual(sortedSmallRecord, sortedRecord); + }) + + it('should create a sorted object', () => { + const sortedRecord = _.createSortedObject(mediumRecordKeys, mediumRecordValues); + assert.deepEqual(sortedMediumRecord, sortedRecord); + }) + + }); + + describe('sortObjectLexicographically', () => { + + it('sort an object lexicographically', () => { + const sortedRecord = _.sortObjectLexicographically(smallRecord); + assert.deepEqual(sortedSmallRecord, sortedRecord); + }); + + it('sort an object lexicographically', () => { + const sortedRecord = _.sortObjectLexicographically(mediumRecord); + assert.deepEqual(sortedMediumRecord, sortedRecord); + }); + + }); + }); \ No newline at end of file From aec1838e5ef2dcb4d31a81117e865a545a055299 Mon Sep 17 00:00:00 2001 From: David East Date: Tue, 7 Jun 2016 06:55:49 -0700 Subject: [PATCH 2/3] fix(query): Fix key ordering to match server key ordering --- src/querybase.ts | 28 +++++++++++++++++++--------- tests/unit/querybase-utils.spec.js | 9 ++++----- tests/unit/querybase.spec.js | 17 +++++++---------- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/querybase.ts b/src/querybase.ts index 7ea83f6..b06ea17 100644 --- a/src/querybase.ts +++ b/src/querybase.ts @@ -206,8 +206,13 @@ const _: QuerybaseUtils = { createSortedObject(keys: string[], values: any[]) { const sortedRecord = {}; const indexOfKeys = this.getKeyIndexPositions(keys); - const indexKeys = this.keys(indexOfKeys); - indexKeys.forEach((key, index) => sortedRecord[key] = values[index]); + const sortedKeys = keys.sort(this.lexicographicallySort); + + sortedKeys.forEach((key) => { + let index = indexOfKeys[key]; + sortedRecord[key] = values[index]; + }); + return sortedRecord; }, @@ -286,7 +291,7 @@ class Querybase { this._assertIndexes(indexOn); this.ref = () => ref; - this.indexOn = () => indexOn; + this.indexOn = () => indexOn.sort(_.lexicographicallySort); /* istanbul ignore next */ this.getKey = () => this.ref().getKey(); this.encodedKeys = () => this.encodeKeys(this.indexOn()); @@ -387,9 +392,13 @@ class Querybase { * @return {FirebaseRef} */ private _createQueryPredicate(criteria): QueryPredicate { + + // Sort provided object lexicographically to match keys in database + const sortedCriteria = _.sortObjectLexicographically(criteria); + // retrieve the keys and values array - const keys = _.keys(criteria); - const values = _.values(criteria); + const keys = _.keys(sortedCriteria); + const values = _.values(sortedCriteria); // warn about the indexes for indexOn rules this._warnAboutIndexOnRule(); @@ -456,7 +465,7 @@ class Querybase { if (_.isString(criteria)) { return this._createChildOrderedQuery(criteria); } - + // Create the query predicate to build the Firebase Query const queryPredicate = this._createQueryPredicate(criteria); return this._createEqualToQuery(queryPredicate); @@ -465,7 +474,7 @@ class Querybase { /** * Creates a set of composite keys with composite data. Creates every * possible combination of keys with respecive combined values. Redudant - * keys are not included ('name~~age' vs. 'agenname'). + * keys are not included ('name~~age' vs. 'age~~name'). * @param {any[]} indexes * @param {Object} data * @param {Object?} indexHash for recursive check @@ -555,13 +564,14 @@ class Querybase { /** * Encode (base64) all keys and data to avoid collisions with the - * chosen Querybase delimiter key (_) + * chosen Querybase delimiter key (~~) * @param {Object} indexWithData * @return {Object} */ indexify(data: Object) { const compositeIndex = this._createCompositeIndex(this.indexOn(), data); - return this._encodeCompositeIndex(compositeIndex); + const encodedIndexes = this._encodeCompositeIndex(compositeIndex); + return encodedIndexes; } /** diff --git a/tests/unit/querybase-utils.spec.js b/tests/unit/querybase-utils.spec.js index 69f3864..e8ba625 100644 --- a/tests/unit/querybase-utils.spec.js +++ b/tests/unit/querybase-utils.spec.js @@ -160,7 +160,6 @@ describe('QuerybaseUtils', () => { it('should create an object with the index positions', () => { const indexOfKeys = _.getKeyIndexPositions(smallRecordKeys); - console.log(indexOfKeys); assert.deepEqual({ 'zed': 0, 'name': 1, 'time': 2 }, indexOfKeys); }); @@ -171,12 +170,12 @@ describe('QuerybaseUtils', () => { it('should create a sorted object', () => { const sortedRecord = _.createSortedObject(smallRecordKeys, smallRecordValues); - assert.deepEqual(sortedSmallRecord, sortedRecord); + assert.deepEqual(_.keys(sortedRecord), _.keys(sortedSmallRecord)); }) it('should create a sorted object', () => { const sortedRecord = _.createSortedObject(mediumRecordKeys, mediumRecordValues); - assert.deepEqual(sortedMediumRecord, sortedRecord); + assert.deepEqual(_.keys(sortedMediumRecord), _.keys(sortedRecord)); }) }); @@ -185,12 +184,12 @@ describe('QuerybaseUtils', () => { it('sort an object lexicographically', () => { const sortedRecord = _.sortObjectLexicographically(smallRecord); - assert.deepEqual(sortedSmallRecord, sortedRecord); + assert.deepEqual(_.keys(sortedSmallRecord), _.keys(sortedRecord)); }); it('sort an object lexicographically', () => { const sortedRecord = _.sortObjectLexicographically(mediumRecord); - assert.deepEqual(sortedMediumRecord, sortedRecord); + assert.deepEqual(_.keys(sortedMediumRecord), _.keys(sortedRecord)); }); }); diff --git a/tests/unit/querybase.spec.js b/tests/unit/querybase.spec.js index 87a326b..f6bb62e 100644 --- a/tests/unit/querybase.spec.js +++ b/tests/unit/querybase.spec.js @@ -177,9 +177,8 @@ describe('Querybase', () => { it('should return a child Querybase ref with new indexes', () => { const childRef = queryRef.child('some/path', ['name', 'color']) - // TODO: array comparison - assert.equal(childRef.indexOn()[0], 'name'); - assert.equal(childRef.indexOn()[1], 'color'); + assert.equal(childRef.indexOn()[0], 'color'); + assert.equal(childRef.indexOn()[1], 'name'); }); it('should throw if no indexes are provided', () => { @@ -202,7 +201,7 @@ describe('Querybase', () => { }); it('should create a Firebase query for multiple criteria', () => { - const query = queryRef.where({ color: 'green', weight: '120' }); + const query = queryRef.where({ weight: '120', color: 'green' }); assert.equal(true, helpers.isFirebaseRef(query)); }); @@ -232,7 +231,7 @@ describe('Querybase', () => { }); // encoded - // { color: 'Blue', height: '67 } + // { height: '67', color: 'Blue' } it('should encode a QueryPredicate for multiple criteria', () => { const expectedPredicate = { @@ -240,7 +239,7 @@ describe('Querybase', () => { value: 'querybase~~Qmx1ZX5+Njc=' }; - const predicate = queryRef._createQueryPredicate({ color: 'Blue', height: 67 }); + const predicate = queryRef._createQueryPredicate({ height: 67, color: 'Blue' }); assert.deepEqual(expectedPredicate, predicate); }); @@ -252,9 +251,9 @@ describe('Querybase', () => { it('should create a composite index', () => { const compositeIndex = queryRef._createCompositeIndex(indexes, { - color: 'Blue', + weight: 130, height: 67, - weight: 130 + color: 'Blue' }); assert.deepEqual(compositeIndex, expectedIndex); @@ -289,8 +288,6 @@ describe('Querybase', () => { 'querybase~~aGVpZ2h0fn53ZWlnaHQ=': 'querybase~~Njd+fjEzMA==' }; - - const encodedIndex = queryRef._encodeCompositeIndex(expectedIndex); assert.deepEqual(expectedEncodedIndex, encodedIndex); }); From 24052ae70f2123c0d0242bb8c54b6521a60dded2 Mon Sep 17 00:00:00 2001 From: David East Date: Mon, 20 Jun 2016 06:55:50 -0700 Subject: [PATCH 3/3] fix(indexes): Limit indexes to three for sanity --- examples/index.html | 21 +++++++++---------- src/querybase.ts | 37 +++++++++++++++++++++++++++++----- tests/unit/querybase.spec.js | 10 +++++++-- typings/firebase/firebase.d.ts | 4 ++-- 4 files changed, 52 insertions(+), 20 deletions(-) diff --git a/examples/index.html b/examples/index.html index 34b6278..3f98360 100644 --- a/examples/index.html +++ b/examples/index.html @@ -23,18 +23,18 @@ var ref = firebase.database().ref(); var peopleRef = ref.child("people"); - var queryRef = querybase.ref(peopleRef, ['name', 'age', 'location', 'weight']); + var queryRef = querybase.ref(peopleRef, ['name', 'age', 'location']); btnSeed.addEventListener('click', function(e) { peopleRef.remove(); - addPerson('David_East', 27, 'SF', 125); - addPerson('Shannon', 28, 'SF', 115); - addPerson('Coco', 1, 'SF', 12); - addPerson('Cash', 3, 'SF', 32); - addPerson('George', 27, 'DEN', 150); - addPerson('Bob', '27', 'SF', 160); - addPerson('Molly', 6, 'SF', 55); - addPerson('Mambo', 4, 'DEN', 6); + addPerson('David_East', 27, 'SF'); + addPerson('Shannon', 28, 'SF'); + addPerson('Coco', 1, 'SF'); + addPerson('Cash', 3, 'SF'); + addPerson('George', 27, 'DEN'); + addPerson('Bob', '27', 'SF'); + addPerson('Molly', 6, 'SF'); + addPerson('Mambo', 4, 'DEN'); }); function listen(ref) { @@ -47,8 +47,7 @@ queryRef.push({ name: name, age: age, - location: location, - weight: weight + location: location }); } diff --git a/src/querybase.ts b/src/querybase.ts index b06ea17..de3b0b9 100644 --- a/src/querybase.ts +++ b/src/querybase.ts @@ -31,6 +31,7 @@ interface QuerybaseUtils { getKeyIndexPositions(arr: string[]): Object; createSortedObject(keys: string[], values: any[]); sortObjectLexicographically(obj: Object): Object; + decodeBase64(encoded: string): string; } const _: QuerybaseUtils = { @@ -145,6 +146,19 @@ const _: QuerybaseUtils = { } }, + /** + * Universal base64 decode method + * @param {string} data + * @return {string} + */ + decodeBase64(encoded: string): string { + if (this.isCommonJS()) { + return new Buffer(encoded, 'base64').toString('ascii'); + } else { + return window.atob(encoded); + } + }, + /** * Creates an object from a keys array and a values array. * @param {any[]} keys @@ -268,6 +282,7 @@ const _: QuerybaseUtils = { * querybaseRef.where('age').between(20, 30); */ class Querybase { + INDEX_LENGTH = 3; // read only properties @@ -276,7 +291,7 @@ class Querybase { // Returns a read-only set of indexes indexOn: () => string[]; // the key of the Database ref - getKey: () => string; + key: string; // the set of indexOn keys base64 encoded private encodedKeys: () => string[]; @@ -289,11 +304,12 @@ class Querybase { // Check for constructor params and throw if not provided this._assertFirebaseRef(ref); this._assertIndexes(indexOn); + this._assertIndexLength(indexOn); this.ref = () => ref; this.indexOn = () => indexOn.sort(_.lexicographicallySort); /* istanbul ignore next */ - this.getKey = () => this.ref().getKey(); + this.key = this.ref().key; this.encodedKeys = () => this.encodeKeys(this.indexOn()); } @@ -302,23 +318,34 @@ class Querybase { * @parameter {Firebase} * @return {void} */ - private _assertFirebaseRef(ref) { + private _assertFirebaseRef(ref: Firebase) { if (ref === null || ref === undefined || !ref.on) { throw new Error(`No Firebase Database Reference provided in the Querybase constructor.`); } } /** - * Check for indexes. Throw and exception if not provided. + * Check for indexes. Throw an exception if not provided. * @param {string[]} indexes * @return {void} */ - private _assertIndexes(indexes) { + private _assertIndexes(indexes: any[]) { if (indexes === null || indexes === undefined) { throw new Error(`No indexes provided in the Querybase constructor. Querybase uses the indexOn() getter to create the composite queries for the where() method.`); } } + /** + * Check for indexes length. Throw and exception if greater than the INDEX_LENGTH value. + * @param {string[]} indexes + * @return {void} + */ + private _assertIndexLength(indexes: any[]) { + if (indexes.length > this.INDEX_LENGTH) { + throw new Error(`Querybase supports only ${this.INDEX_LENGTH} indexes for multiple querying.`) + } + } + /** * Save data to the realtime database with composite keys * @param {any} data diff --git a/tests/unit/querybase.spec.js b/tests/unit/querybase.spec.js index f6bb62e..4af07df 100644 --- a/tests/unit/querybase.spec.js +++ b/tests/unit/querybase.spec.js @@ -54,14 +54,20 @@ describe('Querybase', () => { const errorWrapper = () => new Querybase(ref); expect(errorWrapper).to.throw(Error); }); + + it('should throw if more than 3 indexes are provided', () => { + const fourIndexes = ['color', 'height', 'weight', 'location']; + const errorWrapper = () => new Querybase(ref, fourIndexes); + expect(errorWrapper).to.throw(Error); + }); }); - describe('getKey', () => { + describe('key property', () => { it('should throw if no indexes are provided', () => { const querybaseRef = querybase.ref(ref, ['name', 'age']); - const key = querybaseRef.getKey(); + const key = querybaseRef.key; assert.equal(key, 'items'); }); diff --git a/typings/firebase/firebase.d.ts b/typings/firebase/firebase.d.ts index f7a48d8..0ad65e8 100644 --- a/typings/firebase/firebase.d.ts +++ b/typings/firebase/firebase.d.ts @@ -47,7 +47,7 @@ interface FirebaseDataSnapshot { /** * Gets the key name of the location that generated this DataSnapshot. */ - getKey(): string; + key: string; /** * @deprecated Use key() instead. * Gets the key name of the location that generated this DataSnapshot. @@ -238,7 +238,7 @@ interface Firebase extends FirebaseQuery { /** * Returns the last token in a Firebase location. */ - getKey(): string; + key: string; /** * @deprecated Use key() instead. * Returns the last token in a Firebase location.