Skip to content

Commit

Permalink
Adding FieldMask support to GetAll()
Browse files Browse the repository at this point in the history
  • Loading branch information
schmidt-sebastian committed Nov 9, 2018
1 parent e6a3a68 commit bea0b9d
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 62 deletions.
2 changes: 1 addition & 1 deletion dev/src/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1090,7 +1090,7 @@ export function validateSetOptions(
FieldPath.validateFieldPath(options.mergeFields[i]);
} catch (err) {
throw new Error(
`Argument at index ${i} is not a valid FieldPath. ${err.message}`);
`Element at index ${i} is not a valid FieldPath. ${err.message}`);
}
}
}
Expand Down
108 changes: 81 additions & 27 deletions dev/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ import {DocumentReference} from './reference';
import {isPlainObject, Serializer} from './serializer';
import {Timestamp} from './timestamp';
import {Transaction} from './transaction';
import {DocumentData, GapicClient, Settings, ValidationOptions} from './types';
import {DocumentData, GapicClient, ReadOptions, Settings, ValidationOptions} from './types';
import {AnyDuringMigration, AnyJs} from './types';
import {requestTag} from './util';
import {parseGetAllArguments, requestTag} from './util';
import {customObjectError, Validator} from './validate';
import {WriteBatch, WriteResult} from './write-batch';
import {validateUpdateMap} from './write-batch';
Expand Down Expand Up @@ -163,6 +163,18 @@ const MAX_DEPTH = 20;
* @typedef {Object} SetOptions
*/

/**
* An options object that can be used to configure the behavior of
* [getAll()]{@link Firestore#getAll} calls. By providing a `fieldMask`, these
* calls can be configured to only return a subset of fields.
*
* @property {Array<(string|FieldPath)>} fieldMask Specifies the set of fields
* to return and reduces the amount of data transmitted by the backend.
* Adding a field mask does not filter results. Documents do not need to
* contain values for all the fields of the mask to be part of the result set.
* @typedef {Object} ReadOptions
*/

/**
* The Firestore client represents a Firestore Database and is the entry point
* for all Firestore operations.
Expand Down Expand Up @@ -296,6 +308,7 @@ export class Firestore {
QueryValue: validateFieldValue,
ResourcePath: ResourcePath.validateResourcePath,
SetOptions: validateSetOptions,
ReadOptions: validateReadOptions,
UpdateMap: validateUpdateMap,
UpdatePrecondition: precondition =>
validatePrecondition(precondition, /* allowExists= */ false),
Expand Down Expand Up @@ -680,60 +693,66 @@ export class Firestore {
/**
* Retrieves multiple documents from Firestore.
*
* @param {...DocumentReference} documents The document references to receive.
* @param {DocumentReference} documentRef A `DocumentReferences` to receive.
* @param {Array.<DocumentReference|ReadOptions>} moreDocumentRefsOrReadOptions
* Additional `DocumentReferences` to receive, followed by an optional field
* mask.
* @returns {Promise<Array.<DocumentSnapshot>>} A Promise that
* contains an array with the resulting document snapshots.
*
* @example
* let documentRef1 = firestore.doc('col/doc1');
* let documentRef2 = firestore.doc('col/doc2');
* let docRef1 = firestore.doc('col/doc1');
* let docRef2 = firestore.doc('col/doc2');
*
* firestore.getAll(documentRef1, documentRef2).then(docs => {
* firestore.getAll(docRef1, docRef2, { fieldMask: ['user'] }).then(docs => {
* console.log(`First document: ${JSON.stringify(docs[0])}`);
* console.log(`Second document: ${JSON.stringify(docs[1])}`);
* });
*/
getAll(...documents: DocumentReference[]): Promise<DocumentSnapshot[]> {
documents = is.array(arguments[0]) ? arguments[0].slice() :
Array.prototype.slice.call(arguments);

for (let i = 0; i < documents.length; ++i) {
this._validator.isDocumentReference(i, documents[i]);
}

return this.getAll_(documents, requestTag());
getAll(
documentRef: DocumentReference,
...moreDocumentRefsOrReadOptions: Array<DocumentReference|ReadOptions>):
Promise<DocumentSnapshot[]> {
this._validator.minNumberOfArguments('Firestore.getAll', arguments, 1);

const {documents, fieldMask} = parseGetAllArguments(
this._validator, [documentRef, ...moreDocumentRefsOrReadOptions]);
return this.getAll_(documents, fieldMask, requestTag());
}

/**
* Internal method to retrieve multiple documents from Firestore, optionally
* as part of a transaction.
*
* @private
* @param {Array.<DocumentReference>} docRefs The documents to receive.
* @param {string} requestTag A unique client-assigned identifier for this
* request.
* @param {bytes=} transactionId transactionId - The transaction ID to use
* for this read.
* @returns {Promise<Array.<DocumentSnapshot>>} A Promise that contains an
* array with the resulting documents.
* @param docRefs The documents to receive.
* @param fieldMask An optional field mask to apply to this read.
* @param requestTag A unique client-assigned identifier for this request.
* @param transactionId The transaction ID to use for this read.
* @returns A Promise that contains an array with the resulting documents.
*/
getAll_(
docRefs: DocumentReference[], requestTag: string,
docRefs: DocumentReference[], fieldMask: FieldPath[]|null,
requestTag: string,
transactionId?: Uint8Array): Promise<DocumentSnapshot[]> {
const requestedDocuments = new Set();
const retrievedDocuments = new Map();

for (const docRef of docRefs) {
requestedDocuments.add(docRef.formattedName);
}

const request: api.IBatchGetDocumentsRequest = {
database: this.formattedName,
transaction: transactionId,
documents: Array.from(requestedDocuments)
};

for (const docRef of docRefs) {
requestedDocuments.add(docRef.formattedName);
if (fieldMask) {
const fieldPaths = fieldMask.map(fieldPath => fieldPath.formattedName);
request.mask = {fieldPaths};
}

request.documents = Array.from(requestedDocuments);

const self = this;

return self.readStream('batchGetDocuments', request, requestTag, true)
Expand Down Expand Up @@ -1345,6 +1364,41 @@ function validateDocumentData(
return true;
}

/**
* Validates the use of 'options' as ReadOptions and enforces that 'fieldMask'
* is an array of strings or field paths.
*
* @private
* @param options.fieldMask - The subset of fields to return from a read
* operation.
*/
export function validateReadOptions(
options: {fieldMask?: Array<string|FieldPath>}): boolean {
if (!is.object(options)) {
throw new Error('Input is not an object.');
}

if (options.fieldMask !== undefined && !Array.isArray(options.fieldMask)) {
throw new Error('"fieldMask" is not an array.');
}

if (options.fieldMask !== undefined) {
if (!Array.isArray(options.fieldMask)) {
throw new Error('"fieldMask" is not an array.');
}

for (let i = 0; i < options.fieldMask.length; ++i) {
try {
FieldPath.validateFieldPath(options.fieldMask[i]);
} catch (err) {
throw new Error(
`Element at index ${i} is not a valid FieldPath. ${err.message}`);
}
}
}
return true;
}

/**
* A logging function that takes a single string.
*
Expand Down
26 changes: 15 additions & 11 deletions dev/src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {DocumentSnapshot, Precondition} from './document';
import {Firestore, WriteBatch} from './index';
import {FieldPath} from './path';
import {DocumentReference, Query, QuerySnapshot} from './reference';
import {AnyDuringMigration, AnyJs, DocumentData, Precondition as PublicPrecondition, SetOptions, UpdateData} from './types';
import {AnyDuringMigration, AnyJs, DocumentData, Precondition as PublicPrecondition, ReadOptions, SetOptions, UpdateData} from './types';
import {parseGetAllArguments} from './util';
import {requestTag} from './util';

import api = proto.google.firestore.v1beta1;
Expand Down Expand Up @@ -117,7 +118,7 @@ export class Transaction {

if (refOrQuery instanceof DocumentReference) {
return this._firestore
.getAll_([refOrQuery], this._requestTag, this._transactionId)
.getAll_([refOrQuery], null, this._requestTag, this._transactionId)
.then(res => {
return Promise.resolve(res[0]);
});
Expand All @@ -134,7 +135,10 @@ export class Transaction {
* Retrieves multiple documents from Firestore. Holds a pessimistic lock on
* all returned documents.
*
* @param {...DocumentReference} documents The document references to receive.
* @param {DocumentReference} documentRef A `DocumentReferences` to receive.
* @param {Array.<DocumentReference|ReadOptions>} moreDocumentRefsOrReadOptions
* Additional `DocumentReferences` to receive, followed by an optional field
* mask.
* @returns {Promise<Array.<DocumentSnapshot>>} A Promise that
* contains an array with the resulting document snapshots.
*
Expand All @@ -151,21 +155,21 @@ export class Transaction {
* });
* });
*/
getAll(...documents: DocumentReference[]): Promise<DocumentSnapshot[]> {
getAll(
documentRef: DocumentReference,
...moreDocumentRefsOrReadOptions: Array<DocumentReference|ReadOptions>):
Promise<DocumentSnapshot[]> {
if (!this._writeBatch.isEmpty) {
throw new Error(READ_AFTER_WRITE_ERROR_MSG);
}

documents = Array.isArray(arguments[0]) ?
arguments[0].slice() :
Array.prototype.slice.call(arguments);
this._validator.minNumberOfArguments('Transaction.getAll', arguments, 1);

for (let i = 0; i < documents.length; ++i) {
this._validator.isDocumentReference(i, documents[i]);
}
const {documents, fieldMask} = parseGetAllArguments(
this._validator, [documentRef, ...moreDocumentRefsOrReadOptions]);

return this._firestore.getAll_(
documents, this._requestTag, this._transactionId);
documents, fieldMask, this._requestTag, this._transactionId);
}

/**
Expand Down
16 changes: 16 additions & 0 deletions dev/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,22 @@ export interface SetOptions {
readonly mergeFields?: Array<string|FieldPath>;
}

/**
* An options object that can be used to configure the behavior of `getAll()`
* calls. By providing a `fieldMask`, these calls can be configured to only
* return a subset of fields.
*/
export interface ReadOptions {
/**
* Specifies the set of fields to return and reduces the amount of data
* transmitted by the backend.
*
* Adding a field mask does not filter results. Documents do not need to
* contain values for all the fields of the mask to be part of the result set.
*/
readonly fieldMask?: Array<string|FieldPath>;
}

/**
* Internal user data validation options.
* @private
Expand Down
51 changes: 51 additions & 0 deletions dev/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
* limitations under the License.
*/

import {FieldPath} from './path';
import {DocumentReference} from './reference';
import {isPlainObject} from './serializer';
import {AnyDuringMigration, ReadOptions} from './types';

/**
* Generate a unique client-side identifier.
*
Expand Down Expand Up @@ -43,3 +48,49 @@ export function autoId(): string {
export function requestTag(): string {
return autoId().substr(0, 5);
}

/**
* Parses the arguments for the `getAll()` call supported by both the Firestore
* and Transaction class.
*
* @private
* @param validator The argument validator to use.
* @param documentRefsOrReadOptions An array of document references followed by
* an optional ReadOptions object.
*/
export function parseGetAllArguments(
validator: AnyDuringMigration,
documentRefsOrReadOptions: Array<DocumentReference|ReadOptions>):
{documents: DocumentReference[], fieldMask: FieldPath[]|null} {
let documents: DocumentReference[];
let readOptions: ReadOptions|undefined = undefined;

const usesVarags = !Array.isArray(documentRefsOrReadOptions[0]);

if (usesVarags) {
if (documentRefsOrReadOptions.length > 0 &&
isPlainObject(
documentRefsOrReadOptions[documentRefsOrReadOptions.length - 1])) {
readOptions = documentRefsOrReadOptions.pop() as ReadOptions;
documents = documentRefsOrReadOptions as DocumentReference[];
} else {
documents = documentRefsOrReadOptions as DocumentReference[];
}
} else {
// Support an array of document references as the first argument for
// backwards compatibility.
documents = documentRefsOrReadOptions[0] as DocumentReference[];
readOptions = documentRefsOrReadOptions[1] as ReadOptions;
}

for (let i = 0; i < documents.length; ++i) {
validator.isDocumentReference(i, documents[i]);
}

validator.isOptionalReadOptions('options', readOptions);
const fieldMask = readOptions && readOptions.fieldMask ?
readOptions.fieldMask.map(
fieldPath => FieldPath.fromArgument(fieldPath)) :
null;
return {fieldMask, documents};
}
25 changes: 25 additions & 0 deletions dev/system-test/firestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ describe('Firestore class', () => {
expect(docs.length).to.equal(2);
});
});

it('getAll() supports field mask', () => {
const ref1 = randomCol.doc('doc1');
return ref1.set({foo: 'a', bar: 'b'})
.then(() => {
return firestore.getAll(ref1, {fieldMask: ['foo']});
})
.then(docs => {
expect(docs[0].data()).to.deep.equal({foo: 'a'});
});
});
});

describe('CollectionReference class', () => {
Expand Down Expand Up @@ -1372,6 +1383,20 @@ describe('Transaction class', () => {
});
});

it('getAll() supports field mask', () => {
const ref1 = randomCol.doc('doc1');
return ref1.set({foo: 'a', bar: 'b'}).then(() => {
return firestore
.runTransaction(updateFunction => {
return updateFunction.getAll(ref1, {fieldMask: ['foo']})
.then(([doc]) => doc);
})
.then(doc => {
expect(doc.data()).to.deep.equal({foo: 'a'});
});
});
});

it('has get() with query', () => {
const ref = randomCol.doc('doc');
const query = randomCol.where('foo', '==', 'bar');
Expand Down
2 changes: 1 addition & 1 deletion dev/test/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -971,7 +971,7 @@ describe('set document', () => {
});
})
.to.throw(
/Argument "options" is not a valid SetOptions. Argument at index 0 is not a valid FieldPath./);
/Argument "options" is not a valid SetOptions. Element at index 0 is not a valid FieldPath./);

expect(() => {
firestore.doc('collectionId/documentId').set({foo: 'bar'}, {
Expand Down
Loading

0 comments on commit bea0b9d

Please sign in to comment.