Skip to content
This repository has been archived by the owner on Jul 16, 2020. It is now read-only.

Commit

Permalink
Merge pull request #13 from davideast/key-sort
Browse files Browse the repository at this point in the history
fix(indexes): Limit indexes to three for sanity
  • Loading branch information
davideast committed Jun 20, 2016
2 parents 7695ccd + 24052ae commit db2ca5d
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 41 deletions.
21 changes: 10 additions & 11 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -47,8 +47,7 @@
queryRef.push({
name: name,
age: age,
location: location,
weight: weight
location: location
});
}

Expand Down
134 changes: 118 additions & 16 deletions src/querybase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
Expand All @@ -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);
}

}

/**
Expand All @@ -171,8 +255,8 @@ const _: QuerybaseUtils = {
*
* @example
* // Querybase for multiple equivalency
* const firebaseRef = new Firebase('<my-app>/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({
Expand All @@ -198,6 +282,7 @@ const _: QuerybaseUtils = {
* querybaseRef.where('age').between(20, 30);
*/
class Querybase {
INDEX_LENGTH = 3;

// read only properties

Expand All @@ -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[];

Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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;
}

/**
Expand Down
98 changes: 98 additions & 0 deletions tests/unit/querybase-utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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; })

Expand Down Expand Up @@ -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));
});

});

});

0 comments on commit db2ca5d

Please sign in to comment.