From 94987645062cf3553074b85043d1e564b15435d5 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Tue, 12 Mar 2019 10:15:01 +0800 Subject: [PATCH 1/3] Adding CollectionGroup queries --- dev/src/index.ts | 33 ++++- dev/src/reference.ts | 263 ++++++++++++++++++++++------------- dev/system-test/firestore.ts | 97 +++++++++++++ dev/test/query.ts | 29 ++++ dev/test/typescript.ts | 1 + types/firestore.d.ts | 12 ++ 6 files changed, 339 insertions(+), 96 deletions(-) diff --git a/dev/src/index.ts b/dev/src/index.ts index bee0157fb..9067547b6 100644 --- a/dev/src/index.ts +++ b/dev/src/index.ts @@ -24,7 +24,7 @@ import {DocumentSnapshot, DocumentSnapshotBuilder, QueryDocumentSnapshot} from ' import {logger, setLibVersion} from './logger'; import {DEFAULT_DATABASE_ID, FieldPath, QualifiedResourcePath, ResourcePath, validateResourcePath} from './path'; import {ClientPool} from './pool'; -import {CollectionReference} from './reference'; +import {CollectionReference, Query, QueryOptions} from './reference'; import {DocumentReference} from './reference'; import {Serializer} from './serializer'; import {Timestamp} from './timestamp'; @@ -437,6 +437,37 @@ export class Firestore { return new CollectionReference(this, path); } + /** + * Creates and returns a new Query that includes all documents in the + * database that are contained in a collection or subcollection with the + * given collectionId. + * + * @param {string} collectionId Identifies the collections to query over. + * Every collection or subcollection with this ID as the last segment of its + * path will be included. Cannot contain a slash. + * @returns {Query} The created Query. + * + * @example + * let docA = firestore.doc('mygroup/docA').set({foo: 'bar'}); + * let docB = firestore.doc('abc/def/mygroup/docB').set({foo: 'bar'}); + * + * Promise.all([docA, docB]).then(() => { + * let query = firestore.collectionGroup('mygroup'); + * query = query.where('foo', '==', 'bar'); + * return query.get().then(snapshot => { + * console.log(`Found ${snapshot.size} documents.`); + * }); + * }); + */ + collectionGroup(collectionId: string): Query { + if (collectionId.indexOf('/') !== -1) { + throw new Error(`Invalid collectionId '${ + collectionId}'. Collection IDs must not contain '/'.`); + } + + return new Query(this, QueryOptions.forCollectionGroupQuery(collectionId)); + } + /** * Creates a [WriteBatch]{@link WriteBatch}, used for performing * multiple writes as a single atomic operation. diff --git a/dev/src/reference.ts b/dev/src/reference.ts index d6a9fcd5a..aaebe4fe4 100644 --- a/dev/src/reference.ts +++ b/dev/src/reference.ts @@ -867,15 +867,87 @@ interface QueryCursor { values: unknown[]; } -/** Internal options to customize the Query class. */ -interface QueryOptions { - startAt?: QueryCursor; - startAfter?: QueryCursor; - endAt?: QueryCursor; - endBefore?: QueryCursor; - limit?: number; - offset?: number; - projection?: api.StructuredQuery.IProjection; +/** + * Internal class representing custom Query options. + * + * These options are immutable. Modified options can be created using `with()`. + * @private + */ +export class QueryOptions { + constructor( + readonly parentPath: ResourcePath, readonly collectionId: string, + readonly allDescendants: boolean, readonly fieldFilters: FieldFilter[], + readonly fieldOrders: FieldOrder[], readonly startAt?: QueryCursor, + readonly endAt?: QueryCursor, readonly limit?: number, + readonly offset?: number, + readonly projection?: api.StructuredQuery.IProjection) {} + + + /** + * Returns query options for a collection group query. + * @private + */ + static forCollectionGroupQuery(collectionId: string): QueryOptions { + return new QueryOptions( + /*parentPath=*/ResourcePath.EMPTY, collectionId, + /*allDescendants=*/true, /*fieldFilters=*/[], /*fieldOrders=*/[]); + } + + /** + * Returns query options for a single-collection query. + * @private + */ + static forCollectionQuery(collectionRef: ResourcePath): QueryOptions { + return new QueryOptions( + collectionRef.parent()!, collectionRef.id!, /*allDescendants=*/false, + /*fieldFilters=*/[], /*fieldOrders=*/[]); + } + + /** + * Returns the union of the current and the provided options. + * @private + */ + with(settings: { + parentPath?: ResourcePath; + collectionId?: string; + allDescendants?: boolean; + fieldFilters?: FieldFilter[]; + fieldOrders?: FieldOrder[]; + startAt?: QueryCursor; + endAt?: QueryCursor; + limit?: number; + offset?: number; + projection?: api.StructuredQuery.IProjection; + }): QueryOptions { + return new QueryOptions( + coalesce(settings.parentPath, this.parentPath)!, + coalesce(settings.collectionId, this.collectionId)!, + coalesce(settings.allDescendants, this.allDescendants)!, + coalesce(settings.fieldFilters, this.fieldFilters)!, + coalesce(settings.fieldOrders, this.fieldOrders)!, + coalesce(settings.startAt, this.startAt), + coalesce(settings.endAt, this.endAt), + coalesce(settings.limit, this.limit), + coalesce(settings.offset, this.offset), + coalesce(settings.projection, this.projection)); + } + + isEqual(other: QueryOptions) { + if (this === other) { + return true; + } + + return other instanceof QueryOptions && + this.parentPath.isEqual(other.parentPath) && + this.collectionId === other.collectionId && + this.allDescendants === other.allDescendants && + this.limit === other.limit && this.offset === other.offset && + deepEqual(this.fieldFilters, other.fieldFilters, {strict: true}) && + deepEqual(this.fieldOrders, other.fieldOrders, {strict: true}) && + deepEqual(this.startAt, other.startAt, {strict: true}) && + deepEqual(this.endAt, other.endAt, {strict: true}) && + deepEqual(this.projection, other.projection, {strict: true}); + } } /** @@ -892,17 +964,11 @@ export class Query { * @hideconstructor * * @param _firestore The Firestore Database client. - * @param _path Path of the collection to be queried. - * @param _fieldFilters Sequence of fields constraining the results of the - * query. - * @param _fieldOrders Sequence of fields to control the order of results. - * @param _queryOptions Additional query options. + * @param _queryOptions Options that define the query. */ constructor( - private readonly _firestore: Firestore, readonly _path: ResourcePath, - private readonly _fieldFilters: FieldFilter[] = [], - private readonly _fieldOrders: FieldOrder[] = [], - private readonly _queryOptions: QueryOptions = {}) { + private readonly _firestore: Firestore, + protected readonly _queryOptions: QueryOptions) { this._serializer = new Serializer(_firestore); } @@ -1014,18 +1080,18 @@ export class Query { 'startAfter(), endBefore() or endAt().'); } - fieldPath = FieldPath.fromArgument(fieldPath); + const path = FieldPath.fromArgument(fieldPath); - if (FieldPath.documentId().isEqual(fieldPath)) { + if (FieldPath.documentId().isEqual(path)) { value = this.validateReference(value); } - const combinedFilters = this._fieldFilters.concat(new FieldFilter( - this._serializer, fieldPath, comparisonOperators[opStr], value)); + const fieldFilter = new FieldFilter( + this._serializer, path, comparisonOperators[opStr], value); - return new Query( - this._firestore, this._path, combinedFilters, this._fieldOrders, - this._queryOptions); + const options = this._queryOptions.with( + {fieldFilters: this._queryOptions.fieldFilters.concat(fieldFilter)}); + return new Query(this._firestore, options); } /** @@ -1063,12 +1129,8 @@ export class Query { } } - const options = Object.assign({}, this._queryOptions); - options.projection = {fields}; - - return new Query( - this._firestore, this._path, this._fieldFilters, this._fieldOrders, - options); + const options = this._queryOptions.with({projection: {fields}}); + return new Query(this._firestore, options); } /** @@ -1106,10 +1168,10 @@ export class Query { const newOrder = new FieldOrder( FieldPath.fromArgument(fieldPath), directionOperators[directionStr || 'asc']); - const combinedOrders = this._fieldOrders.concat(newOrder); - return new Query( - this._firestore, this._path, this._fieldFilters, combinedOrders, - this._queryOptions); + + const options = this._queryOptions.with( + {fieldOrders: this._queryOptions.fieldOrders.concat(newOrder)}); + return new Query(this._firestore, options); } /** @@ -1134,11 +1196,8 @@ export class Query { limit(limit: number): Query { validateInteger('limit', limit); - const options = Object.assign({}, this._queryOptions); - options.limit = limit; - return new Query( - this._firestore, this._path, this._fieldFilters, this._fieldOrders, - options); + const options = this._queryOptions.with({limit}); + return new Query(this._firestore, options); } /** @@ -1163,11 +1222,8 @@ export class Query { offset(offset: number): Query { validateInteger('offset', offset); - const options = Object.assign({}, this._queryOptions); - options.offset = offset; - return new Query( - this._firestore, this._path, this._fieldFilters, this._fieldOrders, - options); + const options = this._queryOptions.with({offset}); + return new Query(this._firestore, options); } /** @@ -1182,10 +1238,8 @@ export class Query { } return ( - other instanceof Query && this._path.isEqual(other._path) && - deepEqual(this._fieldFilters, other._fieldFilters, {strict: true}) && - deepEqual(this._fieldOrders, other._fieldOrders, {strict: true}) && - deepEqual(this._queryOptions, other._queryOptions, {strict: true})); + other instanceof Query && + this._queryOptions.isEqual(other._queryOptions)); } /** @@ -1200,16 +1254,16 @@ export class Query { Array): FieldOrder[] { if (!Query._isDocumentSnapshot(cursorValuesOrDocumentSnapshot)) { - return this._fieldOrders; + return this._queryOptions.fieldOrders; } - const fieldOrders = this._fieldOrders.slice(); + const fieldOrders = this._queryOptions.fieldOrders.slice(); let hasDocumentId = false; if (fieldOrders.length === 0) { // If no explicit ordering is specified, use the first inequality to // define an implicit order. - for (const fieldFilter of this._fieldFilters) { + for (const fieldFilter of this._queryOptions.fieldFilters) { if (fieldFilter.isInequalityFilter()) { fieldOrders.push(new FieldOrder(fieldFilter.field)); break; @@ -1299,14 +1353,16 @@ export class Query { * @private */ private validateReference(val: unknown): DocumentReference { + const basePath = this._queryOptions.allDescendants ? + this._queryOptions.parentPath : + this._queryOptions.parentPath.append(this._queryOptions.collectionId); let reference: DocumentReference; if (typeof val === 'string') { - reference = - new DocumentReference(this._firestore, this._path.append(val)); + reference = new DocumentReference(this._firestore, basePath.append(val)); } else if (val instanceof DocumentReference) { reference = val; - if (!this._path.isPrefixOf(reference._path)) { + if (!basePath.isPrefixOf(reference._path)) { throw new Error( `"${reference.path}" is not part of the query result set and ` + 'cannot be used as a query boundary.'); @@ -1317,7 +1373,8 @@ export class Query { 'string or a DocumentReference.'); } - if (reference._path.parent()!.compareTo(this._path) !== 0) { + if (!this._queryOptions.allDescendants && + reference._path.parent()!.compareTo(basePath) !== 0) { throw new Error( 'Only a direct child can be used as a query boundary. ' + `Found: "${reference.path}".`); @@ -1348,15 +1405,13 @@ export class Query { Query { validateMinNumberOfArguments('Query.startAt', arguments, 1); - const options = Object.assign({}, this._queryOptions); - const fieldOrders = this.createImplicitOrderBy(fieldValuesOrDocumentSnapshot); - options.startAt = + const startAt = this.createCursor(fieldOrders, fieldValuesOrDocumentSnapshot, true); - return new Query( - this._firestore, this._path, this._fieldFilters, fieldOrders, options); + const options = this._queryOptions.with({fieldOrders, startAt}); + return new Query(this._firestore, options); } /** @@ -1383,15 +1438,13 @@ export class Query { Query { validateMinNumberOfArguments('Query.startAfter', arguments, 1); - const options = Object.assign({}, this._queryOptions); - const fieldOrders = this.createImplicitOrderBy(fieldValuesOrDocumentSnapshot); - options.startAt = + const startAt = this.createCursor(fieldOrders, fieldValuesOrDocumentSnapshot, false); - return new Query( - this._firestore, this._path, this._fieldFilters, fieldOrders, options); + const options = this._queryOptions.with({fieldOrders, startAt}); + return new Query(this._firestore, options); } /** @@ -1417,15 +1470,13 @@ export class Query { Query { validateMinNumberOfArguments('Query.endBefore', arguments, 1); - const options = Object.assign({}, this._queryOptions); - const fieldOrders = this.createImplicitOrderBy(fieldValuesOrDocumentSnapshot); - options.endAt = + const endAt = this.createCursor(fieldOrders, fieldValuesOrDocumentSnapshot, true); - return new Query( - this._firestore, this._path, this._fieldFilters, fieldOrders, options); + const options = this._queryOptions.with({fieldOrders, endAt}); + return new Query(this._firestore, options); } /** @@ -1451,15 +1502,13 @@ export class Query { Query { validateMinNumberOfArguments('Query.endAt', arguments, 1); - const options = Object.assign({}, this._queryOptions); - const fieldOrders = this.createImplicitOrderBy(fieldValuesOrDocumentSnapshot); - options.endAt = + const endAt = this.createCursor(fieldOrders, fieldValuesOrDocumentSnapshot, false); - return new Query( - this._firestore, this._path, this._fieldFilters, fieldOrders, options); + const options = this._queryOptions.with({fieldOrders, endAt}); + return new Query(this._firestore, options); } /** @@ -1578,25 +1627,31 @@ export class Query { */ toProto(transactionId?: Uint8Array): api.IRunQueryRequest { const projectId = this.firestore.projectId; - const parentPath = this._path.parent()!.toQualifiedResourcePath(projectId); + const parentPath = + this._queryOptions.parentPath.toQualifiedResourcePath(projectId); + const reqOpts: api.IRunQueryRequest = { parent: parentPath.formattedName, structuredQuery: { from: [ { - collectionId: this._path.id, + collectionId: this._queryOptions.collectionId, }, ], }, }; + if (this._queryOptions.allDescendants) { + reqOpts.structuredQuery!.from![0].allDescendants = true; + } + const structuredQuery = reqOpts.structuredQuery!; - if (this._fieldFilters.length === 1) { - structuredQuery.where = this._fieldFilters[0].toProto(); - } else if (this._fieldFilters.length > 1) { + if (this._queryOptions.fieldFilters.length === 1) { + structuredQuery.where = this._queryOptions.fieldFilters[0].toProto(); + } else if (this._queryOptions.fieldFilters.length > 1) { const filters: api.StructuredQuery.IFilter[] = []; - for (const fieldFilter of this._fieldFilters) { + for (const fieldFilter of this._queryOptions.fieldFilters) { filters.push(fieldFilter.toProto()); } structuredQuery.where = { @@ -1607,9 +1662,9 @@ export class Query { }; } - if (this._fieldOrders.length) { + if (this._queryOptions.fieldOrders.length) { const orderBy: api.StructuredQuery.IOrder[] = []; - for (const fieldOrder of this._fieldOrders) { + for (const fieldOrder of this._queryOptions.fieldOrders) { orderBy.push(fieldOrder.toProto()); } structuredQuery.orderBy = orderBy; @@ -1719,10 +1774,12 @@ export class Query { return (doc1, doc2) => { // Add implicit sorting by name, using the last specified direction. const lastDirection: api.StructuredQuery.Direction = - this._fieldOrders.length === 0 ? + this._queryOptions.fieldOrders.length === 0 ? 'ASCENDING' : - this._fieldOrders[this._fieldOrders.length - 1].direction; - const orderBys = this._fieldOrders.concat( + this._queryOptions + .fieldOrders[this._queryOptions.fieldOrders.length - 1] + .direction; + const orderBys = this._queryOptions.fieldOrders.concat( new FieldOrder(FieldPath.documentId(), lastDirection)); for (const orderBy of orderBys) { @@ -1769,7 +1826,16 @@ export class CollectionReference extends Query { * @param path The Path of this collection. */ constructor(firestore: Firestore, path: ResourcePath) { - super(firestore, path); + super(firestore, QueryOptions.forCollectionQuery(path)); + } + + /** + * Returns a resource path for this collection. + * @private + */ + private get resourcePath() { + return this._queryOptions.parentPath.append( + this._queryOptions.collectionId); } /** @@ -1784,7 +1850,7 @@ export class CollectionReference extends Query { * console.log(`ID of the subcollection: ${collectionRef.id}`); */ get id(): string { - return this._path.id!; + return this._queryOptions.collectionId; } /** @@ -1801,7 +1867,7 @@ export class CollectionReference extends Query { * console.log(`Parent name: ${documentRef.path}`); */ get parent(): DocumentReference { - return new DocumentReference(this.firestore, this._path.parent()!); + return new DocumentReference(this.firestore, this._queryOptions.parentPath); } /** @@ -1817,7 +1883,7 @@ export class CollectionReference extends Query { * console.log(`Path of the subcollection: ${collectionRef.path}`); */ get path(): string { - return this._path.relativeName; + return this.resourcePath.relativeName; } /** @@ -1849,9 +1915,8 @@ export class CollectionReference extends Query { */ listDocuments(): Promise { return this.firestore.initializeIfNeeded().then(() => { - const resourcePath = - this._path.toQualifiedResourcePath(this.firestore.projectId); - const parentPath = resourcePath.parent()!; + const parentPath = this._queryOptions.parentPath.toQualifiedResourcePath( + this.firestore.projectId); const request: api.IListDocumentsRequest = { parent: parentPath.formattedName, @@ -1900,7 +1965,7 @@ export class CollectionReference extends Query { validateResourcePath('documentPath', documentPath!); } - const path = this._path.append(documentPath!); + const path = this.resourcePath.append(documentPath!); if (!path.isDocument) { throw new Error(`Value for argument "documentPath" must point to a document, but was "${ documentPath}". Your path does not contain an even number of components.`); @@ -2044,3 +2109,11 @@ function isArrayEqual boolean}>( return true; } + +/** + * Returns the first non-undefined value or `undefined` if no such value exists. + * @private + */ +function coalesce(...values: Array): T|undefined { + return values.find(value => value !== undefined); +} diff --git a/dev/system-test/firestore.ts b/dev/system-test/firestore.ts index 630e95974..3171dcd90 100644 --- a/dev/system-test/firestore.ts +++ b/dev/system-test/firestore.ts @@ -1158,6 +1158,103 @@ describe('Query class', () => { }); }); + it('can query collection groups', async () => { + // Use `randomCol` to get a random collection group name to use but ensure + // it starts with 'b' for predictable ordering. + const collectionGroup = 'b' + randomCol.id; + + const docPaths = [ + `abc/123/${collectionGroup}/cg-doc1`, + `abc/123/${collectionGroup}/cg-doc2`, `${collectionGroup}/cg-doc3`, + `${collectionGroup}/cg-doc4`, `def/456/${collectionGroup}/cg-doc5`, + `${collectionGroup}/virtual-doc/nested-coll/not-cg-doc`, + `x${collectionGroup}/not-cg-doc`, `${collectionGroup}x/not-cg-doc`, + `abc/123/${collectionGroup}x/not-cg-doc`, + `abc/123/x${collectionGroup}/not-cg-doc`, `abc/${collectionGroup}` + ]; + const batch = firestore.batch(); + for (const docPath of docPaths) { + batch.set(firestore.doc(docPath), {x: 1}); + } + await batch.commit(); + + const querySnapshot = + await firestore.collectionGroup(collectionGroup).get(); + expect(querySnapshot.docs.map(d => d.id)).to.deep.equal([ + 'cg-doc1', 'cg-doc2', 'cg-doc3', 'cg-doc4', 'cg-doc5' + ]); + }); + + it('can query collection groups with startAt / endAt by arbitrary documentId', + async () => { + // Use `randomCol` to get a random collection group name to use but + // ensure it starts with 'b' for predictable ordering. + const collectionGroup = 'b' + randomCol.id; + + const docPaths = [ + `a/a/${collectionGroup}/cg-doc1`, `a/b/a/b/${collectionGroup}/cg-doc2`, + `a/b/${collectionGroup}/cg-doc3`, `a/b/c/d/${collectionGroup}/cg-doc4`, + `a/c/${collectionGroup}/cg-doc5`, `${collectionGroup}/cg-doc6`, + `a/b/nope/nope` + ]; + const batch = firestore.batch(); + for (const docPath of docPaths) { + batch.set(firestore.doc(docPath), {x: 1}); + } + await batch.commit(); + + let querySnapshot = await firestore.collectionGroup(collectionGroup) + .orderBy(FieldPath.documentId()) + .startAt(`a/b`) + .endAt('a/b0') + .get(); + expect(querySnapshot.docs.map(d => d.id)).to.deep.equal([ + 'cg-doc2', 'cg-doc3', 'cg-doc4' + ]); + + querySnapshot = await firestore.collectionGroup(collectionGroup) + .orderBy(FieldPath.documentId()) + .startAfter('a/b') + .endBefore(`a/b/${collectionGroup}/cg-doc3`) + .get(); + expect(querySnapshot.docs.map(d => d.id)).to.deep.equal(['cg-doc2']); + }); + + it('can query collection groups with where filters on arbitrary documentId', + async () => { + // Use `randomCol` to get a random collection group name to use but + // ensure it starts with 'b' for predictable ordering. + const collectionGroup = 'b' + randomCol.id; + + const docPaths = [ + `a/a/${collectionGroup}/cg-doc1`, `a/b/a/b/${collectionGroup}/cg-doc2`, + `a/b/${collectionGroup}/cg-doc3`, `a/b/c/d/${collectionGroup}/cg-doc4`, + `a/c/${collectionGroup}/cg-doc5`, `${collectionGroup}/cg-doc6`, + `a/b/nope/nope` + ]; + const batch = firestore.batch(); + for (const docPath of docPaths) { + batch.set(firestore.doc(docPath), {x: 1}); + } + await batch.commit(); + + let querySnapshot = await firestore.collectionGroup(collectionGroup) + .where(FieldPath.documentId(), '>=', `a/b`) + .where(FieldPath.documentId(), '<=', 'a/b0') + .get(); + expect(querySnapshot.docs.map(d => d.id)).to.deep.equal([ + 'cg-doc2', 'cg-doc3', 'cg-doc4' + ]); + + querySnapshot = await firestore.collectionGroup(collectionGroup) + .where(FieldPath.documentId(), '>', `a/b`) + .where( + FieldPath.documentId(), '<', + `a/b/${collectionGroup}/cg-doc3`) + .get(); + expect(querySnapshot.docs.map(d => d.id)).to.deep.equal(['cg-doc2']); + }); + describe('watch', () => { type ExpectedChange = { type: string; doc: DocumentSnapshot; }; diff --git a/dev/test/query.ts b/dev/test/query.ts index e0e9781b2..75aa9f4a9 100644 --- a/dev/test/query.ts +++ b/dev/test/query.ts @@ -172,6 +172,10 @@ function offset(n: number): api.IStructuredQuery { }; } +function allDescendants(): api.IStructuredQuery { + return {from: [{collectionId: 'collectionId', allDescendants: true}]}; +} + function select(...fields: string[]): api.IStructuredQuery { const select: api.StructuredQuery.IProjection = { fields: [], @@ -1628,3 +1632,28 @@ describe('endBefore() interface', () => { }); }); }); + +describe('collectionGroup queries', () => { + it('serialize correctly', () => { + const overrides: ApiOverride = { + runQuery: (request) => { + queryEquals( + request, allDescendants(), fieldFilters('foo', 'EQUAL', 'bar')); + return stream(); + } + }; + return createInstance(overrides).then(firestore => { + const query = + firestore.collectionGroup('collectionId').where('foo', '==', 'bar'); + return query.get(); + }); + }); + + it('rejects slashes', () => { + return createInstance().then(firestore => { + expect(() => firestore.collectionGroup('foo/bar')) + .to.throw( + 'Invalid collectionId \'foo/bar\'. Collection IDs must not contain \'/\'.'); + }); + }); +}); diff --git a/dev/test/typescript.ts b/dev/test/typescript.ts index cb29d4c8b..156bf8f65 100644 --- a/dev/test/typescript.ts +++ b/dev/test/typescript.ts @@ -65,6 +65,7 @@ xdescribe('firestore.d.ts', () => { const collRef: CollectionReference = firestore.collection('coll'); const docRef1: DocumentReference = firestore.doc('coll/doc'); const docRef2: DocumentReference = firestore.doc('coll/doc'); + const collectionGroup: Query = firestore.collectionGroup('collectionId'); firestore.getAll(docRef1, docRef2).then((docs: DocumentSnapshot[]) => {}); firestore.getAll(docRef1, docRef2, {}) .then((docs: DocumentSnapshot[]) => {}); diff --git a/types/firestore.d.ts b/types/firestore.d.ts index 00f0cef5a..6eed865b9 100644 --- a/types/firestore.d.ts +++ b/types/firestore.d.ts @@ -138,6 +138,18 @@ declare namespace FirebaseFirestore { */ doc(documentPath: string): DocumentReference; + /** + * Creates and returns a new Query that includes all documents in the + * database that are contained in a collection or subcollection with the + * given collectionId. + * + * @param collectionId Identifies the collections to query over. Every + * collection or subcollection with this ID as the last segment of its path + * will be included. Cannot contain a slash. + * @return The created Query. + */ + collectionGroup(collectionId: string): Query; + /** * Retrieves multiple documents from Firestore. * From 7d646f85d8184fab6e420415a771919223a8e507 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Tue, 12 Mar 2019 10:57:53 +0800 Subject: [PATCH 2/3] Remove unused export --- dev/src/transaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/src/transaction.ts b/dev/src/transaction.ts index 334c63794..de6ba2fa7 100644 --- a/dev/src/transaction.ts +++ b/dev/src/transaction.ts @@ -433,7 +433,7 @@ export function parseGetAllArguments( * @param value The input to validate. * @param options Options that specify whether the ReadOptions can be omitted. */ -export function validateReadOptions( +function validateReadOptions( arg: number|string, value: unknown, options?: RequiredArgumentOptions): void { if (!validateOptional(value, options)) { From 32d35b53e4bd403c158326dd9cd00d381ad3921d Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Thu, 14 Mar 2019 08:30:05 +0800 Subject: [PATCH 3/3] Using Partial for QueryOptions --- dev/src/reference.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/dev/src/reference.ts b/dev/src/reference.ts index aaebe4fe4..c7eaf000b 100644 --- a/dev/src/reference.ts +++ b/dev/src/reference.ts @@ -907,18 +907,7 @@ export class QueryOptions { * Returns the union of the current and the provided options. * @private */ - with(settings: { - parentPath?: ResourcePath; - collectionId?: string; - allDescendants?: boolean; - fieldFilters?: FieldFilter[]; - fieldOrders?: FieldOrder[]; - startAt?: QueryCursor; - endAt?: QueryCursor; - limit?: number; - offset?: number; - projection?: api.StructuredQuery.IProjection; - }): QueryOptions { + with(settings: Partial): QueryOptions { return new QueryOptions( coalesce(settings.parentPath, this.parentPath)!, coalesce(settings.collectionId, this.collectionId)!,