Skip to content
This repository was archived by the owner on Jul 16, 2020. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
});

});

});
Loading