"use strict"; /*! * Copyright 2019 Google Inc. All Rights Reserved. * * 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. */ Object.defineProperty(exports, "__esModule", { value: true }); const deepEqual = require('deep-equal'); const assert = require("assert"); const field_value_1 = require("./field-value"); const path_1 = require("./path"); const util_1 = require("./util"); /** * Returns a builder for DocumentSnapshot and QueryDocumentSnapshot instances. * Invoke `.build()' to assemble the final snapshot. * * @private */ class DocumentSnapshotBuilder { // We include the DocumentReference in the constructor in order to allow the // DocumentSnapshotBuilder to be typed with when it is constructed. constructor(ref) { this.ref = ref; } /** * Builds the DocumentSnapshot. * * @private * @returns Returns either a QueryDocumentSnapshot (if `fieldsProto` was * provided) or a DocumentSnapshot. */ build() { assert((this.fieldsProto !== undefined) === (this.createTime !== undefined), 'Create time should be set iff document exists.'); assert((this.fieldsProto !== undefined) === (this.updateTime !== undefined), 'Update time should be set iff document exists.'); return this.fieldsProto ? new QueryDocumentSnapshot(this.ref, this.fieldsProto, this.readTime, this.createTime, this.updateTime) : new DocumentSnapshot(this.ref, undefined, this.readTime); } } exports.DocumentSnapshotBuilder = DocumentSnapshotBuilder; /** * A DocumentSnapshot is an immutable representation for a document in a * Firestore database. The data can be extracted with * [data()]{@link DocumentSnapshot#data} or * [get(fieldPath)]{@link DocumentSnapshot#get} to get a * specific field. * *

For a DocumentSnapshot that points to a non-existing document, any data * access will return 'undefined'. You can use the * [exists]{@link DocumentSnapshot#exists} property to explicitly verify a * document's existence. * * @class */ class DocumentSnapshot { /** * @hideconstructor * * @param ref The reference to the document. * @param _fieldsProto The fields of the Firestore `Document` Protobuf backing * this document (or undefined if the document does not exist). * @param readTime The time when this snapshot was read (or undefined if * the document exists only locally). * @param createTime The time when the document was created (or undefined if * the document does not exist). * @param updateTime The time when the document was last updated (or undefined * if the document does not exist). */ constructor(ref, _fieldsProto, readTime, createTime, updateTime) { this._fieldsProto = _fieldsProto; this._ref = ref; this._serializer = ref.firestore._serializer; this._readTime = readTime; this._createTime = createTime; this._updateTime = updateTime; } /** * Creates a DocumentSnapshot from an object. * * @private * @param ref The reference to the document. * @param obj The object to store in the DocumentSnapshot. * @return The created DocumentSnapshot. */ static fromObject(ref, obj) { const serializer = ref.firestore._serializer; return new DocumentSnapshot(ref, serializer.encodeFields(obj)); } /** * Creates a DocumentSnapshot from an UpdateMap. * * This methods expands the top-level field paths in a JavaScript map and * turns { foo.bar : foobar } into { foo { bar : foobar }} * * @private * @param ref The reference to the document. * @param data The field/value map to expand. * @return The created DocumentSnapshot. */ static fromUpdateMap(ref, data) { const serializer = ref.firestore._serializer; /** * Merges 'value' at the field path specified by the path array into * 'target'. */ function merge(target, value, path, pos) { const key = path[pos]; const isLast = pos === path.length - 1; if (target[key] === undefined) { if (isLast) { if (value instanceof field_value_1.FieldTransform) { // If there is already data at this path, we need to retain it. // Otherwise, we don't include it in the DocumentSnapshot. return !util_1.isEmpty(target) ? target : null; } // The merge is done. const leafNode = serializer.encodeValue(value); if (leafNode) { target[key] = leafNode; } return target; } else { // We need to expand the target object. const childNode = { mapValue: { fields: {}, }, }; const nestedValue = merge(childNode.mapValue.fields, value, path, pos + 1); if (nestedValue) { childNode.mapValue.fields = nestedValue; target[key] = childNode; return target; } else { return !util_1.isEmpty(target) ? target : null; } } } else { assert(!isLast, "Can't merge current value into a nested object"); target[key].mapValue.fields = merge(target[key].mapValue.fields, value, path, pos + 1); return target; } } const res = {}; for (const [key, value] of data) { const path = key.toArray(); merge(res, value, path, 0); } return new DocumentSnapshot(ref, res); } /** * True if the document exists. * * @type {boolean} * @name DocumentSnapshot#exists * @readonly * * @example * let documentRef = firestore.doc('col/doc'); * * documentRef.get().then((documentSnapshot) => { * if (documentSnapshot.exists) { * console.log(`Data: ${JSON.stringify(documentSnapshot.data())}`); * } * }); */ get exists() { return this._fieldsProto !== undefined; } /** * A [DocumentReference]{@link DocumentReference} for the document * stored in this snapshot. * * @type {DocumentReference} * @name DocumentSnapshot#ref * @readonly * * @example * let documentRef = firestore.doc('col/doc'); * * documentRef.get().then((documentSnapshot) => { * if (documentSnapshot.exists) { * console.log(`Found document at '${documentSnapshot.ref.path}'`); * } * }); */ get ref() { return this._ref; } /** * The ID of the document for which this DocumentSnapshot contains data. * * @type {string} * @name DocumentSnapshot#id * @readonly * * @example * let documentRef = firestore.doc('col/doc'); * * documentRef.get().then((documentSnapshot) => { * if (documentSnapshot.exists) { * console.log(`Document found with name '${documentSnapshot.id}'`); * } * }); */ get id() { return this._ref.id; } /** * The time the document was created. Undefined for documents that don't * exist. * * @type {Timestamp|undefined} * @name DocumentSnapshot#createTime * @readonly * * @example * let documentRef = firestore.doc('col/doc'); * * documentRef.get().then(documentSnapshot => { * if (documentSnapshot.exists) { * let createTime = documentSnapshot.createTime; * console.log(`Document created at '${createTime.toDate()}'`); * } * }); */ get createTime() { return this._createTime; } /** * The time the document was last updated (at the time the snapshot was * generated). Undefined for documents that don't exist. * * @type {Timestamp|undefined} * @name DocumentSnapshot#updateTime * @readonly * * @example * let documentRef = firestore.doc('col/doc'); * * documentRef.get().then(documentSnapshot => { * if (documentSnapshot.exists) { * let updateTime = documentSnapshot.updateTime; * console.log(`Document updated at '${updateTime.toDate()}'`); * } * }); */ get updateTime() { return this._updateTime; } /** * The time this snapshot was read. * * @type {Timestamp} * @name DocumentSnapshot#readTime * @readonly * * @example * let documentRef = firestore.doc('col/doc'); * * documentRef.get().then(documentSnapshot => { * let readTime = documentSnapshot.readTime; * console.log(`Document read at '${readTime.toDate()}'`); * }); */ get readTime() { if (this._readTime === undefined) { throw new Error(`Called 'readTime' on a local document`); } return this._readTime; } /** * Retrieves all fields in the document as an object. Returns 'undefined' if * the document doesn't exist. * * @returns {T|undefined} An object containing all fields in the document or * 'undefined' if the document doesn't exist. * * @example * let documentRef = firestore.doc('col/doc'); * * documentRef.get().then(documentSnapshot => { * let data = documentSnapshot.data(); * console.log(`Retrieved data: ${JSON.stringify(data)}`); * }); */ data() { const fields = this._fieldsProto; if (fields === undefined) { return undefined; } const obj = {}; for (const prop of Object.keys(fields)) { obj[prop] = this._serializer.decodeValue(fields[prop]); } return this.ref._converter.fromFirestore(obj); } /** * Retrieves the field specified by `field`. * * @param {string|FieldPath} field The field path * (e.g. 'foo' or 'foo.bar') to a specific field. * @returns {*} The data at the specified field location or undefined if no * such field exists. * * @example * let documentRef = firestore.doc('col/doc'); * * documentRef.set({ a: { b: 'c' }}).then(() => { * return documentRef.get(); * }).then(documentSnapshot => { * let field = documentSnapshot.get('a.b'); * console.log(`Retrieved field value: ${field}`); * }); */ // We deliberately use `any` in the external API to not impose type-checking // on end users. // tslint:disable-next-line no-any get(field) { // tslint:disable-line no-any path_1.validateFieldPath('field', field); const protoField = this.protoField(field); if (protoField === undefined) { return undefined; } return this._serializer.decodeValue(protoField); } /** * Retrieves the field specified by 'fieldPath' in its Protobuf JS * representation. * * @private * @param field The path (e.g. 'foo' or 'foo.bar') to a specific field. * @returns The Protobuf-encoded data at the specified field location or * undefined if no such field exists. */ protoField(field) { let fields = this._fieldsProto; if (fields === undefined) { return undefined; } const components = path_1.FieldPath.fromArgument(field).toArray(); while (components.length > 1) { fields = fields[components.shift()]; if (!fields || !fields.mapValue) { return undefined; } fields = fields.mapValue.fields; } return fields[components[0]]; } /** * Checks whether this DocumentSnapshot contains any fields. * * @private * @return {boolean} */ get isEmpty() { return this._fieldsProto === undefined || util_1.isEmpty(this._fieldsProto); } /** * Convert a document snapshot to the Firestore 'Document' Protobuf. * * @private * @returns The document in the format the API expects. */ toProto() { return { update: { name: this._ref.formattedName, fields: this._fieldsProto, }, }; } /** * Returns true if the document's data and path in this `DocumentSnapshot` is * equal to the provided value. * * @param {*} other The value to compare against. * @return {boolean} true if this `DocumentSnapshot` is equal to the provided * value. */ isEqual(other) { // Since the read time is different on every document read, we explicitly // ignore all document metadata in this comparison. return (this === other || (other instanceof DocumentSnapshot && this._ref.isEqual(other._ref) && deepEqual(this._fieldsProto, other._fieldsProto, { strict: true }))); } } exports.DocumentSnapshot = DocumentSnapshot; /** * A QueryDocumentSnapshot contains data read from a document in your * Firestore database as part of a query. The document is guaranteed to exist * and its data can be extracted with [data()]{@link QueryDocumentSnapshot#data} * or [get()]{@link DocumentSnapshot#get} to get a specific field. * * A QueryDocumentSnapshot offers the same API surface as a * {@link DocumentSnapshot}. Since query results contain only existing * documents, the [exists]{@link DocumentSnapshot#exists} property will * always be true and [data()]{@link QueryDocumentSnapshot#data} will never * return 'undefined'. * * @class * @extends DocumentSnapshot */ class QueryDocumentSnapshot extends DocumentSnapshot { /** * @hideconstructor * * @param ref The reference to the document. * @param fieldsProto The fields of the Firestore `Document` Protobuf backing * this document. * @param readTime The time when this snapshot was read. * @param createTime The time when the document was created. * @param updateTime The time when the document was last updated. */ constructor(ref, fieldsProto, readTime, createTime, updateTime) { super(ref, fieldsProto, readTime, createTime, updateTime); } /** * The time the document was created. * * @type {Timestamp} * @name QueryDocumentSnapshot#createTime * @readonly * @override * * @example * let query = firestore.collection('col'); * * query.get().forEach(snapshot => { * console.log(`Document created at '${snapshot.createTime.toDate()}'`); * }); */ get createTime() { return super.createTime; } /** * The time the document was last updated (at the time the snapshot was * generated). * * @type {Timestamp} * @name QueryDocumentSnapshot#updateTime * @readonly * @override * * @example * let query = firestore.collection('col'); * * query.get().forEach(snapshot => { * console.log(`Document updated at '${snapshot.updateTime.toDate()}'`); * }); */ get updateTime() { return super.updateTime; } /** * Retrieves all fields in the document as an object. * * @override * * @returns {T} An object containing all fields in the document. * * @example * let query = firestore.collection('col'); * * query.get().forEach(documentSnapshot => { * let data = documentSnapshot.data(); * console.log(`Retrieved data: ${JSON.stringify(data)}`); * }); */ data() { const data = super.data(); if (!data) { throw new Error('The data in a QueryDocumentSnapshot should always exist.'); } return data; } } exports.QueryDocumentSnapshot = QueryDocumentSnapshot; /** * A Firestore Document Mask contains the field paths affected by an update. * * @class * @private */ class DocumentMask { /** * @private * @hideconstructor * * @param fieldPaths The field paths in this mask. */ constructor(fieldPaths) { this._sortedPaths = fieldPaths; this._sortedPaths.sort((a, b) => a.compareTo(b)); } /** * Creates a document mask with the field paths of a document. * * @private * @param data A map with fields to modify. Only the keys are used to extract * the document mask. */ static fromUpdateMap(data) { const fieldPaths = []; data.forEach((value, key) => { if (!(value instanceof field_value_1.FieldTransform) || value.includeInDocumentMask) { fieldPaths.push(path_1.FieldPath.fromArgument(key)); } }); return new DocumentMask(fieldPaths); } /** * Creates a document mask from an array of field paths. * * @private * @param fieldMask A list of field paths. */ static fromFieldMask(fieldMask) { const fieldPaths = []; for (const fieldPath of fieldMask) { fieldPaths.push(path_1.FieldPath.fromArgument(fieldPath)); } return new DocumentMask(fieldPaths); } /** * Creates a document mask with the field names of a document. * * @private * @param data An object with fields to modify. Only the keys are used to * extract the document mask. */ static fromObject(data) { const fieldPaths = []; function extractFieldPaths(currentData, currentPath) { let isEmpty = true; for (const key of Object.keys(currentData)) { isEmpty = false; // We don't split on dots since fromObject is called with // DocumentData. const childSegment = new path_1.FieldPath(key); const childPath = currentPath ? currentPath.append(childSegment) : childSegment; const value = currentData[key]; if (value instanceof field_value_1.FieldTransform) { if (value.includeInDocumentMask) { fieldPaths.push(childPath); } } else if (util_1.isPlainObject(value)) { extractFieldPaths(value, childPath); } else { fieldPaths.push(childPath); } } // Add a field path for an explicitly updated empty map. if (currentPath && isEmpty) { fieldPaths.push(currentPath); } } extractFieldPaths(data); return new DocumentMask(fieldPaths); } /** * Returns true if this document mask contains no fields. * * @private * @return {boolean} Whether this document mask is empty. */ get isEmpty() { return this._sortedPaths.length === 0; } /** * Removes the specified values from a sorted field path array. * * @private * @param input A sorted array of FieldPaths. * @param values An array of FieldPaths to remove. */ static removeFromSortedArray(input, values) { for (let i = 0; i < input.length;) { let removed = false; for (const fieldPath of values) { if (input[i].isEqual(fieldPath)) { input.splice(i, 1); removed = true; break; } } if (!removed) { ++i; } } } /** * Removes the field path specified in 'fieldPaths' from this document mask. * * @private * @param fieldPaths An array of FieldPaths. */ removeFields(fieldPaths) { DocumentMask.removeFromSortedArray(this._sortedPaths, fieldPaths); } /** * Returns whether this document mask contains 'fieldPath'. * * @private * @param fieldPath The field path to test. * @return Whether this document mask contains 'fieldPath'. */ contains(fieldPath) { for (const sortedPath of this._sortedPaths) { const cmp = sortedPath.compareTo(fieldPath); if (cmp === 0) { return true; } else if (cmp > 0) { return false; } } return false; } /** * Removes all properties from 'data' that are not contained in this document * mask. * * @private * @param data An object to filter. * @return A shallow copy of the object filtered by this document mask. */ applyTo(data) { /*! * Applies this DocumentMask to 'data' and computes the list of field paths * that were specified in the mask but are not present in 'data'. */ const applyDocumentMask = (data) => { const remainingPaths = this._sortedPaths.slice(0); const processObject = (currentData, currentPath) => { let result = null; Object.keys(currentData).forEach(key => { const childPath = currentPath ? currentPath.append(key) : new path_1.FieldPath(key); if (this.contains(childPath)) { DocumentMask.removeFromSortedArray(remainingPaths, [childPath]); result = result || {}; result[key] = currentData[key]; } else if (util_1.isObject(currentData[key])) { const childObject = processObject(currentData[key], childPath); if (childObject) { result = result || {}; result[key] = childObject; } } }); return result; }; // processObject() returns 'null' if the DocumentMask is empty. const filteredData = processObject(data) || {}; return { filteredData, remainingPaths, }; }; const result = applyDocumentMask(data); if (result.remainingPaths.length !== 0) { throw new Error(`Input data is missing for field "${result.remainingPaths[0]}".`); } return result.filteredData; } /** * Converts a document mask to the Firestore 'DocumentMask' Proto. * * @private * @returns A Firestore 'DocumentMask' Proto. */ toProto() { if (this.isEmpty) { return {}; } const encodedPaths = []; for (const fieldPath of this._sortedPaths) { encodedPaths.push(fieldPath.formattedName); } return { fieldPaths: encodedPaths, }; } } exports.DocumentMask = DocumentMask; /** * A Firestore Document Transform. * * A DocumentTransform contains pending server-side transforms and their * corresponding field paths. * * @private * @class */ class DocumentTransform { /** * @private * @hideconstructor * * @param ref The DocumentReference for this transform. * @param transforms A Map of FieldPaths to FieldTransforms. */ constructor(ref, transforms) { this.ref = ref; this.transforms = transforms; } /** * Generates a DocumentTransform from a JavaScript object. * * @private * @param ref The `DocumentReference` to use for the DocumentTransform. * @param obj The object to extract the transformations from. * @returns The Document Transform. */ static fromObject(ref, obj) { const updateMap = new Map(); for (const prop of Object.keys(obj)) { updateMap.set(new path_1.FieldPath(prop), obj[prop]); } return DocumentTransform.fromUpdateMap(ref, updateMap); } /** * Generates a DocumentTransform from an Update Map. * * @private * @param ref The `DocumentReference` to use for the DocumentTransform. * @param data The update data to extract the transformations from. * @returns The Document Transform. */ static fromUpdateMap(ref, data) { const transforms = new Map(); function encode_(val, path, allowTransforms) { if (val instanceof field_value_1.FieldTransform && val.includeInDocumentTransform) { if (allowTransforms) { transforms.set(path, val); } else { throw new Error(`${val.methodName}() is not supported inside of array values.`); } } else if (Array.isArray(val)) { for (let i = 0; i < val.length; ++i) { // We need to verify that no array value contains a document transform encode_(val[i], path.append(String(i)), false); } } else if (util_1.isPlainObject(val)) { for (const prop of Object.keys(val)) { encode_(val[prop], path.append(new path_1.FieldPath(prop)), allowTransforms); } } } data.forEach((value, key) => { encode_(value, path_1.FieldPath.fromArgument(key), true); }); return new DocumentTransform(ref, transforms); } /** * Whether this DocumentTransform contains any actionable transformations. * * @private */ get isEmpty() { return this.transforms.size === 0; } /** * Returns the array of fields in this DocumentTransform. * * @private */ get fields() { return Array.from(this.transforms.keys()); } /** * Validates the user provided field values in this document transform. * @private */ validate() { this.transforms.forEach(transform => transform.validate()); } /** * Converts a document transform to the Firestore 'DocumentTransform' Proto. * * @private * @param serializer The Firestore serializer * @returns A Firestore 'DocumentTransform' Proto or 'null' if this transform * is empty. */ toProto(serializer) { if (this.isEmpty) { return null; } const fieldTransforms = []; for (const [path, transform] of this.transforms) { fieldTransforms.push(transform.toProto(serializer, path)); } return { transform: { document: this.ref.formattedName, fieldTransforms, }, }; } } exports.DocumentTransform = DocumentTransform; /** * A Firestore Precondition encapsulates options for database writes. * * @private * @class */ class Precondition { /** * @private * @hideconstructor * * @param options.exists - Whether the referenced document should exist in * Firestore, * @param options.lastUpdateTime - The last update time of the referenced * document in Firestore. * @param options */ constructor(options) { if (options !== undefined) { this._exists = options.exists; this._lastUpdateTime = options.lastUpdateTime; } } /** * Generates the Protobuf `Preconditon` object for this precondition. * * @private * @returns The `Preconditon` Protobuf object or 'null' if there are no * preconditions. */ toProto() { if (this.isEmpty) { return null; } const proto = {}; if (this._lastUpdateTime !== undefined) { const valueProto = this._lastUpdateTime.toProto(); proto.updateTime = valueProto.timestampValue; } else { proto.exists = this._exists; } return proto; } /** * Whether this DocumentTransform contains any enforcement. * * @private */ get isEmpty() { return this._exists === undefined && !this._lastUpdateTime; } } exports.Precondition = Precondition; //# sourceMappingURL=document.js.map