diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index 9ce91f4fd2b..bd278bfbb9e 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -1489,7 +1489,9 @@ export class Query implements firestore.Query { } fieldValue = this.firestore._dataConverter.parseQueryValue( 'Query.where', - value + value, + // We only allow nested arrays for IN queries. + /** allowArrays = */ operator === Operator.IN ? true : false ); } const filter = FieldFilter.create(fieldPath, operator, fieldValue); diff --git a/packages/firestore/src/api/user_data_converter.ts b/packages/firestore/src/api/user_data_converter.ts index e99a68e1ae8..2b0e87d1278 100644 --- a/packages/firestore/src/api/user_data_converter.ts +++ b/packages/firestore/src/api/user_data_converter.ts @@ -134,7 +134,12 @@ enum UserDataSource { * Indicates the source is a where clause, cursor bound, arrayUnion() * element, etc. Of note, isWrite(source) will return false. */ - Argument + Argument, + /** + * Indicates that the source is an Argument that may directly contain nested + * arrays (e.g. the operand of an `in` query). + */ + ArrayArgument } function isWrite(dataSource: UserDataSource): boolean { @@ -144,6 +149,7 @@ function isWrite(dataSource: UserDataSource): boolean { case UserDataSource.Update: return true; case UserDataSource.Argument: + case UserDataSource.ArrayArgument: return false; default: throw fail(`Unexpected case for UserDataSource: ${dataSource}`); @@ -478,10 +484,17 @@ export class UserDataConverter { /** * Parse a "query value" (e.g. value in a where filter or a value in a cursor * bound). + * + * @param allowArrays Whether the query value is an array that may directly + * contain additional arrays (e.g. the operand of an `in` query). */ - parseQueryValue(methodName: string, input: unknown): FieldValue { + parseQueryValue( + methodName: string, + input: unknown, + allowArrays = false + ): FieldValue { const context = new ParseContext( - UserDataSource.Argument, + allowArrays ? UserDataSource.ArrayArgument : UserDataSource.Argument, methodName, FieldPath.EMPTY_PATH ); @@ -536,7 +549,14 @@ export class UserDataConverter { if (input instanceof Array) { // TODO(b/34871131): Include the path containing the array in the error // message. - if (context.arrayElement) { + // In the case of IN queries, the parsed data is an array (representing + // the set of values to be included for the IN query) that may directly + // contain additional arrays (each representing an individual field + // value), so we disable this validation. + if ( + context.arrayElement && + context.dataSource !== UserDataSource.ArrayArgument + ) { throw context.createError('Nested arrays are not supported'); } return this.parseArray(input as unknown[], context); diff --git a/packages/firestore/test/integration/api/query.test.ts b/packages/firestore/test/integration/api/query.test.ts index 999cdf6b98c..049c1f8e275 100644 --- a/packages/firestore/test/integration/api/query.test.ts +++ b/packages/firestore/test/integration/api/query.test.ts @@ -26,7 +26,6 @@ import { EventsAccumulator } from '../util/events_accumulator'; import firebase from '../util/firebase_export'; import { apiDescribe, - isRunningAgainstEmulator, toChangesArray, toDataArray, withTestCollection, @@ -799,14 +798,18 @@ apiDescribe('Queries', (persistence: boolean) => { c: { zip: 98103 }, d: { zip: [98101] }, e: { zip: ['98101', { zip: 98101 }] }, - f: { zip: { code: 500 } } + f: { zip: { code: 500 } }, + g: { zip: [98101, 98102] } }; await withTestCollection(persistence, testDocs, async coll => { - const snapshot = await coll.where('zip', 'in', [98101, 98103]).get(); + const snapshot = await coll + .where('zip', 'in', [98101, 98103, [98101, 98102]]) + .get(); expect(toDataArray(snapshot)).to.deep.equal([ { zip: 98101 }, - { zip: 98103 } + { zip: 98103 }, + { zip: [98101, 98102] } ]); // With objects. @@ -815,28 +818,24 @@ apiDescribe('Queries', (persistence: boolean) => { }); }); - // eslint-disable-next-line no-restricted-properties, - (isRunningAgainstEmulator() ? it : it.skip)( - 'can use IN filters by document ID', - async () => { - const testDocs = { - aa: { key: 'aa' }, - ab: { key: 'ab' }, - ba: { key: 'ba' }, - bb: { key: 'bb' } - }; - await withTestCollection(persistence, testDocs, async coll => { - const snapshot = await coll - .where(FieldPath.documentId(), 'in', ['aa', 'ab']) - .get(); - - expect(toDataArray(snapshot)).to.deep.equal([ - { key: 'aa' }, - { key: 'ab' } - ]); - }); - } - ); + it('can use IN filters by document ID', async () => { + const testDocs = { + aa: { key: 'aa' }, + ab: { key: 'ab' }, + ba: { key: 'ba' }, + bb: { key: 'bb' } + }; + await withTestCollection(persistence, testDocs, async coll => { + const snapshot = await coll + .where(FieldPath.documentId(), 'in', ['aa', 'ab']) + .get(); + + expect(toDataArray(snapshot)).to.deep.equal([ + { key: 'aa' }, + { key: 'ab' } + ]); + }); + }); it('can use array-contains-any filters', async () => { const testDocs = {