diff --git a/.changeset/hip-glasses-grin.md b/.changeset/hip-glasses-grin.md new file mode 100644 index 00000000000..b3583102f9f --- /dev/null +++ b/.changeset/hip-glasses-grin.md @@ -0,0 +1,5 @@ +--- +'@firebase/database': minor +--- + +Add `startAfter` and `endBefore` filters for paginating RTDB queries. diff --git a/packages/database/package.json b/packages/database/package.json index 80d8bd75875..d5e93598f57 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -7,7 +7,9 @@ "browser": "dist/index.esm.js", "module": "dist/index.esm.js", "esm2017": "dist/index.esm2017.js", - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", diff --git a/packages/database/src/api/Query.ts b/packages/database/src/api/Query.ts index f4ef30e62a1..42c965489b4 100644 --- a/packages/database/src/api/Query.ts +++ b/packages/database/src/api/Query.ts @@ -101,7 +101,10 @@ export class Query { 'or equalTo() must be a string.'; if (params.hasStart()) { const startName = params.getIndexStartName(); - if (startName !== MIN_NAME) { + if ( + startName !== MIN_NAME && + !(params.hasStartAfter() && startName === MAX_NAME) + ) { throw new Error(tooManyArgsError); } else if (typeof startNode !== 'string') { throw new Error(wrongArgTypeError); @@ -109,7 +112,10 @@ export class Query { } if (params.hasEnd()) { const endName = params.getIndexEndName(); - if (endName !== MAX_NAME) { + if ( + endName !== MAX_NAME && + !(params.hasEndBefore() && endName === MIN_NAME) + ) { throw new Error(tooManyArgsError); } else if (typeof endNode !== 'string') { throw new Error(wrongArgTypeError); @@ -526,6 +532,28 @@ export class Query { value = null; name = null; } + + return new Query(this.repo, this.path, newParams, this.orderByCalled_); + } + + startAfter( + value: number | string | boolean | null = null, + name?: string | null + ): Query { + validateArgCount('Query.startAfter', 0, 2, arguments.length); + validateFirebaseDataArg('Query.startAfter', 1, value, this.path, false); + validateKey('Query.startAfter', 2, name, true); + + const newParams = this.queryParams_.startAfter(value, name); + Query.validateLimit_(newParams); + Query.validateQueryEndpoints_(newParams); + if (this.queryParams_.hasStart()) { + throw new Error( + 'Query.startAfter: Starting point was already set (by another call to startAt, startAfter ' + + 'or equalTo).' + ); + } + return new Query(this.repo, this.path, newParams, this.orderByCalled_); } @@ -547,7 +575,28 @@ export class Query { Query.validateQueryEndpoints_(newParams); if (this.queryParams_.hasEnd()) { throw new Error( - 'Query.endAt: Ending point was already set (by another call to endAt or ' + + 'Query.endAt: Ending point was already set (by another call to endAt, endBefore, or ' + + 'equalTo).' + ); + } + + return new Query(this.repo, this.path, newParams, this.orderByCalled_); + } + + endBefore( + value: number | string | boolean | null = null, + name?: string | null + ): Query { + validateArgCount('Query.endBefore', 0, 2, arguments.length); + validateFirebaseDataArg('Query.endBefore', 1, value, this.path, false); + validateKey('Query.endBefore', 2, name, true); + + const newParams = this.queryParams_.endBefore(value, name); + Query.validateLimit_(newParams); + Query.validateQueryEndpoints_(newParams); + if (this.queryParams_.hasEnd()) { + throw new Error( + 'Query.endBefore: Ending point was already set (by another call to endAt, endBefore, or ' + 'equalTo).' ); } diff --git a/packages/database/src/core/util/NextPushId.ts b/packages/database/src/core/util/NextPushId.ts index 8296e4f4169..a76697e77ec 100644 --- a/packages/database/src/core/util/NextPushId.ts +++ b/packages/database/src/core/util/NextPushId.ts @@ -16,6 +16,23 @@ */ import { assert } from '@firebase/util'; +import { + tryParseInt, + MAX_NAME, + MIN_NAME, + INTEGER_32_MIN, + INTEGER_32_MAX +} from '../util/util'; + +// Modeled after base64 web-safe chars, but ordered by ASCII. +const PUSH_CHARS = + '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'; + +const MIN_PUSH_CHAR = '-'; + +const MAX_PUSH_CHAR = 'z'; + +const MAX_KEY_LEN = 786; /** * Fancy ID generator that creates 20-character string identifiers with the @@ -32,10 +49,6 @@ import { assert } from '@firebase/util'; * in the case of a timestamp collision). */ export const nextPushId = (function () { - // Modeled after base64 web-safe chars, but ordered by ASCII. - const PUSH_CHARS = - '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'; - // Timestamp of last push, used to prevent local collisions if you push twice // in one ms. let lastPushTime = 0; @@ -82,3 +95,83 @@ export const nextPushId = (function () { return id; }; })(); + +export const successor = function (key: string) { + if (key === '' + INTEGER_32_MAX) { + // See https://firebase.google.com/docs/database/web/lists-of-data#data-order + return MIN_PUSH_CHAR; + } + const keyAsInt: number = tryParseInt(key); + if (keyAsInt != null) { + return '' + (keyAsInt + 1); + } + const next = new Array(key.length); + + for (let i = 0; i < next.length; i++) { + next[i] = key.charAt(i); + } + + if (next.length < MAX_KEY_LEN) { + next.push(MIN_PUSH_CHAR); + return next.join(''); + } + + let i = next.length - 1; + + while (i >= 0 && next[i] === MAX_PUSH_CHAR) { + i--; + } + + // `successor` was called on the largest possible key, so return the + // MAX_NAME, which sorts larger than all keys. + if (i === -1) { + return MAX_NAME; + } + + const source = next[i]; + const sourcePlusOne = PUSH_CHARS.charAt(PUSH_CHARS.indexOf(source) + 1); + next[i] = sourcePlusOne; + + return next.slice(0, i + 1).join(''); +}; + +// `key` is assumed to be non-empty. +export const predecessor = function (key: string) { + if (key === '' + INTEGER_32_MIN) { + return MIN_NAME; + } + const keyAsInt: number = tryParseInt(key); + if (keyAsInt != null) { + return '' + (keyAsInt - 1); + } + const next = new Array(key.length); + for (let i = 0; i < next.length; i++) { + next[i] = key.charAt(i); + } + // If `key` ends in `MIN_PUSH_CHAR`, the largest key lexicographically + // smaller than `key`, is `key[0:key.length - 1]`. The next key smaller + // than that, `predecessor(predecessor(key))`, is + // + // `key[0:key.length - 2] + (key[key.length - 1] - 1) + \ + // { MAX_PUSH_CHAR repeated MAX_KEY_LEN - (key.length - 1) times } + // + // analogous to increment/decrement for base-10 integers. + // + // This works because lexigographic comparison works character-by-character, + // using length as a tie-breaker if one key is a prefix of the other. + if (next[next.length - 1] === MIN_PUSH_CHAR) { + if (next.length === 1) { + // See https://firebase.google.com/docs/database/web/lists-of-data#orderbykey + return '' + INTEGER_32_MAX; + } + delete next[next.length - 1]; + return next.join(''); + } + // Replace the last character with it's immediate predecessor, and + // fill the suffix of the key with MAX_PUSH_CHAR. This is the + // lexicographically largest possible key smaller than `key`. + next[next.length - 1] = PUSH_CHARS.charAt( + PUSH_CHARS.indexOf(next[next.length - 1]) - 1 + ); + return next.join('') + MAX_PUSH_CHAR.repeat(MAX_KEY_LEN - next.length); +}; diff --git a/packages/database/src/core/util/util.ts b/packages/database/src/core/util/util.ts index 86acf13ae86..926f10520c1 100644 --- a/packages/database/src/core/util/util.ts +++ b/packages/database/src/core/util/util.ts @@ -551,6 +551,16 @@ export const errorForServerCode = function (code: string, query: Query): Error { */ export const INTEGER_REGEXP_ = new RegExp('^-?(0*)\\d{1,10}$'); +/** + * For use in keys, the minimum possible 32-bit integer. + */ +export const INTEGER_32_MIN = -2147483648; + +/** + * For use in kyes, the maximum possible 32-bit integer. + */ +export const INTEGER_32_MAX = 2147483647; + /** * If the string contains a 32-bit integer, return it. Else return null. * @param {!string} str @@ -559,7 +569,7 @@ export const INTEGER_REGEXP_ = new RegExp('^-?(0*)\\d{1,10}$'); export const tryParseInt = function (str: string): number | null { if (INTEGER_REGEXP_.test(str)) { const intVal = Number(str); - if (intVal >= -2147483648 && intVal <= 2147483647) { + if (intVal >= INTEGER_32_MIN && intVal <= INTEGER_32_MAX) { return intVal; } } diff --git a/packages/database/src/core/view/QueryParams.ts b/packages/database/src/core/view/QueryParams.ts index 04342789d58..850b173a0f7 100644 --- a/packages/database/src/core/view/QueryParams.ts +++ b/packages/database/src/core/view/QueryParams.ts @@ -17,6 +17,7 @@ import { assert, stringify } from '@firebase/util'; import { MIN_NAME, MAX_NAME } from '../util/util'; +import { predecessor, successor } from '../util/NextPushId'; import { KEY_INDEX } from '../snap/indexes/KeyIndex'; import { PRIORITY_INDEX } from '../snap/indexes/PriorityIndex'; import { VALUE_INDEX } from '../snap/indexes/ValueIndex'; @@ -37,8 +38,10 @@ export class QueryParams { private limitSet_ = false; private startSet_ = false; private startNameSet_ = false; + private startAfterSet_ = false; private endSet_ = false; private endNameSet_ = false; + private endBeforeSet_ = false; private limit_ = 0; private viewFrom_ = ''; @@ -98,6 +101,14 @@ export class QueryParams { return this.startSet_; } + hasStartAfter(): boolean { + return this.startAfterSet_; + } + + hasEndBefore(): boolean { + return this.endBeforeSet_; + } + /** * @return {boolean} True if it would return from left. */ @@ -277,6 +288,18 @@ export class QueryParams { return newParams; } + startAfter(indexValue: unknown, key?: string | null): QueryParams { + let childKey: string; + if (key == null) { + childKey = MAX_NAME; + } else { + childKey = successor(key); + } + const params: QueryParams = this.startAt(indexValue, childKey); + params.startAfterSet_ = true; + return params; + } + /** * @param {*} indexValue * @param {?string=} key @@ -299,6 +322,18 @@ export class QueryParams { return newParams; } + endBefore(indexValue: unknown, key?: string | null): QueryParams { + let childKey: string; + if (key == null) { + childKey = MIN_NAME; + } else { + childKey = predecessor(key); + } + const params: QueryParams = this.endAt(indexValue, childKey); + params.endBeforeSet_ = true; + return params; + } + /** * @param {!Index} index * @return {!QueryParams} diff --git a/packages/database/test/order_by.test.ts b/packages/database/test/order_by.test.ts index da3a2da0da9..f857ebe3b5c 100644 --- a/packages/database/test/order_by.test.ts +++ b/packages/database/test/order_by.test.ts @@ -352,6 +352,120 @@ describe('.orderBy tests', () => { expect(addedPrevNames).to.deep.equal(expectedPrevNames); }); + it('startAfter / endAt works on value index', () => { + const ref = getRandomNode() as Reference; + + const initial = { + alex: 60, + rob: 56, + vassili: 55.5, + tony: 52, + greg: 52 + }; + + const expectedOrder = ['vassili', 'rob']; + const expectedPrevNames = [null, 'vassili']; + + const valueOrder = []; + const addedOrder = []; + const addedPrevNames = []; + + const orderedRef = ref.orderByValue().startAfter(52, 'tony').endAt(59); + + orderedRef.on('value', snap => { + snap.forEach(childSnap => { + valueOrder.push(childSnap.key); + }); + }); + + orderedRef.on('child_added', (snap, prevName) => { + addedOrder.push(snap.key); + addedPrevNames.push(prevName); + }); + + ref.set(initial); + + expect(addedOrder).to.deep.equal(expectedOrder); + expect(valueOrder).to.deep.equal(expectedOrder); + expect(addedPrevNames).to.deep.equal(expectedPrevNames); + }); + + it('startAt / endBefore works on value index', () => { + const ref = getRandomNode() as Reference; + + const initial = { + alex: 60, + rob: 56, + vassili: 55.5, + tony: 52, + greg: 52 + }; + + const expectedOrder = ['tony', 'vassili', 'rob']; + const expectedPrevNames = [null, 'tony', 'vassili']; + + const valueOrder = []; + const addedOrder = []; + const addedPrevNames = []; + + const orderedRef = ref.orderByValue().startAt(52, 'tony').endBefore(60); + + orderedRef.on('value', snap => { + snap.forEach(childSnap => { + valueOrder.push(childSnap.key); + }); + }); + + orderedRef.on('child_added', (snap, prevName) => { + addedOrder.push(snap.key); + addedPrevNames.push(prevName); + }); + + ref.set(initial); + + expect(addedOrder).to.deep.equal(expectedOrder); + expect(valueOrder).to.deep.equal(expectedOrder); + expect(addedPrevNames).to.deep.equal(expectedPrevNames); + }); + + it('startAfter / endBefore works on value index', () => { + const ref = getRandomNode() as Reference; + + const initial = { + alex: 60, + rob: 56, + vassili: 55.5, + tony: 52, + greg: 52 + }; + + const expectedOrder = ['vassili', 'rob']; + const expectedPrevNames = [null, 'vassili']; + + const valueOrder = []; + const addedOrder = []; + const addedPrevNames = []; + + const orderedRef = ref.orderByValue().startAfter(52, 'tony').endBefore(60); + + orderedRef.on('value', snap => { + snap.forEach(childSnap => { + valueOrder.push(childSnap.key); + }); + }); + + orderedRef.on('child_added', (snap, prevName) => { + addedOrder.push(snap.key); + addedPrevNames.push(prevName); + }); + + ref.set(initial); + + expect(addedOrder).to.deep.equal(expectedOrder); + expect(valueOrder).to.deep.equal(expectedOrder); + expect(addedPrevNames).to.deep.equal(expectedPrevNames); + }); + it('Removing default listener removes non-default listener that loads all data', done => { const ref = getRandomNode() as Reference; diff --git a/packages/database/test/pushid.test.ts b/packages/database/test/pushid.test.ts new file mode 100644 index 00000000000..73469c2a800 --- /dev/null +++ b/packages/database/test/pushid.test.ts @@ -0,0 +1,61 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { predecessor, successor } from '../src/core/util/NextPushId'; +import { + INTEGER_32_MIN, + INTEGER_32_MAX, + MIN_NAME, + MAX_NAME +} from '../src/core/util/util'; + +// Copied from src/core/util/NextPushId.ts +const MIN_PUSH_CHAR = '-'; + +const MAX_PUSH_CHAR = 'z'; + +const MAX_KEY_LEN = 786; + +describe('Push ID tests', () => { + it('predecessor special values', () => { + expect(predecessor('' + MIN_PUSH_CHAR)).to.equal('' + INTEGER_32_MAX); + expect(predecessor('' + INTEGER_32_MIN)).to.equal('' + MIN_NAME); + }); + + it('predecessor basic test', () => { + expect(predecessor('abc')).to.equal( + 'abb' + MAX_PUSH_CHAR.repeat(MAX_KEY_LEN - 'abc'.length) + ); + expect(predecessor('abc' + MIN_PUSH_CHAR)).to.equal('abc'); + }); + + it('successor special values', () => { + expect(successor('' + INTEGER_32_MAX)).to.equal(MIN_PUSH_CHAR); + expect(successor(MAX_PUSH_CHAR.repeat(MAX_KEY_LEN))).to.equal(MAX_NAME); + }); + + it('successor basic tests', () => { + expect(successor('abc')).to.equal('abc' + MIN_PUSH_CHAR); + expect( + successor('abc' + MAX_PUSH_CHAR.repeat(MAX_KEY_LEN - 'abc'.length)) + ).to.equal('abd'); + expect(successor('abc' + MIN_PUSH_CHAR)).to.equal( + 'abc' + MIN_PUSH_CHAR.repeat(2) + ); + }); +}); diff --git a/packages/database/test/query.test.ts b/packages/database/test/query.test.ts index 5d4dbd1c50e..426df605ca9 100644 --- a/packages/database/test/query.test.ts +++ b/packages/database/test/query.test.ts @@ -27,6 +27,7 @@ import { EventAccumulatorFactory } from './helpers/EventAccumulator'; import * as _ from 'lodash'; +import { INTEGER_32_MIN, INTEGER_32_MAX } from '../src/core/util/util'; use(chaiAsPromised); @@ -50,13 +51,20 @@ describe('Query Tests', () => { path.limitToLast(10); path.startAt('199').limitToFirst(10); + path.startAfter('199').limitToFirst(10); path.startAt('199', 'test').limitToFirst(10); + path.startAfter('199', 'test').limitToFirst(10); path.endAt('199').limitToLast(1); + path.endBefore('199').limitToLast(1); path.startAt('50', 'test').endAt('100', 'tree'); + path.startAfter('50', 'test').endAt('100', 'tree'); path.startAt('4').endAt('10'); + path.startAfter('4').endAt('10'); path.startAt().limitToFirst(10); + path.startAfter().limitToFirst(10); path.endAt().limitToLast(10); path.orderByKey().startAt('foo'); + path.orderByKey().startAfter('foo'); path.orderByKey().endAt('foo'); path.orderByKey().equalTo('foo'); path.orderByChild('child'); @@ -138,6 +146,9 @@ describe('Query Tests', () => { expect(() => { path.orderByPriority().startAt(true); }).to.throw(); + expect(() => { + path.orderByPriority().startAfter(true); + }).to.throw(); expect(() => { path.orderByPriority().endAt(false); }).to.throw(); @@ -165,6 +176,9 @@ describe('Query Tests', () => { expect(() => { (path as any).orderByChild('foo').startAt({ a: 1 }); }).to.throw(); + expect(() => { + (path as any).orderByChild('foo').startAfter({ a: 1 }); + }).to.throw(); expect(() => { (path as any).orderByChild('foo').endAt({ a: 1 }); }).to.throw(); @@ -174,9 +188,21 @@ describe('Query Tests', () => { expect(() => { path.startAt('foo').startAt('foo'); }).to.throw(); + expect(() => { + path.startAfter('foo').startAfter('foo'); + }).to.throw(); + expect(() => { + path.startAt('foo').startAfter('foo'); + }).to.throw(); + expect(() => { + path.startAfter('foo').startAt('foo'); + }).to.throw(); expect(() => { path.startAt('foo').equalTo('foo'); }).to.throw(); + expect(() => { + path.startAfter('foo').equalTo('foo'); + }).to.throw(); expect(() => { path.endAt('foo').endAt('foo'); }).to.throw(); @@ -186,6 +212,9 @@ describe('Query Tests', () => { expect(() => { path.equalTo('foo').startAt('foo'); }).to.throw(); + expect(() => { + path.equalTo('foo').startAfter('foo'); + }).to.throw(); expect(() => { path.equalTo('foo').endAt('foo'); }).to.throw(); @@ -195,6 +224,9 @@ describe('Query Tests', () => { expect(() => { path.orderByKey().startAt('foo', 'foo'); }).to.throw(); + expect(() => { + path.orderByKey().startAfter('foo', 'foo'); + }).to.throw(); expect(() => { path.orderByKey().endAt('foo', 'foo'); }).to.throw(); @@ -204,12 +236,21 @@ describe('Query Tests', () => { expect(() => { path.orderByKey().startAt(1); }).to.throw(); + expect(() => { + path.orderByKey().startAfter(1); + }).to.throw(); expect(() => { path.orderByKey().startAt(true); }).to.throw(); + expect(() => { + path.orderByKey().startAfter(true); + }).to.throw(); expect(() => { path.orderByKey().startAt(null); }).to.throw(); + expect(() => { + path.orderByKey().startAfter(null); + }).to.throw(); expect(() => { path.orderByKey().endAt(1); }).to.throw(); @@ -231,6 +272,9 @@ describe('Query Tests', () => { expect(() => { path.startAt('foo', 'foo').orderByKey(); }).to.throw(); + expect(() => { + path.startAfter('foo', 'foo').orderByKey(); + }).to.throw(); expect(() => { path.endAt('foo', 'foo').orderByKey(); }).to.throw(); @@ -240,9 +284,15 @@ describe('Query Tests', () => { expect(() => { path.startAt(1).orderByKey(); }).to.throw(); + expect(() => { + path.startAfter(1).orderByKey(); + }).to.throw(); expect(() => { path.startAt(true).orderByKey(); }).to.throw(); + expect(() => { + path.startAfter(true).orderByKey(); + }).to.throw(); expect(() => { path.endAt(1).orderByKey(); }).to.throw(); @@ -284,6 +334,27 @@ describe('Query Tests', () => { }); }); + it('Passing invalidKeys to startAfter throws.', () => { + const f = getRandomNode() as Reference; + const badKeys = [ + '.test', + 'test.', + 'fo$o', + '[what', + 'ever]', + 'ha#sh', + '/thing', + 'th/ing', + 'thing/' + ]; + // Changed from basic array iteration to avoid closure issues accessing mutable state + _.each(badKeys, badKey => { + expect(() => { + f.startAfter(null, badKey); + }).to.throw(); + }); + }); + it('Passing invalid paths to orderBy throws', () => { const ref = getRandomNode() as Reference; expect(() => { @@ -308,18 +379,33 @@ describe('Query Tests', () => { expect(queryId(path.startAt('pri', 'name'))).to.equal( '{"sn":"name","sp":"pri"}' ); + expect(queryId(path.startAfter('pri', 'name'))).to.equal( + '{"sn":"name-","sp":"pri"}' + ); expect(queryId(path.startAt('spri').endAt('epri'))).to.equal( '{"ep":"epri","sp":"spri"}' ); + expect(queryId(path.startAfter('spri').endAt('epri'))).to.equal( + '{"ep":"epri","sn":"[MAX_NAME]","sp":"spri"}' + ); expect( queryId(path.startAt('spri', 'sname').endAt('epri', 'ename')) ).to.equal('{"en":"ename","ep":"epri","sn":"sname","sp":"spri"}'); + expect( + queryId(path.startAfter('spri', 'sname').endAt('epri', 'ename')) + ).to.equal('{"en":"ename","ep":"epri","sn":"sname-","sp":"spri"}'); expect(queryId(path.startAt('pri').limitToFirst(100))).to.equal( '{"l":100,"sp":"pri","vf":"l"}' ); + expect(queryId(path.startAfter('pri').limitToFirst(100))).to.equal( + '{"l":100,"sn":"[MAX_NAME]","sp":"pri","vf":"l"}' + ); expect(queryId(path.startAt('bar').orderByChild('foo'))).to.equal( '{"i":"foo","sp":"bar"}' ); + expect(queryId(path.startAfter('bar').orderByChild('foo'))).to.equal( + '{"i":"foo","sn":"[MAX_NAME]","sp":"bar"}' + ); }); it('Passing invalid queries to isEqual throws', () => { @@ -391,10 +477,15 @@ describe('Query Tests', () => { const childQueryOrderedByTimestamp = childRef.orderByChild('timestamp'); const childQueryStartAt1 = childQueryOrderedByTimestamp.startAt(1); const childQueryStartAt2 = childQueryOrderedByTimestamp.startAt(2); + const childQueryStartAfter1 = childQueryOrderedByTimestamp.startAfter(1); + const childQueryStartAfter2 = childQueryOrderedByTimestamp.startAfter(2); const childQueryEndAt2 = childQueryOrderedByTimestamp.endAt(2); const childQueryStartAt1EndAt2 = childQueryOrderedByTimestamp .startAt(1) .endAt(2); + const childQueryStartAfter1EndAt2 = childQueryOrderedByTimestamp + .startAfter(1) + .endAt(2); // Equivalent queries expect(childRef.isEqual(childQueryLast25.ref), 'Query.isEqual - 9').to.be @@ -436,14 +527,22 @@ describe('Query Tests', () => { expect(childQueryStartAt1.isEqual(childQueryStartAt2), 'Query.isEqual - 18') .to.be.false; expect( - childQueryStartAt1.isEqual(childQueryStartAt1EndAt2), + childQueryStartAfter1.isEqual(childQueryStartAfter2), 'Query.isEqual - 19' ).to.be.false; - expect(childQueryEndAt2.isEqual(childQueryStartAt2), 'Query.isEqual - 20') + expect( + childQueryStartAt1.isEqual(childQueryStartAt1EndAt2), + 'Query.isEqual - 20' + ).to.be.false; + expect( + childQueryStartAfter1.isEqual(childQueryStartAfter1EndAt2), + 'Query.isEqual - 21' + ).to.be.false; + expect(childQueryEndAt2.isEqual(childQueryStartAt2), 'Query.isEqual - 22') .to.be.false; expect( childQueryEndAt2.isEqual(childQueryStartAt1EndAt2), - 'Query.isEqual - 21' + 'Query.isEqual - 23' ).to.be.false; }); @@ -769,6 +868,45 @@ describe('Query Tests', () => { ); }); + it('Set various limits with a startAfter name, ensure resulting data is correct.', async () => { + const node = getRandomNode() as Reference; + + await node.set({ a: 1, b: 2, c: 3, d: 4 }); + + const tasks: TaskList = [ + // Using the priority index here, so startAfter() skips everything. + [node.startAfter().limitToFirst(1), null], + [node.startAfter(null, 'c').limitToFirst(1), { d: 4 }], + [node.startAfter(null, 'b').limitToFirst(1), { c: 3 }], + [node.startAfter(null, 'b').limitToFirst(2), { c: 3, d: 4 }], + [node.startAfter(null, 'b').limitToFirst(3), { c: 3, d: 4 }], + [node.startAfter(null, 'b').limitToLast(1), { d: 4 }], + [node.startAfter(null, 'b').limitToLast(1), { d: 4 }], + [node.startAfter(null, 'b').limitToLast(2), { c: 3, d: 4 }], + [node.startAfter(null, 'b').limitToLast(3), { c: 3, d: 4 }], + [node.limitToFirst(1).startAfter(null, 'c'), { d: 4 }], + [node.limitToFirst(1).startAfter(null, 'b'), { c: 3 }], + [node.limitToFirst(2).startAfter(null, 'b'), { c: 3, d: 4 }], + [node.limitToFirst(3).startAfter(null, 'b'), { c: 3, d: 4 }], + [node.limitToLast(1).startAfter(null, 'b'), { d: 4 }], + [node.limitToLast(1).startAfter(null, 'b'), { d: 4 }], + [node.limitToLast(2).startAfter(null, 'b'), { c: 3, d: 4 }], + [node.limitToLast(3).startAfter(null, 'b'), { c: 3, d: 4 }] + ]; + + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + it('Set various limits with a endAt name, ensure resulting data is correct.', async () => { const node = getRandomNode() as Reference; @@ -913,6 +1051,33 @@ describe('Query Tests', () => { expect(removed).to.equal('b '); }); + it('Set startAfter and limit, ensure child_removed and child_added events are fired when limit is hit.', () => { + const node = getRandomNode() as Reference; + + let added = '', + removed = ''; + node + .startAfter(null, 'a') + .limitToFirst(2) + .on('child_added', snap => { + added += snap.key + ' '; + }); + node + .startAfter(null, 'a') + .limitToFirst(2) + .on('child_removed', snap => { + removed += snap.key + ' '; + }); + node.set({ a: 1, b: 2, c: 3 }); + expect(added).to.equal('b c '); + expect(removed).to.equal(''); + + added = ''; + node.child('aa').set(4); + expect(added).to.equal('aa '); + expect(removed).to.equal('c '); + }); + it('Set start and limit, ensure child_removed and child_added events are fired when limit is hit, using server data', async () => { const node = getRandomNode() as Reference; @@ -947,6 +1112,40 @@ describe('Query Tests', () => { expect(removed).to.equal('b '); }); + it('Set start and limit, ensure child_removed and child_added events are fired when limit is hit, using server data', async () => { + const node = getRandomNode() as Reference; + + await node.set({ a: 1, b: 2, c: 3 }); + const ea = EventAccumulatorFactory.waitsForCount(2); + + let added = '', + removed = ''; + node + .startAfter(null, 'a') + .limitToFirst(2) + .on('child_added', snap => { + added += snap.key + ' '; + ea.addEvent(); + }); + node + .startAfter(null, 'a') + .limitToFirst(2) + .on('child_removed', snap => { + removed += snap.key + ' '; + }); + + await ea.promise; + + expect(added).to.equal('b c '); + expect(removed).to.equal(''); + + added = ''; + await node.child('bb').set(4); + + expect(added).to.equal('bb '); + expect(removed).to.equal('c '); + }); + it("Set start and limit, ensure child_added events are fired when limit isn't hit yet.", () => { const node = getRandomNode() as Reference; @@ -974,6 +1173,33 @@ describe('Query Tests', () => { expect(removed).to.equal(''); }); + it("Set startAfter and limit, ensure child_added events are fired when limit isn't hit yet.", () => { + const node = getRandomNode() as Reference; + + let added = '', + removed = ''; + node + .startAfter(null, 'a') + .limitToFirst(2) + .on('child_added', snap => { + added += snap.key + ' '; + }); + node + .startAfter(null, 'a') + .limitToFirst(2) + .on('child_removed', snap => { + removed += snap.key + ' '; + }); + node.set({ c: 3 }); + expect(added).to.equal('c '); + expect(removed).to.equal(''); + + added = ''; + node.child('b').set(4); + expect(added).to.equal('b '); + expect(removed).to.equal(''); + }); + it("Set start and limit, ensure child_added events are fired when limit isn't hit yet, using server data", async () => { const node = getRandomNode() as Reference; @@ -1009,6 +1235,41 @@ describe('Query Tests', () => { expect(removed).to.equal(''); }); + it("Set startAfter and limit, ensure child_added events are fired when limit isn't hit yet, using server data", async () => { + const node = getRandomNode() as Reference; + + await node.set({ c: 3 }); + + const ea = EventAccumulatorFactory.waitsForCount(1); + + let added = ''; + let removed = ''; + node + .startAfter(null, 'a') + .limitToFirst(2) + .on('child_added', snap => { + added += snap.key + ' '; + ea.addEvent(); + }); + node + .startAfter(null, 'a') + .limitToFirst(2) + .on('child_removed', snap => { + removed += snap.key + ' '; + }); + + await ea.promise; + + expect(added).to.equal('c '); + expect(removed).to.equal(''); + + added = ''; + await node.child('b').set(4); + + expect(added).to.equal('b '); + expect(removed).to.equal(''); + }); + it('Set a limit, ensure child_removed and child_added events are fired when limit is satisfied and you remove an item.', async () => { const node = getRandomNode() as Reference; const ea = EventAccumulatorFactory.waitsForCount(1); @@ -1145,9 +1406,15 @@ describe('Query Tests', () => { ); }); - it('Ensure startAt / endAt with priority work with server data.', async () => { + it('Ensure startAfter / endAt with priority works.', async () => { const node = getRandomNode() as Reference; + const tasks: TaskList = [ + [node.startAfter('w').endAt('y'), { b: 2, c: 3 }], + [node.startAfter('w').endAt('x'), { c: 3 }], + [node.startAfter('a').endAt('c'), null] + ]; + await node.set({ a: { '.value': 1, '.priority': 'z' }, b: { '.value': 2, '.priority': 'y' }, @@ -1155,12 +1422,6 @@ describe('Query Tests', () => { d: { '.value': 4, '.priority': 'w' } }); - const tasks: TaskList = [ - [node.startAt('w').endAt('y'), { b: 2, c: 3, d: 4 }], - [node.startAt('w').endAt('w'), { d: 4 }], - [node.startAt('a').endAt('c'), null] - ]; - return Promise.all( tasks.map(async task => { const [query, val] = task; @@ -1174,22 +1435,22 @@ describe('Query Tests', () => { ); }); - it('Ensure startAt / endAt with priority and name works.', async () => { + it('Ensure startAt / endBefore with priority works.', async () => { const node = getRandomNode() as Reference; - await node.set({ - a: { '.value': 1, '.priority': 1 }, - b: { '.value': 2, '.priority': 1 }, - c: { '.value': 3, '.priority': 2 }, - d: { '.value': 4, '.priority': 2 } - }); - const tasks: TaskList = [ - [node.startAt(1, 'a').endAt(2, 'd'), { a: 1, b: 2, c: 3, d: 4 }], - [node.startAt(1, 'b').endAt(2, 'c'), { b: 2, c: 3 }], - [node.startAt(1, 'c').endAt(2), { c: 3, d: 4 }] + [node.startAt('w').endBefore('y'), { c: 3, d: 4 }], + [node.startAt('w').endBefore('x'), { d: 4 }], + [node.startAt('a').endBefore('c'), null] ]; + await node.set({ + a: { '.value': 1, '.priority': 'z' }, + b: { '.value': 2, '.priority': 'y' }, + c: { '.value': 3, '.priority': 'x' }, + d: { '.value': 4, '.priority': 'w' } + }); + return Promise.all( tasks.map(async task => { const [query, val] = task; @@ -1203,20 +1464,22 @@ describe('Query Tests', () => { ); }); - it('Ensure startAt / endAt with priority and name work with server data', async () => { + it('Ensure startAfter / endBefore with priority works.', async () => { const node = getRandomNode() as Reference; - await node.set({ - a: { '.value': 1, '.priority': 1 }, - b: { '.value': 2, '.priority': 1 }, - c: { '.value': 3, '.priority': 2 }, - d: { '.value': 4, '.priority': 2 } - }); const tasks: TaskList = [ - [node.startAt(1, 'a').endAt(2, 'd'), { a: 1, b: 2, c: 3, d: 4 }], - [node.startAt(1, 'b').endAt(2, 'c'), { b: 2, c: 3 }], - [node.startAt(1, 'c').endAt(2), { c: 3, d: 4 }] + [node.startAfter('w').endBefore('z'), { b: 2, c: 3 }], + [node.startAfter('w').endBefore('y'), { c: 3 }], + [node.startAfter('w').endBefore('w'), null] ]; + + await node.set({ + a: { '.value': 1, '.priority': 'z' }, + b: { '.value': 2, '.priority': 'y' }, + c: { '.value': 3, '.priority': 'x' }, + d: { '.value': 4, '.priority': 'w' } + }); + return Promise.all( tasks.map(async task => { const [query, val] = task; @@ -1230,22 +1493,449 @@ describe('Query Tests', () => { ); }); - it('Ensure startAt / endAt with priority and name works (2).', () => { + it('Ensure startAt / endAt with priority work with server data.', async () => { const node = getRandomNode() as Reference; + await node.set({ + a: { '.value': 1, '.priority': 'z' }, + b: { '.value': 2, '.priority': 'y' }, + c: { '.value': 3, '.priority': 'x' }, + d: { '.value': 4, '.priority': 'w' } + }); + const tasks: TaskList = [ - [node.startAt(1, 'c').endAt(2, 'b'), { a: 1, b: 2, c: 3, d: 4 }], - [node.startAt(1, 'd').endAt(2, 'a'), { d: 4, a: 1 }], - [node.startAt(1, 'e').endAt(2), { a: 1, b: 2 }] + [node.startAt('w').endAt('y'), { b: 2, c: 3, d: 4 }], + [node.startAt('w').endAt('w'), { d: 4 }], + [node.startAt('a').endAt('c'), null] ]; - node.set({ - c: { '.value': 3, '.priority': 1 }, - d: { '.value': 4, '.priority': 1 }, - a: { '.value': 1, '.priority': 2 }, - b: { '.value': 2, '.priority': 2 } - }); - + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAfter / endAt with priority work with server data.', async () => { + const node = getRandomNode() as Reference; + + await node.set({ + a: { '.value': 1, '.priority': 'z' }, + b: { '.value': 2, '.priority': 'y' }, + c: { '.value': 3, '.priority': 'x' }, + d: { '.value': 4, '.priority': 'w' } + }); + + const tasks: TaskList = [ + [node.startAfter('w').endAt('y'), { b: 2, c: 3 }], + [node.startAfter('w').endAt('x'), { c: 3 }], + [node.startAfter('a').endAt('c'), null] + ]; + + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAt / endBefore with priority work with server data.', async () => { + const node = getRandomNode() as Reference; + + await node.set({ + a: { '.value': 1, '.priority': 'z' }, + b: { '.value': 2, '.priority': 'y' }, + c: { '.value': 3, '.priority': 'x' }, + d: { '.value': 4, '.priority': 'w' } + }); + + const tasks: TaskList = [ + [node.startAt('w').endBefore('y'), { c: 3, d: 4 }], + [node.startAt('w').endBefore('x'), { d: 4 }], + [node.startAt('a').endBefore('c'), null] + ]; + + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAfter / endBefore with priority work with server data.', async () => { + const node = getRandomNode() as Reference; + + await node.set({ + a: { '.value': 1, '.priority': 'z' }, + b: { '.value': 2, '.priority': 'y' }, + c: { '.value': 3, '.priority': 'x' }, + d: { '.value': 4, '.priority': 'w' } + }); + + const tasks: TaskList = [ + [node.startAfter('w').endBefore('z'), { b: 2, c: 3 }], + [node.startAfter('w').endBefore('y'), { c: 3 }], + [node.startAfter('w').endBefore('w'), null] + ]; + + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAt / endAt with priority and name works.', async () => { + const node = getRandomNode() as Reference; + + await node.set({ + a: { '.value': 1, '.priority': 1 }, + b: { '.value': 2, '.priority': 1 }, + c: { '.value': 3, '.priority': 2 }, + d: { '.value': 4, '.priority': 2 } + }); + + const tasks: TaskList = [ + [node.startAt(1, 'a').endAt(2, 'd'), { a: 1, b: 2, c: 3, d: 4 }], + [node.startAt(1, 'b').endAt(2, 'c'), { b: 2, c: 3 }], + [node.startAt(1, 'c').endAt(2), { c: 3, d: 4 }] + ]; + + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAfter / endAt with priority and name works.', async () => { + const node = getRandomNode() as Reference; + + await node.set({ + a: { '.value': 1, '.priority': 1 }, + b: { '.value': 2, '.priority': 1 }, + c: { '.value': 3, '.priority': 2 }, + d: { '.value': 4, '.priority': 2 } + }); + + const tasks: TaskList = [ + [node.startAfter(1, 'a').endAt(2, 'd'), { b: 2, c: 3, d: 4 }], + [node.startAfter(1, 'b').endAt(2, 'c'), { c: 3 }], + [node.startAfter(1, 'c').endAt(2), { c: 3, d: 4 }] + ]; + + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAt / endBefore with priority and name works.', async () => { + const node = getRandomNode() as Reference; + + await node.set({ + a: { '.value': 1, '.priority': 1 }, + b: { '.value': 2, '.priority': 1 }, + c: { '.value': 3, '.priority': 2 }, + d: { '.value': 4, '.priority': 2 } + }); + + const tasks: TaskList = [ + [node.startAt(1, 'a').endBefore(2, 'd'), { a: 1, b: 2, c: 3 }], + [node.startAt(1, 'b').endBefore(2, 'c'), { b: 2 }], + [node.startAt(1, 'c').endBefore(2), null] + ]; + + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAfter / endBefore with priority and name works.', async () => { + const node = getRandomNode() as Reference; + + await node.set({ + a: { '.value': 1, '.priority': 1 }, + b: { '.value': 2, '.priority': 1 }, + c: { '.value': 3, '.priority': 2 }, + d: { '.value': 4, '.priority': 2 } + }); + + const tasks: TaskList = [ + [node.startAfter(1, 'a').endBefore(2, 'd'), { b: 2, c: 3 }], + [node.startAfter(1, 'b').endBefore(2, 'c'), null], + [node.startAfter(1, 'c').endBefore(2), null] + ]; + + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAt / endAt with priority and name work with server data', async () => { + const node = getRandomNode() as Reference; + + await node.set({ + a: { '.value': 1, '.priority': 1 }, + b: { '.value': 2, '.priority': 1 }, + c: { '.value': 3, '.priority': 2 }, + d: { '.value': 4, '.priority': 2 } + }); + const tasks: TaskList = [ + [node.startAt(1, 'a').endAt(2, 'd'), { a: 1, b: 2, c: 3, d: 4 }], + [node.startAt(1, 'b').endAt(2, 'c'), { b: 2, c: 3 }], + [node.startAt(1, 'c').endAt(2), { c: 3, d: 4 }] + ]; + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAfter / endAt with priority and name work with server data', async () => { + const node = getRandomNode() as Reference; + + await node.set({ + a: { '.value': 1, '.priority': 1 }, + b: { '.value': 2, '.priority': 1 }, + c: { '.value': 3, '.priority': 2 }, + d: { '.value': 4, '.priority': 2 } + }); + const tasks: TaskList = [ + [node.startAfter(1, 'a').endAt(2, 'd'), { b: 2, c: 3, d: 4 }], + [node.startAfter(1, 'b').endAt(2, 'c'), { c: 3 }], + [node.startAfter(1, 'c').endAt(2), { c: 3, d: 4 }] + ]; + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAt / endBefore with priority and name work with server data', async () => { + const node = getRandomNode() as Reference; + + await node.set({ + a: { '.value': 1, '.priority': 1 }, + b: { '.value': 2, '.priority': 1 }, + c: { '.value': 3, '.priority': 2 }, + d: { '.value': 4, '.priority': 2 } + }); + const tasks: TaskList = [ + [node.startAt(1, 'a').endBefore(2, 'd'), { a: 1, b: 2, c: 3 }], + [node.startAt(1, 'b').endBefore(2, 'c'), { b: 2 }], + [node.startAt(1, 'c').endBefore(2), null] + ]; + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAfter / endBefore with priority and name work with server data', async () => { + const node = getRandomNode() as Reference; + + await node.set({ + a: { '.value': 1, '.priority': 1 }, + b: { '.value': 2, '.priority': 1 }, + c: { '.value': 3, '.priority': 2 }, + d: { '.value': 4, '.priority': 2 } + }); + const tasks: TaskList = [ + [node.startAfter(1, 'a').endBefore(2, 'd'), { b: 2, c: 3 }], + [node.startAfter(1, 'b').endBefore(2, 'c'), null], + [node.startAfter(1, 'c').endBefore(2), null] + ]; + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAt / endAt with priority and name works (2).', () => { + const node = getRandomNode() as Reference; + + const tasks: TaskList = [ + [node.startAt(1, 'c').endAt(2, 'b'), { a: 1, b: 2, c: 3, d: 4 }], + [node.startAt(1, 'd').endAt(2, 'a'), { d: 4, a: 1 }], + [node.startAt(1, 'e').endAt(2), { a: 1, b: 2 }] + ]; + + node.set({ + c: { '.value': 3, '.priority': 1 }, + d: { '.value': 4, '.priority': 1 }, + a: { '.value': 1, '.priority': 2 }, + b: { '.value': 2, '.priority': 2 } + }); + + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAfter / endAt with priority and name works (2).', () => { + const node = getRandomNode() as Reference; + + const tasks: TaskList = [ + [node.startAfter(1, 'c').endAt(2, 'b'), { a: 1, b: 2, d: 4 }], + [node.startAfter(1, 'd').endAt(2, 'a'), { a: 1 }], + [node.startAfter(1, 'e').endAt(2), { a: 1, b: 2 }] + ]; + + node.set({ + c: { '.value': 3, '.priority': 1 }, + d: { '.value': 4, '.priority': 1 }, + a: { '.value': 1, '.priority': 2 }, + b: { '.value': 2, '.priority': 2 } + }); + + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAt / endBefore with priority and name works with queries before set.', () => { + const node = getRandomNode() as Reference; + + const tasks: TaskList = [ + [node.startAt(1, 'c').endBefore(2, 'b'), { a: 1, c: 3, d: 4 }], + [node.startAt(1, 'd').endBefore(2, 'a'), { d: 4 }], + [node.startAt(1, 'e').endBefore(2), null] + ]; + + node.set({ + c: { '.value': 3, '.priority': 1 }, + d: { '.value': 4, '.priority': 1 }, + a: { '.value': 1, '.priority': 2 }, + b: { '.value': 2, '.priority': 2 } + }); + + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAfter / endBefore with priority and name works (2).', () => { + const node = getRandomNode() as Reference; + + const tasks: TaskList = [ + [node.startAfter(1, 'c').endBefore(2, 'b'), { a: 1, d: 4 }], + [node.startAfter(1, 'd').endBefore(2, 'a'), null], + [node.startAfter(1, 'e').endBefore(2), null] + ]; + + node.set({ + c: { '.value': 3, '.priority': 1 }, + d: { '.value': 4, '.priority': 1 }, + a: { '.value': 1, '.priority': 2 }, + b: { '.value': 2, '.priority': 2 } + }); + return Promise.all( tasks.map(async task => { const [query, val] = task; @@ -1288,6 +1978,93 @@ describe('Query Tests', () => { ); }); + it('Ensure startAfter / endAt with priority and name works (2). With server data', async () => { + const node = getRandomNode() as Reference; + + await node.set({ + c: { '.value': 3, '.priority': 1 }, + d: { '.value': 4, '.priority': 1 }, + a: { '.value': 1, '.priority': 2 }, + b: { '.value': 2, '.priority': 2 } + }); + + const tasks: TaskList = [ + [node.startAfter(1, 'c').endAt(2, 'b'), { a: 1, b: 2, d: 4 }], + [node.startAfter(1, 'd').endAt(2, 'a'), { a: 1 }], + [node.startAfter(1, 'e').endAt(2), { a: 1, b: 2 }] + ]; + + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAt / endBefore with priority and name works (2). With server data.', () => { + const node = getRandomNode() as Reference; + + node.set({ + c: { '.value': 3, '.priority': 1 }, + d: { '.value': 4, '.priority': 1 }, + a: { '.value': 1, '.priority': 2 }, + b: { '.value': 2, '.priority': 2 } + }); + + const tasks: TaskList = [ + [node.startAt(1, 'c').endBefore(2, 'b'), { a: 1, c: 3, d: 4 }], + [node.startAt(1, 'd').endBefore(2, 'a'), { d: 4 }], + [node.startAt(1, 'e').endBefore(2), null] + ]; + + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + + it('Ensure startAfter / endBefore with priority and name works (2). With server data.', () => { + const node = getRandomNode() as Reference; + + node.set({ + c: { '.value': 3, '.priority': 1 }, + d: { '.value': 4, '.priority': 1 }, + a: { '.value': 1, '.priority': 2 }, + b: { '.value': 2, '.priority': 2 } + }); + + const tasks: TaskList = [ + [node.startAfter(1, 'c').endBefore(2, 'b'), { a: 1, d: 4 }], + [node.startAfter(1, 'd').endBefore(2, 'a'), null], + [node.startAfter(1, 'e').endBefore(2), null] + ]; + + return Promise.all( + tasks.map(async task => { + const [query, val] = task; + const ea = EventAccumulatorFactory.waitsForCount(1); + query.on('value', snap => { + ea.addEvent(snap.val()); + }); + const [newVal] = await ea.promise; + expect(newVal).to.deep.equal(val); + }) + ); + }); + it('Set a limit, add some nodes, ensure prevName works correctly.', () => { const node = getRandomNode() as Reference; @@ -1507,7 +2284,36 @@ describe('Query Tests', () => { }); }); - it('Filtering to only null priorities works.', async () => { + it('Filtering to only null priorities works.', async () => { + const f = getRandomNode() as Reference; + + const ea = EventAccumulatorFactory.waitsForCount(1); + f.root.child('.info/connected').on('value', snap => { + ea.addEvent(); + }); + + await ea.promise; + + f.set({ + a: { '.priority': null, '.value': 0 }, + b: { '.priority': null, '.value': 1 }, + c: { '.priority': '2', '.value': 2 }, + d: { '.priority': 3, '.value': 3 }, + e: { '.priority': 'hi', '.value': 4 } + }); + + const snapAcc = EventAccumulatorFactory.waitsForCount(1); + f.startAt(null) + .endAt(null) + .on('value', snap => { + snapAcc.addEvent(snap.val()); + }); + + const [val] = await snapAcc.promise; + expect(val).to.deep.equal({ a: 0, b: 1 }); + }); + + it('Null priorities not included in startAfter().', async () => { const f = getRandomNode() as Reference; const ea = EventAccumulatorFactory.waitsForCount(1); @@ -1526,14 +2332,14 @@ describe('Query Tests', () => { }); const snapAcc = EventAccumulatorFactory.waitsForCount(1); - f.startAt(null) + f.startAfter(null) .endAt(null) .on('value', snap => { snapAcc.addEvent(snap.val()); }); const [val] = await snapAcc.promise; - expect(val).to.deep.equal({ a: 0, b: 1 }); + expect(val).to.deep.equal(null); }); it('null priorities included in endAt(2).', async () => { @@ -1556,6 +2362,26 @@ describe('Query Tests', () => { expect(val).to.deep.equal({ a: 0, b: 1, c: 2 }); }); + it('null priorities included in endBefore.', async () => { + const f = getRandomNode() as Reference; + + f.set({ + a: { '.priority': null, '.value': 0 }, + b: { '.priority': null, '.value': 1 }, + c: { '.priority': 2, '.value': 2 }, + d: { '.priority': 3, '.value': 3 }, + e: { '.priority': 'hi', '.value': 4 } + }); + + const ea = EventAccumulatorFactory.waitsForCount(1); + f.endBefore(2).on('value', snap => { + ea.addEvent(snap.val()); + }); + + const [val] = await ea.promise; + expect(val).to.deep.equal({ a: 0, b: 1 }); + }); + it('null priorities not included in startAt(2).', async () => { const f = getRandomNode() as Reference; @@ -1577,6 +2403,27 @@ describe('Query Tests', () => { expect(val).to.deep.equal({ c: 2, d: 3, e: 4 }); }); + it('null priorities not included in startAfter(2).', async () => { + const f = getRandomNode() as Reference; + + f.set({ + a: { '.priority': null, '.value': 0 }, + b: { '.priority': null, '.value': 1 }, + c: { '.priority': 2, '.value': 2 }, + d: { '.priority': 3, '.value': 3 }, + e: { '.priority': 'hi', '.value': 4 } + }); + + const ea = EventAccumulatorFactory.waitsForCount(1); + + f.startAfter(2).on('value', snap => { + ea.addEvent(snap.val()); + }); + + const [val] = await ea.promise; + expect(val).to.deep.equal({ d: 3, e: 4 }); + }); + function dumpListens(node: Query) { const listens: Map> = (node.repo .persistentConnection_ as any).listens; @@ -1994,6 +2841,35 @@ describe('Query Tests', () => { }); }); + it('.startAfter() with two arguments works properly (case 1169).', done => { + const ref = getRandomNode() as Reference; + const data = { + Walker: { + name: 'Walker', + score: 20, + '.priority': 20 + }, + Michael: { + name: 'Michael', + score: 100, + '.priority': 100 + } + }; + ref.set(data, () => { + ref + .startAfter(20, 'Walker') + .limitToFirst(2) + .on('value', s => { + const childNames = []; + s.forEach(node => { + childNames.push(node.key); + }); + expect(childNames).to.deep.equal(['Michael']); + done(); + }); + }); + }); + it('handles multiple queries on the same node', async () => { const ref = getRandomNode() as Reference; @@ -2124,6 +3000,28 @@ describe('Query Tests', () => { ); }); + it(".endBefore(null, 'f').limitToLast(5) returns the right set of children.", done => { + const ref = getRandomNode() as Reference; + ref.set( + { a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', f: 'f', g: 'g', h: 'h' }, + () => { + ref + .endBefore(null, 'f') + .limitToLast(5) + .on('value', s => { + expect(s.val()).to.deep.equal({ + a: 'a', + b: 'b', + c: 'c', + d: 'd', + e: 'e' + }); + done(); + }); + } + ); + }); + it('complex update() at query root raises correct value event', done => { const nodePair = getRandomNode(2); const writer = nodePair[0]; @@ -2626,6 +3524,99 @@ describe('Query Tests', () => { ref.child('a').setWithPriority('a', 10); expect(addedFirst).to.deep.equal(['a', 'a']); + expect(removedSecond).to.deep.equal([]); + + ref.child('a').setWithPriority('a', 5); + expect(removedSecond).to.deep.equal(['a']); + }); + + it('Case 2003: Correctly get events for startAtfter/endAt queries when priority changes.', () => { + const ref = getRandomNode() as Reference; + const addedFirst = [], + removedFirst = [], + addedSecond = [], + removedSecond = []; + ref + .startAfter(0) + .endAt(10) + .on('child_added', snap => { + addedFirst.push(snap.key); + }); + ref + .startAfter(0) + .endAt(10) + .on('child_removed', snap => { + removedFirst.push(snap.key); + }); + ref + .startAfter(10) + .endAt(20) + .on('child_added', snap => { + addedSecond.push(snap.key); + }); + ref + .startAfter(10) + .endAt(20) + .on('child_removed', snap => { + removedSecond.push(snap.key); + }); + + ref.child('a').setWithPriority('a', 5); + expect(addedFirst).to.deep.equal(['a']); + ref.child('a').setWithPriority('a', 15); + expect(removedFirst).to.deep.equal(['a']); + expect(addedSecond).to.deep.equal(['a']); + + ref.child('a').setWithPriority('a', 10); + ref.child('a').setWithPriority('a', 0); + expect(addedFirst).to.deep.equal(['a', 'a']); + expect(removedSecond).to.deep.equal(['a']); + + ref.child('a').setWithPriority('a', 5); + expect(removedSecond).to.deep.equal(['a']); + }); + + it('Correctly get events for startAt/endBefore queries when priority changes.', () => { + const ref = getRandomNode() as Reference; + const addedFirst = [], + removedFirst = [], + addedSecond = [], + removedSecond = []; + ref + .startAt(0) + .endBefore(10) + .on('child_added', snap => { + addedFirst.push(snap.key); + }); + ref + .startAt(0) + .endBefore(10) + .on('child_removed', snap => { + removedFirst.push(snap.key); + }); + ref + .startAt(10) + .endBefore(20) + .on('child_added', snap => { + addedSecond.push(snap.key); + }); + ref + .startAt(10) + .endBefore(20) + .on('child_removed', snap => { + removedSecond.push(snap.key); + }); + + ref.child('a').setWithPriority('a', 5); + expect(addedFirst).to.deep.equal(['a']); + ref.child('a').setWithPriority('a', 15); + expect(removedFirst).to.deep.equal(['a']); + expect(addedSecond).to.deep.equal(['a']); + + ref.child('a').setWithPriority('a', 10); + ref.child('a').setWithPriority('a', 0); + expect(addedFirst).to.deep.equal(['a', 'a']); + expect(removedSecond).to.deep.equal(['a']); ref.child('a').setWithPriority('a', 5); expect(removedSecond).to.deep.equal(['a']); @@ -3153,6 +4144,120 @@ describe('Query Tests', () => { ); }); + it('Integer keys behave numerically with startAfter.', done => { + const ref = getRandomNode() as Reference; + ref.set( + { + 1: true, + 50: true, + 550: true, + 6: true, + 600: true, + 70: true, + 8: true, + 80: true + }, + () => { + ref + .startAfter(null, '50') + .endAt(null, '80') + .once('value', s => { + expect(s.val()).to.deep.equal({ 70: true, 80: true }); + done(); + }); + } + ); + }); + + it('Integer keys behave numerically with startAfter with overflow.', done => { + const ref = getRandomNode() as Reference; + ref.set( + { + 1: true, + 50: true, + 550: true, + 6: true, + 600: true, + 70: true, + 8: true, + 80: true, + 'a': true + }, + () => { + ref.startAfter(null, '' + INTEGER_32_MAX).once('value', s => { + expect(s.val()).to.deep.equal({ 'a': true }); + done(); + }); + } + ); + }); + + it('Integer keys behave numerically with endBefore.', done => { + const ref = getRandomNode() as Reference; + ref.set( + { + 1: true, + 50: true, + 550: true, + 6: true, + 600: true, + 70: true, + 8: true, + 80: true + }, + () => { + ref.endBefore(null, '50').once('value', s => { + expect(s.val()).to.deep.equal({ + 1: true, + 6: true, + 8: true + }); + done(); + }); + } + ); + }); + + it('Integer keys behave numerically with endBefore with underflow.', done => { + const ref = getRandomNode() as Reference; + ref.set( + { + 1: true + }, + () => { + ref.endBefore(null, '' + INTEGER_32_MIN).once('value', s => { + expect(s.val()).to.deep.equal(null); + done(); + }); + } + ); + }); + + it('Integer keys behave numerically with endBefore at boundary.', done => { + const ref = getRandomNode() as Reference; + const integerData = { + 1: true, + 50: true, + 550: true, + 6: true, + 600: true, + 70: true, + 8: true, + 80: true + }; + const data = Object.assign({}, integerData); + data['a'] = true; + ref.set(data, () => { + ref.endBefore(null, '' + INTEGER_32_MAX).once('value', s => { + expect(s.val()).to.deep.equal(integerData), + ref.startAfter(null, '' + INTEGER_32_MAX).once('value', s => { + expect(s.val()).to.deep.equal({ 'a': true }); + done(); + }); + }); + }); + }); + it('.limitToLast() on node with priority.', done => { const ref = getRandomNode() as Reference; ref.set({ a: 'blah', '.priority': 'priority' }, () => {