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 1caded4..de3b0b9 100644 --- a/src/querybase.ts +++ b/src/querybase.ts @@ -27,6 +27,11 @@ 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; + decodeBase64(encoded: string): string; } const _: QuerybaseUtils = { @@ -141,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 @@ -160,7 +178,73 @@ 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 sortedKeys = keys.sort(this.lexicographicallySort); + + sortedKeys.forEach((key) => { + let index = indexOfKeys[key]; + 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 +255,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({ @@ -198,6 +282,7 @@ const _: QuerybaseUtils = { * querybaseRef.where('age').between(20, 30); */ class Querybase { + INDEX_LENGTH = 3; // read only properties @@ -206,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[]; @@ -219,36 +304,48 @@ 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; + this.indexOn = () => indexOn.sort(_.lexicographicallySort); /* istanbul ignore next */ - this.getKey = () => this.ref().getKey(); + this.key = this.ref().key; this.encodedKeys = () => this.encodeKeys(this.indexOn()); } /** - * 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) { + private _assertFirebaseRef(ref: Firebase) { 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.`); } } /** - * 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 @@ -322,9 +419,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(); @@ -391,7 +492,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); @@ -400,7 +501,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 @@ -490,13 +591,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 fb6eb41..e8ba625 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,66 @@ 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); + assert.deepEqual({ 'zed': 0, 'name': 1, 'time': 2 }, indexOfKeys); + + }); + + }); + + describe('createSortedObject', () => { + + it('should create a sorted object', () => { + const sortedRecord = _.createSortedObject(smallRecordKeys, smallRecordValues); + assert.deepEqual(_.keys(sortedRecord), _.keys(sortedSmallRecord)); + }) + + it('should create a sorted object', () => { + const sortedRecord = _.createSortedObject(mediumRecordKeys, mediumRecordValues); + assert.deepEqual(_.keys(sortedMediumRecord), _.keys(sortedRecord)); + }) + + }); + + describe('sortObjectLexicographically', () => { + + it('sort an object lexicographically', () => { + const sortedRecord = _.sortObjectLexicographically(smallRecord); + assert.deepEqual(_.keys(sortedSmallRecord), _.keys(sortedRecord)); + }); + + it('sort an object lexicographically', () => { + const sortedRecord = _.sortObjectLexicographically(mediumRecord); + assert.deepEqual(_.keys(sortedMediumRecord), _.keys(sortedRecord)); + }); + + }); + }); \ No newline at end of file diff --git a/tests/unit/querybase.spec.js b/tests/unit/querybase.spec.js index 87a326b..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'); }); @@ -177,9 +183,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 +207,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 +237,7 @@ describe('Querybase', () => { }); // encoded - // { color: 'Blue', height: '67 } + // { height: '67', color: 'Blue' } it('should encode a QueryPredicate for multiple criteria', () => { const expectedPredicate = { @@ -240,7 +245,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 +257,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 +294,6 @@ describe('Querybase', () => { 'querybase~~aGVpZ2h0fn53ZWlnaHQ=': 'querybase~~Njd+fjEzMA==' }; - - const encodedIndex = queryRef._encodeCompositeIndex(expectedIndex); assert.deepEqual(expectedEncodedIndex, encodedIndex); }); 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.