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.